diff --git a/.circleci/config.yml b/.circleci/config.yml index c33deeb10..ff7ba8583 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -281,6 +281,5 @@ workflows: only: /^[\.0-9]*$/ branches: ignore: /.*/ - diff --git a/configuration/amd64/docker-compose.yml b/configuration/amd64/docker-compose.yml index 82e1ae302..0583520ef 100755 --- a/configuration/amd64/docker-compose.yml +++ b/configuration/amd64/docker-compose.yml @@ -19,7 +19,7 @@ services: - ./mysql/port_drayage.sql:/docker-entrypoint-initdb.d/port_drayage.sql php: - image: usdotfhwaops/php:7.2.3 + image: usdotfhwaops/php:7.3.0 container_name: php network_mode: host depends_on: @@ -29,7 +29,7 @@ services: tty: true v2xhub: - image: usdotfhwaops/v2xhubamd:7.2.3 + image: usdotfhwaops/v2xhubamd:7.3.0 container_name: v2xhub network_mode: host restart: always @@ -43,7 +43,7 @@ services: - ./logs:/var/log/tmx - ./MAP:/var/www/plugins/MAP port_drayage_webservice: - image: usdotfhwaops/port-drayage-webservice:7.2.3 + image: usdotfhwaops/port-drayage-webservice:7.3.0 container_name: port_drayage_webservice network_mode: host secrets: diff --git a/configuration/arm64/docker-compose.yml b/configuration/arm64/docker-compose.yml index a58b5c507..8b208bba8 100644 --- a/configuration/arm64/docker-compose.yml +++ b/configuration/arm64/docker-compose.yml @@ -19,7 +19,7 @@ services: - ./mysql/port_drayage.sql:/docker-entrypoint-initdb.d/port_drayage.sql php: - image: usdotfhwaops/php_arm:7.2.3 + image: usdotfhwaops/php_arm:7.3.0 container_name: php network_mode: host depends_on: @@ -29,7 +29,7 @@ services: tty: true v2xhub: - image: usdotfhwaops/v2xhubarm:7.2.3 + image: usdotfhwaops/v2xhubarm:7.3.0 container_name: v2xhub network_mode: host restart: always @@ -43,7 +43,7 @@ services: - ./logs:/var/log/tmx - ./MAP:/var/www/plugins/MAP port_drayage_webservice: - image: usdotfhwaops/port-drayage-webservice_arm:7.2.3 + image: usdotfhwaops/port-drayage-webservice_arm:7.3.0 container_name: port_drayage_webservice network_mode: host secrets: diff --git a/docs/Release_notes.md b/docs/Release_notes.md index 89d299c43..82f5dd340 100644 --- a/docs/Release_notes.md +++ b/docs/Release_notes.md @@ -1,4 +1,12 @@ V2X-Hub Release Notes +--------------------------------- +Version 7.3.0, released June 14th, 2022 +-------------------------------------------------------- +**Summary:** +V2X Hub release 7.3.0 includes added functionality for subscribing to FLIR camera for Pedestrian tracking and PSM broadcast to vehicles in the Pedestrian Plugin. The new FLIR functionality subscribes to the websocket output of the FLIR tracking feed and will generate a PSM for each new track the camera picks up. To use new FLIR websocket data, set `DataProvider` for the PedestrianPlugin to FLIR and configure the `WebSocketHost` and `WebSocketPort` for the FLIR camera. Additional you must set the `FLIRCameraRotation` in degrees, which is a measure of the camera's rotation from true north. To use the REST PedestrianPlugin functionality simply set the `DataProvider` to PSM. + +Enhancements in this release: + - Issue 345: Added Websocket client to consume FLIR data and publish PSM ---------------------------- Version 7.2.3, released May 19th, 2022 -------------------------------------------------------- diff --git a/src/tmx/TmxUtils/test/J2735MessageTest.cpp b/src/tmx/TmxUtils/test/J2735MessageTest.cpp index 44d0f4afc..a5bb3642e 100644 --- a/src/tmx/TmxUtils/test/J2735MessageTest.cpp +++ b/src/tmx/TmxUtils/test/J2735MessageTest.cpp @@ -484,7 +484,7 @@ TEST_F(J2735MessageTest, EncodeMobilityResponse) TEST_F(J2735MessageTest, EncodeBasicSafetyMessage) { - BasicSafetyMessage_t* message = (BasicSafetyMessage_t*) malloc( sizeof(BasicSafetyMessage_t) ); + BasicSafetyMessage_t* message = (BasicSafetyMessage_t*) calloc(1, sizeof(BasicSafetyMessage_t) ); /** * Populate BSMcoreData @@ -542,4 +542,17 @@ TEST_F(J2735MessageTest, EncodeBasicSafetyMessage) } +TEST_F(J2735MessageTest, EncodePersonalSafetyMessage){ + string psm="1090115eadf0389549376-771491840255255655350160100001"; + std::stringstream ss; + PsmMessage psmmessage; + PsmEncodedMessage psmENC; + tmx::message_container_type container; + ss<(ss); + psmmessage.set_contents(container.get_storage().get_tree()); + psmENC.encode_j2735_message(psmmessage); + std::cout << psmENC.get_payload_str()<(); + std::string time = ""; + std::string type = ""; + float angle = 0; + int alpha = 0; + std::string lat = ""; + std::string lon = ""; + float speed = 0; + std::string timeString = ""; + int id = 0; + std::string idResult; + + if (messageType.compare("Subscription") == 0) + { + std::string subscrStatus = pr.get_child("subscription").get_child("returnValue").get_value(); + PLOG(logDEBUG) << "Ped presence data subscription status: " << subscrStatus << std::endl; + + } + //Example received pedestrian tracking data + //{"dataNumber": "473085", "messageType": "Data", "time": "2022-04-20T15:25:51.001-04:00", + //"track": [{"angle": "263.00000000", "class": "Pedestrian", "iD": "15968646", "latitude": "38.95499217", + //"longitude": "-77.14920953", "speed": "1.41873741", "x": "0.09458912", "y": "14.80903757"}], "type": "PedestrianPresenceTracking"} + else if (messageType.compare("Data") == 0) + { + time = pr.get_child("time").get_value(); + type = pr.get_child("type").get_value(); + + PLOG(logINFO) << "Received " << type << " data at time: " << time << std::endl; + + if (type.compare("PedestrianPresenceTracking") == 0) + { + try + { + for (auto it: pr.get_child("track")) + { + + if (!it.second.get_child("angle").data().empty()) + { + //angle only reported in whole number increments, so int is fine + angle = std::stoi(it.second.get_child("angle").data()); + //convert camera reference frame angle + alpha = cameraRotation_ - angle - 270; + + if (alpha < 0) + { + alpha = (alpha % 360) + 360; + } + + //divide by 0.0125 for J2735 format + alpha /= 0.0125; + } + if (!it.second.get_child("iD").data().empty()) + { + id = std::stoi(it.second.get_child("iD").data()); + + //need to convert the id to 4 octet string + std::stringstream idstream; + idstream << std::hex << id; + std::string result(idstream.str()); + + idResult = result; + int str_length_diff = 8 - idResult.length(); + idResult.append(str_length_diff, '0'); + + } + if (!it.second.get_child("latitude").data().empty()) + { + //converting lat/lon to J2735 lat/lon format + lat = it.second.get_child("latitude").data(); + lat.erase(std::remove(lat.begin(), lat.end(), '.'), lat.end()); + lat.pop_back(); + } + if (!it.second.get_child("longitude").data().empty()) + { + //converting lat/lon to J2735 lat/lon format + lon = it.second.get_child("longitude").data(); + lon.erase(std::remove(lon.begin(), lon.end(), '.'), lon.end()); + lon.pop_back(); + } + if (!it.second.get_child("speed").data().empty()) + { + //speed from the FLIR camera is reported in m/s, need to convert to units of 0.02 m/s + speed = std::stof(it.second.get_child("speed").data()) / 0.02; + } + //need to parse out seconds from datetime string + std::vector dateTimeArr = timeStringParser(time); + + msgCount += 1; + if (msgCount > 127){ + msgCount = 0; + } + + PLOG(logINFO) << "Received FLIR camera data at: " << dateTimeArr[0] << "/" << dateTimeArr[1] << "/" << dateTimeArr[2] + << " " << dateTimeArr[3] << ":" << dateTimeArr[4] << ":" << dateTimeArr[5] << ":" << dateTimeArr[6] << std::endl; + + PLOG(logINFO) << "Received FLIR camera data for pedestrian " << idResult << " at location: (" << lat << ", " << lon << + ")" << ", travelling at speed: " << speed << ", with heading: " << alpha << " degrees" << std::endl; + + PLOG(logINFO) << "PSM message count: " << msgCount << std::endl; + + //constructing xml to send to BroadcastPSM function + char psm_xml_char[10000]; + snprintf(psm_xml_char,10000,"" + "%i%i%s%s%s" + "25525565535" + "%.0f%i%i%i" + "%i%i%i%i" + "000" + "001" + "", dateTimeArr[6], msgCount, idResult.c_str(), lat.c_str(), lon.c_str(), speed, alpha, dateTimeArr[0], dateTimeArr[1], + dateTimeArr[2], dateTimeArr[3], dateTimeArr[4], dateTimeArr[6]); + + std::string psm_xml_str(psm_xml_char, sizeof(psm_xml_char) / sizeof(psm_xml_char[0])); + + std::lock_guard lock(_psmLock); + psmxml = psm_xml_str; + psmQueue.push(psmxml); + + PLOG(logDEBUG) << "Sending PSM xml to BroadcastPsm: " << psmxml.c_str() < FLIRWebSockAsyncClnSession::getPSMQueue() + { + std::lock_guard lock(_psmLock); + + //pass copy of the queue to Pedestrian Plugin + std::queue queueToPass = psmQueue; + + //empty the queue internally + std::queue empty; + std::swap(psmQueue, empty); + + return queueToPass; + + } + + vector FLIRWebSockAsyncClnSession::timeStringParser(string dateTimeStr) const + { + std::string delimiter1 = "."; + std::string delimiter2 = "-"; + std::string delimiter3 = "T"; + std::string delimiter4 = ":"; + std::vector parsedArr; + + std::string year = dateTimeStr.substr(0, dateTimeStr.find(delimiter2)); + year.erase(0, std::min(year.find_first_not_of('0'), year.size()-1)); + dateTimeStr.erase(0, dateTimeStr.find(delimiter2) + delimiter2.length()); + + std::string month = dateTimeStr.substr(0, dateTimeStr.find(delimiter2)); + month.erase(0, std::min(month.find_first_not_of('0'), month.size()-1)); + dateTimeStr.erase(0, dateTimeStr.find(delimiter2) + delimiter2.length()); + + std::string day = dateTimeStr.substr(0, dateTimeStr.find(delimiter3)); + day.erase(0, std::min(day.find_first_not_of('0'), day.size()-1)); + dateTimeStr.erase(0, dateTimeStr.find(delimiter3) + delimiter3.length()); + + std::string hour = dateTimeStr.substr(0, dateTimeStr.find(delimiter4)); + hour.erase(0, std::min(hour.find_first_not_of('0'), hour.size()-1)); + dateTimeStr.erase(0, dateTimeStr.find(delimiter4) + delimiter4.length()); + + std::string mins = dateTimeStr.substr(0, dateTimeStr.find(delimiter4)); + mins.erase(0, std::min(mins.find_first_not_of('0'), mins.size()-1)); + dateTimeStr.erase(0, dateTimeStr.find(delimiter4) + delimiter4.length()); + + std::string sec = dateTimeStr.substr(0, dateTimeStr.find(delimiter1)); + sec.erase(0, std::min(sec.find_first_not_of('0'), sec.size()-1)); + dateTimeStr.erase(0, dateTimeStr.find(delimiter1) + delimiter1.length()); + + std::string milliseconds = dateTimeStr.substr(0, dateTimeStr.find(delimiter2)); + milliseconds.erase(0, std::min(milliseconds.find_first_not_of('0'), milliseconds.size()-1)); + + int millisecondsTotal = (std::stoi(sec) * 1000) + std::stoi(milliseconds); + parsedArr.push_back(std::stoi(year)); + parsedArr.push_back(std::stoi(month)); + parsedArr.push_back(std::stoi(day)); + parsedArr.push_back(std::stoi(hour)); + parsedArr.push_back(std::stoi(mins)); + parsedArr.push_back(std::stoi(sec)); + parsedArr.push_back(millisecondsTotal); + + return parsedArr; + } + +} \ No newline at end of file diff --git a/src/v2i-hub/PedestrianPlugin/src/PedestrianPlugin.cpp b/src/v2i-hub/PedestrianPlugin/src/PedestrianPlugin.cpp index ebae2742e..ee1a6330e 100644 --- a/src/v2i-hub/PedestrianPlugin/src/PedestrianPlugin.cpp +++ b/src/v2i-hub/PedestrianPlugin/src/PedestrianPlugin.cpp @@ -8,6 +8,7 @@ #include "include/PedestrianPlugin.hpp" + namespace PedestrianPlugin { @@ -31,13 +32,48 @@ PedestrianPlugin::PedestrianPlugin(string name): PluginClient(name) // fire up the web service on a thread PROTECTION required std::lock_guard lock(_cfgLock); - GetConfigValue("WebServiceIP",webip); - GetConfigValue("WebServicePort",webport); + GetConfigValue("IPAddress",webip); + GetConfigValue("WebPort",webport); + GetConfigValue("DataProvider",dataprovider); + GetConfigValue("WebSocketHost",webSocketIP); + GetConfigValue("WebSocketPort",webSocketURLExt); + GetConfigValue("FLIRCameraRotation",cameraRotation); + + + PLOG(logDEBUG) << "Pedestrian data provider: "<< dataprovider.c_str() << std::endl; + + + PLOG(logDEBUG) << "Before creating websocket to: " << webSocketIP.c_str() << " on port: " << webSocketURLExt.c_str() << std::endl; + + if (dataprovider.compare("FLIR") == 0) + { + try + { + std::thread webthread(&PedestrianPlugin::StartWebSocket,this); + PLOG(logDEBUG) << "Thread started!!: " << std::endl; + + webthread.detach(); // wait for the thread to finish + std::thread xmlThread(&PedestrianPlugin::checkXML,this); + PLOG(logDEBUG) << "XML Thread started!!: " << std::endl; + + xmlThread.detach(); // wait for the thread to finish + + } + catch(const std::exception& e) + { + PLOG(logERROR) << "Error connecting to websocket: " << e.what() << std::endl; + } + + + } + else // default if PSM XML data consumed using the webservice implementation + { + std::thread webthread(&PedestrianPlugin::StartWebService,this); + webthread.detach(); // wait for the thread to finish + } - std::thread webthread(&PedestrianPlugin::StartWebService,this); - webthread.detach(); // wait for the thread to finish } @@ -63,6 +99,55 @@ void PedestrianPlugin::PedestrianRequestHandler(QHttpEngine::Socket *socket) } } +int PedestrianPlugin::StartWebSocket() +{ + PLOG(logDEBUG) << "In PedestrianPlugin::StartWebSocket " << std::endl; + // The io_context is required for all I/O + net::io_context ioc; + + flirSession = std::make_shared(ioc); + + // Launch the asynchronous operation + flirSession->run(webSocketIP.c_str(), webSocketURLExt.c_str(), cameraRotation); + + PLOG(logDEBUG) << "Successfully running the I/O service" << std::endl; + + // Run the I/O service. The call will return when + // the socket is closed. + ioc.run(); + + return EXIT_SUCCESS; +} + +int PedestrianPlugin::checkXML() +{ + //first xml will be empty string + std::string lastGeneratedXML = ""; + + //if a new psm xml has been generated the FLIR web socket, send it to the BroadcastPSM function + while (true) + { + if (flirSession == nullptr) + { + PLOG(logDEBUG) << "flir session not yet initialized: " << std::endl; + } + else + { + //retrieve the PSM queue and send each one to be broadcast, then pop + std::queue currentPSMQueue = flirSession->getPSMQueue(); + + while(!currentPSMQueue.empty()) + { + char* char_arr = ¤tPSMQueue.front()[0]; + + BroadcastPsm(char_arr); + currentPSMQueue.pop(); + } + } + + } + return EXIT_SUCCESS; +} int PedestrianPlugin::StartWebService() { @@ -110,8 +195,11 @@ void PedestrianPlugin::UpdateConfigSettings() GetConfigValue("WebServiceIP",webip); GetConfigValue("WebServicePort",webport); + GetConfigValue("WebSocketHost",webSocketIP); + GetConfigValue("WebSocketURLExt",webSocketURLExt); GetConfigValue("Instance", instance); - + GetConfigValue("DataProvider", dataprovider); + GetConfigValue("FLIRCameraRotation",cameraRotation); } @@ -137,8 +225,6 @@ void PedestrianPlugin::HandleMapDataMessage(MapDataMessage &msg, routeable_messa { static std::atomic count {0}; - PLOG(logINFO) << "New MAP: " << msg; - int mapCount = count; SetStatus("ReceivedMaps", mapCount); } @@ -156,39 +242,43 @@ void PedestrianPlugin::BroadcastPsm(char * psmJson) { //overloaded PsmMessage psmmessage; PsmEncodedMessage psmENC; tmx::message_container_type container; - std::unique_ptr msg; - - - std::stringstream ss; - ss << psmJson; - - container.load(ss); - psmmessage.set_contents(container.get_storage().get_tree()); - - const std::string psmString(psmJson); - psmENC.encode_j2735_message(psmmessage); + std::unique_ptr msg; + try + { + std::stringstream ss; + ss << psmJson; - msg.reset(); - msg.reset(dynamic_cast(factory.NewMessage(api::MSGSUBTYPE_PERSONALSAFETYMESSAGE_STRING))); + container.load(ss); + psmmessage.set_contents(container.get_storage().get_tree()); + const std::string psmString(psmJson); - string enc = psmENC.get_encoding(); - msg->refresh_timestamp(); - msg->set_payload(psmENC.get_payload_str()); - msg->set_encoding(enc); - msg->set_flags(IvpMsgFlags_RouteDSRC); - msg->addDsrcMetadata(172, 0x8002); - msg->refresh_timestamp(); + psmENC.encode_j2735_message(psmmessage); + msg.reset(); + msg.reset(dynamic_cast(factory.NewMessage(api::MSGSUBTYPE_PERSONALSAFETYMESSAGE_STRING))); + string enc = psmENC.get_encoding(); + msg->refresh_timestamp(); + msg->set_payload(psmENC.get_payload_str()); + msg->set_encoding(enc); + msg->set_flags(IvpMsgFlags_RouteDSRC); + msg->addDsrcMetadata(172, 0x8002); + msg->refresh_timestamp(); - routeable_message *rMsg = dynamic_cast(msg.get()); - BroadcastMessage(*rMsg); + routeable_message *rMsg = dynamic_cast(msg.get()); + BroadcastMessage(*rMsg); + PLOG(logINFO) << " Pedestrian Plugin :: Broadcast PSM:: " << psmENC.get_payload_str() << std::endl; + } + catch(const std::exception& e) + { + PLOG(logWARNING) << "Error: " << e.what() << " broadcasting PSM for xml: " << psmJson << std::endl; + } - PLOG(logINFO) << " Pedestrian Plugin :: Broadcast PSM:: " << psmENC.get_payload_str(); + } diff --git a/src/v2i-hub/PedestrianPlugin/src/include/FLIRWebSockAsyncClnSession.hpp b/src/v2i-hub/PedestrianPlugin/src/include/FLIRWebSockAsyncClnSession.hpp new file mode 100644 index 000000000..41448e3af --- /dev/null +++ b/src/v2i-hub/PedestrianPlugin/src/include/FLIRWebSockAsyncClnSession.hpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +namespace pt = boost::property_tree; // from +using tcp = boost::asio::ip::tcp; // from + +//------------------------------------------------------------------------------ + +namespace PedestrianPlugin +{ + // Sends a WebSocket message and prints the response + class FLIRWebSockAsyncClnSession : public std::enable_shared_from_this + { + tcp::resolver resolver_; + websocket::stream ws_; + beast::flat_buffer buffer_; + std::string host_; + std::string pedPresenceTrackingReq = std::string("{\"messageType\":\"Subscription\", \"subscription\":{ \"type\":\"Data\", \"action\":\"Subscribe\", \"inclusions\":[{\"type\":\"PedestrianPresenceTracking\"}]}}"); + float cameraRotation_; + std::string psmxml = ""; + std::queue psmQueue; + std::mutex _psmLock; + int msgCount = 0; + public: + + // Resolver and socket require an io_context + explicit + FLIRWebSockAsyncClnSession(net::io_context& ioc) + : resolver_(net::make_strand(ioc)) + , ws_(net::make_strand(ioc)) + { + + }; + + /** + * @brief Reports a failure with any of the websocket functions below + * + * @param: the error code for the specific function + * @param: description of the error + */ + void + fail(beast::error_code ec, char const* what) const; + + /** + * @brief Start the asynchronous web socket connection to the camera. Each function will call the + * function below it. + * + * @param: ip address of camera to connect to + * @param: port to connect to + * @param: calculated camera rotation + */ + void + run( + char const* host, + char const* port, + float cameraRotation); + + /** + * @brief Lookup the domain name of the IP address from run function. + * + * @param: error code containing information describing resolve issue + * @param: result of domain name lookup + */ + void + on_resolve( + beast::error_code ec, + tcp::resolver::results_type results); + + /** + * @brief Configures websocket settings and initiates handshake + * + * @param: error code containing information describing connection issue + */ + void + on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep); + + /** + * @brief Performs the websocket handshake and calls write function + * + * @param: error code containing information describing handshake issue + */ + void + on_handshake(beast::error_code ec); + + /** + * @brief Sends the subscription request json to the camera and calls read function for camera response + * + * @param: error code containing information describing issue with json send + * @param: the bytes of the json + */ + void + on_write( + beast::error_code ec, + std::size_t bytes_transferred); + + /** + * @brief Used to read in all messages from the camera and parse out desired fields + * + * @param: error code containing information describing issue with reading camera data + * @param: the bytes of the received camera data + */ + void + on_read( + beast::error_code ec, + std::size_t bytes_transferred); + + /** + * @brief Closes the websocket connection to the camera + * + * @param: error code containing information describing issue with closing websocket + */ + void + on_close(beast::error_code ec); + + /** + * @brief Get method for queue containing psm for all tracked pedestrians. Copies the queue into + * a temporary queue and returns temporary queue. Clears the original queue. + * + * @return std::queue the psm queue + */ + std::queue getPSMQueue(); + + /** + * @brief Parses the datetime string that the camera returns into a vector containing each component + * + * @param: datetime string from camera + * @return: vector with all components + */ + std::vector timeStringParser(std::string dateTimeStr) const; + }; + +}; + diff --git a/src/v2i-hub/PedestrianPlugin/src/include/PedestrianPlugin.hpp b/src/v2i-hub/PedestrianPlugin/src/include/PedestrianPlugin.hpp index 6d0c688d6..7f8e7554e 100644 --- a/src/v2i-hub/PedestrianPlugin/src/include/PedestrianPlugin.hpp +++ b/src/v2i-hub/PedestrianPlugin/src/include/PedestrianPlugin.hpp @@ -5,7 +5,6 @@ // Copyright : Copyright (c) 2019 FHWA Saxton Transportation Operations Laboratory. All rights reserved. // Description : Pedestrian Plugin //========================================================================== - #include #include "PluginClient.h" @@ -25,6 +24,7 @@ #include #include "PedestrianPluginWorker.hpp" +#include "FLIRWebSockAsyncClnSession.hpp" #include @@ -43,6 +43,7 @@ #include #include #include +#include using namespace std; @@ -66,7 +67,11 @@ class PedestrianPlugin: public PluginClient int Main(); uint16_t webport; std::string webip; - + std::string webSocketIP; + std::string webSocketURLExt; + std::string dataprovider; + float cameraRotation; + std::shared_ptr flirSession; protected: void UpdateConfigSettings(); @@ -83,14 +88,23 @@ class PedestrianPlugin: public PluginClient void PedestrianRequestHandler(QHttpEngine::Socket *socket); void writeResponse(int responseCode , QHttpEngine::Socket *socket); + int StartWebSocket(); + + void OnWebSocketConnected(); + void OnWebSocketDataReceived(QString message); + void OnWebSocketClosed(); + + int checkXML(); + private: tmx::utils::UdpClient *_signSimClient = NULL; J2735MessageFactory factory; - + }; std::mutex _cfgLock; }; +