From 17667255dd4a98ac4050d09ebe8f84054444ccff Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 21 Jun 2024 12:02:42 -0400 Subject: [PATCH] PR Updates including documentation and test script --- src/tmx/TmxUtils/src/Vector3d.h | 4 +- .../CDASimAdapter/src/CDASimConnection.cpp | 21 +---- .../src/include/CDASimConnection.hpp | 6 -- .../test/TestCDASimConnection.cpp | 19 ----- src/v2i-hub/MUSTSensorDriverPlugin/README.md | 49 ++++++++++++ .../docs/communication_diagram.png | Bin 0 -> 37467 bytes .../MUSTSensorDriverPlugin/manifest.json | 19 ++--- .../scripts/MockMUSTSensor.py | 75 ++++++++++++++++++ .../src/MUSTSensorDetection.cpp | 8 +- .../src/MUSTSensorDetection.h | 2 +- .../src/MUSTSensorDriverPlugin.cpp | 26 ++++-- .../src/MUSTSensorDriverPlugin.h | 7 +- 12 files changed, 171 insertions(+), 65 deletions(-) create mode 100644 src/v2i-hub/MUSTSensorDriverPlugin/README.md create mode 100644 src/v2i-hub/MUSTSensorDriverPlugin/docs/communication_diagram.png create mode 100644 src/v2i-hub/MUSTSensorDriverPlugin/scripts/MockMUSTSensor.py diff --git a/src/tmx/TmxUtils/src/Vector3d.h b/src/tmx/TmxUtils/src/Vector3d.h index d8e6a1dfb..6d4e9f184 100644 --- a/src/tmx/TmxUtils/src/Vector3d.h +++ b/src/tmx/TmxUtils/src/Vector3d.h @@ -4,7 +4,7 @@ namespace tmx::utils { /// Three dimensional Vector - typedef struct Vector3d + using Vector3d = struct Vector3d { Vector3d() : X(0), Y(0), Z(0) {} @@ -14,6 +14,6 @@ namespace tmx::utils { double X; double Y; double Z; - } Vector3d; + }; } // namespace tmx::utils diff --git a/src/v2i-hub/CDASimAdapter/src/CDASimConnection.cpp b/src/v2i-hub/CDASimAdapter/src/CDASimConnection.cpp index 4e6d08ab4..4ab7cabd7 100644 --- a/src/v2i-hub/CDASimAdapter/src/CDASimConnection.cpp +++ b/src/v2i-hub/CDASimAdapter/src/CDASimConnection.cpp @@ -132,7 +132,7 @@ namespace CDASimAdapter{ tmx::messages::TimeSyncMessage msg; msg.clear(); if (time_sync_listener) { - std::string str_msg = consume_server_message(time_sync_listener); + std::string str_msg = time_sync_listener->stringTimedReceive(); msg.set_contents( str_msg ); } else { @@ -148,7 +148,7 @@ namespace CDASimAdapter{ externalObj.clear(); if(sensor_detected_object_listener) { - std::string str_msg = consume_server_message(sensor_detected_object_listener); + std::string str_msg = sensor_detected_object_listener->stringTimedReceive(); externalObj.set_contents(str_msg); } else @@ -180,23 +180,6 @@ namespace CDASimAdapter{ return ""; } - std::string CDASimConnection::consume_server_message( const std::shared_ptr _server) const { - std::vector msg(4000); - int num_of_bytes = _server->TimedReceive(msg.data(),4000, 5); - if (num_of_bytes > 0 ) { - msg.resize(num_of_bytes); - std::string ret(msg.data()); - PLOG(logDEBUG) << "UDP Server message received : " << ret << " of size " << num_of_bytes << std::endl; - return ret; - } - else if ( num_of_bytes == 0 ) { - throw UdpServerRuntimeError("Received empty message!"); - } - else { - throw UdpServerRuntimeError("Listen timed out after 5 ms!"); - } - return ""; - } std::string CDASimConnection::consume_v2x_message_from_simulation() const { if ( carma_simulation_listener) { diff --git a/src/v2i-hub/CDASimAdapter/src/include/CDASimConnection.hpp b/src/v2i-hub/CDASimAdapter/src/include/CDASimConnection.hpp index b79cd3534..3a9bdf1a4 100644 --- a/src/v2i-hub/CDASimAdapter/src/include/CDASimConnection.hpp +++ b/src/v2i-hub/CDASimAdapter/src/include/CDASimConnection.hpp @@ -64,12 +64,6 @@ namespace CDASimAdapter { * @param _client UDP client to forward message with. */ void forward_message(const std::string &v2x_message, const std::shared_ptr _client ) const ; - /** - * @brief Method to consume incoming std::string message from UDP Server. - * @param _server UDP Server to consume string message from. - * @return string of message. - */ - std::string consume_server_message( const std::shared_ptr _server ) const; /** * @brief Method to consume incoming std::string message in hex format from UDP Server. diff --git a/src/v2i-hub/CDASimAdapter/test/TestCDASimConnection.cpp b/src/v2i-hub/CDASimAdapter/test/TestCDASimConnection.cpp index c1ec10d4d..710dc49d8 100644 --- a/src/v2i-hub/CDASimAdapter/test/TestCDASimConnection.cpp +++ b/src/v2i-hub/CDASimAdapter/test/TestCDASimConnection.cpp @@ -59,25 +59,6 @@ namespace CDASimAdapter { connection->forward_message(test_message, client); } - TEST_F( TestCDASimConnection, consume_msg){ - - std::shared_ptr server = std::make_shared(); - char *msg_data = new char(); - char test_string[] = "Test Message"; - EXPECT_CALL( *server, TimedReceive(_, _, _) ).Times(2). - WillOnce(testing::DoAll(Return(-1))). - WillRepeatedly( testing::DoAll( SetArrayArgument<0>(test_string, test_string + strlen(test_string) + 1),Return(10))); - ASSERT_THROW(connection->consume_server_message(server), UdpServerRuntimeError); - - std::string msg = connection->consume_server_message(server); - - std::string compare_str; - compare_str = test_string; - ASSERT_EQ(compare_str.compare( msg ) , 0); - delete msg_data; - - } - TEST_F( TestCDASimConnection, setup_upd_connection) { ASSERT_TRUE(connection->setup_udp_connection("127.0.0.1", "127.0.0.1", 4567, 4568, 4569, 4570)); } diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/README.md b/src/v2i-hub/MUSTSensorDriverPlugin/README.md new file mode 100644 index 000000000..bb2e135c9 --- /dev/null +++ b/src/v2i-hub/MUSTSensorDriverPlugin/README.md @@ -0,0 +1,49 @@ +# MUST Sensor Driver Plugin Documentation + +## Introduction + +MUST (Mobile Unit for Sensing Traffic) Sensor is from AI Waysion described in more detail [here](https://www.aiwaysion.com/technology).This is a camera based sensor that is planned to be used for cooperative perception in freight use cases.. The MUST Sensor provides detections via UDP packets made up of CSV (Comma Separated Values) string data. The V2X-Hub MUST Sensor Driver Plugin will then consume these messages and translate them to **Sensor Detected Object** messages, which is V2X-Hub's generic detection message. This message is consumable by the **CARMA Streets Sensor Data Sharing Service** which will generate **Sensor Data Sharing Message**s according to the J3224 standard for broadcast to other traffic actors in the area. + +## Related Plugins + +A list of plugins related to the MUST Sensor Driver Plugin. + +### Immediate Forward Plugin + +For RSU Immediate Message Forwarding (IMF) functionality forward SDSMs (Sensor Data Sharing Message). + +### CARMA Streets Plugin + +For forwarding detection data (SensorDetectedObject) to **Sensor Data Sharing Service** for creation of SDSMs. + +## Configuration/Deployment + +This plugin has several configuration parameters. Below these are listed out as together with descriptions on how to set them + +**DetectionReceiverIp**: This is the IP address on which V2X-Hub will listen for detections. In most scenarios this can be left at default since 127.0.0.1 should resolve to the IP address of V2X-Hub. + +**DetectionReceiverPort**: This is the Port on which V2X-Hub will list for detection. In most scenarios this can be left at default as well. The MUST Sensor must be configured to broadcast it's detection information to the configured IP and Port. + +**SensorId**: This is a unique string identifier for this sensor. Multiple instances of MUST Sensors can be connected via multiple instance of this plugin. Additionally other sensor can also be connected to V2X-Hub conccurently. For cooperative perception functionality to work correctly, each of thes sensor must have a unique string identifier. + +> [!NOTE] +> V2X-Hub currently has no mechanism by which to verify that all configured sensors have unique string indentifies. Currently this can only be verified via manual inspection. + +**ProjectionString**: This parameter holds a string that describes coordinate transforms necessary for projecting the detection data provide in cartesian coordinates to WSG84 coordinate frame. + +> [!NOTE] +> Both **CARMA Streets** and our vehicle automatation **CARMA Platform** rely on the PROJ4 library for projecting data between internal local maps coordinate frames and WSG84. Additional documentation on the projection string can be found in PROJ documentation (https://proj.org/en/9.4/index.html)(https://proj.org/en/9.4/usage/quickstart.html)(https://proj.org/en/9.4/usage/transformation.html) + +After setting these configuration parameters the plugin can simply be enabled. + +## Design + +![Alt text](docs/communication_diagram.png) +This plugin consists of a simple UDP Server listening for detection data from the MUST Sensor. Each received detection is deserialized and translated to **Sensor Detected Object** data via free functions. Then this **Sensor Detected Object** is forward on the TMX Message bus. If enabled, the **CARMA Streets Plugin** will receive this message forward it to the **CARMA Streets [Sensor Data Sharing Service](https://github.com/usdot-fhwa-stol/carma-streets/blob/develop/sensor_data_sharing_service/README.md)** which is responsible for generating SDSMs from detection data. These SDSMs are sent back to V2X-Hub for broadcast to vehicle's via the RSU (Road Side Unit). + +### Messages + +**Sensor Detected Object**: V2X-Hub's generic message for detection data. + +## Functionality Testing + diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/docs/communication_diagram.png b/src/v2i-hub/MUSTSensorDriverPlugin/docs/communication_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..f4c292378b32d7af99b546519c0632269d077300 GIT binary patch literal 37467 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

CS7;mofc;g2WZ%}Tl; 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 A0RR91 literal 0 HcmV?d00001 diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/manifest.json b/src/v2i-hub/MUSTSensorDriverPlugin/manifest.json index 771e63149..dcd15c60f 100755 --- a/src/v2i-hub/MUSTSensorDriverPlugin/manifest.json +++ b/src/v2i-hub/MUSTSensorDriverPlugin/manifest.json @@ -1,22 +1,23 @@ { - "name": "RSUHealthMonitor", - "description": "Monitor RSU health status", + "name": "MUSTSensorDriver", + "description": "Plugin for processing MUST Sensor Data.", "version": "@PROJECT_VERSION@", - "exeLocation": "/bin/RSUHealthMonitorPlugin", + "exeLocation": "/bin/MUSTSensorDriverPlugin", "coreIpAddr":"127.0.0.1", "corePort":24601, - "messageTypes": [], + "messageTypes": [ + { + "type": "Application", + "subtype": "SensorDetectedObject", + "description": "Generic message for detection from Sensor." + } + ], "configuration": [ { "key": "LogLevel", "default": "INFO", "description": "The log level for this plugin" }, - { - "key":"Interval", - "default":"1", - "description": "Sending RSU SNMP GET request at every configured interval. Default every 1 second. Unit of measure: second." - }, { "key":"DetectionReceiverIP", "default":"127.0.0.1", diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/scripts/MockMUSTSensor.py b/src/v2i-hub/MUSTSensorDriverPlugin/scripts/MockMUSTSensor.py new file mode 100644 index 000000000..e35efeb9f --- /dev/null +++ b/src/v2i-hub/MUSTSensorDriverPlugin/scripts/MockMUSTSensor.py @@ -0,0 +1,75 @@ +#!/usr/bin/python3 + +import socket +import sys +import time +import argparse +from dataclasses import dataclass +from enum import Enum + + +class DetectionClassification(Enum): + """Enumeration used for indentifying type of detection + """ + SEDAN='sedan' + VAN='van' + TRUCK='truck' +class DetectionSize(Enum): + """Enumeration used for indentifying the type of KafkaLogMessage + """ + SMALL='small' + MEDIUM='medium' + LARGE='large' + +@dataclass +class MUSTDetection: + """Class used to store data for each Kafka Log Message + """ + classification: DetectionClassification + x: float + y: float + heading: float + speed: float + size: DetectionSize + confidence: float + track_id: int + timestamp: int + + def to_csv(): + return f'{self.classification},{self.x},{self.y},{self.heading},{self.speed},{self.size},{self.confidence},{self.track_id},{self.timstamp}' + +def move_detection(): + return + +def create_socket(): + try: + return socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + except socket.error as err: + print('Socket error because of %s' %(err)) +def send_detection(sock, detection, host): + try: + msg = detection + encoded_msg = str.encode(msg) + sock.sendto(encoded_msg,host) + print( encoded_msg.decode(encoding= 'UTF-8'), 'was sent to ', host) + except socket.gaierror: + print ('There an error resolving the host') + +detections = [] +detections.append(MUSTDetection(DetectionClassification.SEDAN, 0, 0, 330, 1, DetectionSize.MEDIUM, 95, 2,round(time.time() * 1000))) + +def main(): + parser = argparse.ArgumentParser(description='Script to mock detection data coming from MUST Sensor') + parser.add_argument('--ip', help='IP address to send detection data to.', type=str, default="127.0.0.1") + parser.add_argument('--port', help='Port to send detection data to.', type=str, default=4545) + args = parser.parse_args() + sock = create_socket() + host = (args.ip, args.port) + + print("Mocking MUST Sensor detections ...") + while True: + for detection in detections: + send_detection(sock,detection,host) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.cpp b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.cpp index 07f45ae7b..d23ee4d98 100644 --- a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.cpp +++ b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.cpp @@ -29,7 +29,7 @@ namespace MUSTSensorDriverPlugin { return detection; } - tmx::messages::SensorDetectedObject mustDetectionToSensorDetectedObject(const MUSTSensorDetection &detection, const std::string &sensorId, const std::string &projString) { + tmx::messages::SensorDetectedObject mustDetectionToSensorDetectedObject(const MUSTSensorDetection &detection, std::string_view sensorId, std::string_view projString) { tmx::messages::SensorDetectedObject detectedObject; detectedObject.objectId = detection.trackID; detectedObject.position.X = detection.position_x; @@ -47,7 +47,8 @@ namespace MUSTSensorDriverPlugin { return stringToDetectionClassificationMap.at(str); } - catch( const std::out_of_range &e) { + catch( const std::out_of_range ) { + FILE_LOG(tmx::utils::logWARNING) << "No registered Detection Classification for " << str << " in stringToDetectionClassificationMap! Setting classification as NA." << std::endl; return DetectionClassification::NA; } } @@ -70,7 +71,8 @@ namespace MUSTSensorDriverPlugin { return stringToDetectionSizeMap.at(str); } - catch( const std::out_of_range &e) { + catch( const std::out_of_range ) { + FILE_LOG(tmx::utils::logWARNING) << "No registered Detection Size for " << str << " in stringToDetectionSizeMap! Setting classification as NA." << std::endl; return DetectionSize::NA; } }; diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.h b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.h index c5fb00749..74352c1bb 100644 --- a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.h +++ b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDetection.h @@ -59,7 +59,7 @@ namespace MUSTSensorDriverPlugin { MUSTSensorDetection csvToDectection(const std::string &csv ); - tmx::messages::SensorDetectedObject mustDetectionToSensorDetectedObject(const MUSTSensorDetection &detection, const std::string &sensorId, const std::string &projString); + tmx::messages::SensorDetectedObject mustDetectionToSensorDetectedObject(const MUSTSensorDetection &detection, std::string_view sensorId, std::string_view projString); tmx::utils::Vector3d headingSpeedToVelocity(double heading, double speed); } \ No newline at end of file diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp index 8e51474ca..527c2218c 100644 --- a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp +++ b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp @@ -44,8 +44,11 @@ namespace MUSTSensorDriverPlugin { GetConfigValue("DetectionReceiverIP", ip_address); GetConfigValue("DetectionReceiverPort", port); createUdpServer(ip_address, port); - auto message_receiver_tick_id = mustSensorPacketReceiverThread->AddPeriodicTick([this]() { + SetStatus(keyMUSTSensorConnectionStatus, "IDLE", true); + + mustSensorPacketReceiverThreadId = mustSensorPacketReceiverThread->AddPeriodicTick([this]() { this->processMUSTSensorDetection(); + } // end of lambda expression , std::chrono::milliseconds(5) ); mustSensorPacketReceiverThread->Start(); @@ -53,10 +56,23 @@ namespace MUSTSensorDriverPlugin { } void MUSTSensorDriverPlugin::processMUSTSensorDetection(){ if (mustSensorPacketReceiver) { - MUSTSensorDetection detection = csvToDectection(mustSensorPacketReceiver->stringTimedReceive()); - tmx::messages::SensorDetectedObject msg = mustDetectionToSensorDetectedObject(detection, sensorId, projString); - PLOG(logDEBUG1) << "Sending Simulated SensorDetectedObject Message " << msg << std::endl; - this->BroadcastMessage(msg, _name, 0 , IvpMsgFlags_None); + try { + MUSTSensorDetection detection = csvToDectection(mustSensorPacketReceiver->stringTimedReceive()); + if ( !connected ) { + connected = true; + SetStatus(keyMUSTSensorConnectionStatus, "CONNECTED", true); + } + tmx::messages::SensorDetectedObject msg = mustDetectionToSensorDetectedObject(detection, sensorId, projString); + PLOG(logDEBUG1) << "Sending Simulated SensorDetectedObject Message " << msg << std::endl; + this->BroadcastMessage(msg, _name, 0 , IvpMsgFlags_None); + } + catch( const tmx::utils::UdpServerRuntimeError &e) { + SetStatus(keyMUSTSensorConnectionStatus, "DISCONNECTED", true); + connected = false; + } + }else { + SetStatus(keyMUSTSensorConnectionStatus, "DISCONNECTED", true); + connected = false; } } diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.h b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.h index 66985190d..fd37dfafa 100644 --- a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.h +++ b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.h @@ -38,7 +38,7 @@ namespace MUSTSensorDriverPlugin /** * @brief Status label simulation time to be displayed by each plugin. */ - const char* keySensorConnectionStatus = "Sensor Connection Status"; + const char* keyMUSTSensorConnectionStatus = "MUST Sensor Connection Status"; std::unique_ptr mustSensorPacketReceiver; @@ -47,6 +47,11 @@ namespace MUSTSensorDriverPlugin std::string sensorId; std::string projString; + + bool connected = false; + + // Message receiver thread id + int mustSensorPacketReceiverThreadId; /** * @brief Callback triggered on configuration updates */