From dbbc089a3d4f7aa08020cf1f1175448059165f83 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 15 May 2024 14:38:19 -0400 Subject: [PATCH 01/31] Update SPaT Plugin --- .../src/PedestrianDetectionForSPAT.cpp | 50 --------------- .../src/PedestrianDetectionForSPAT.h | 14 ----- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 61 ++----------------- src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 1 - .../SpatPlugin/src/signalController.cpp | 36 +---------- src/v2i-hub/SpatPlugin/src/signalController.h | 4 +- 6 files changed, 8 insertions(+), 158 deletions(-) delete mode 100644 src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.cpp delete mode 100644 src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.h diff --git a/src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.cpp b/src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.cpp deleted file mode 100644 index 235618446..000000000 --- a/src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "PedestrianDetectionForSPAT.h" - -#include - -using namespace tmx::messages; - -void PedestrianDetectionForSPAT::updateEncodedSpat(SpatEncodedMessage & spatEncodedMsg, - std::shared_ptr _spatMessage, - const std::string & currentPedLanes) const -{ - // Add pedestrian lanes with active detections and clear the rest - auto spat = _spatMessage->get_j2735_data(); - // check for valid SPAT and also if ped lanes is filled in - if (spat && spat->intersections.list.array && spat->intersections.list.count > 0 && currentPedLanes.length()) { - // parse ped zone string into set of int values - std::vector zones; - { - std::vector zoneList(currentPedLanes.length()); - std::copy(currentPedLanes.begin(), currentPedLanes.end(), zoneList.begin()); - char *restOfString = nullptr; - auto c = strtok_r(zoneList.data(), ",", &restOfString); - - while (c != nullptr) { - zones.push_back(strtol(c, nullptr, 0)); - c = strtok_r(nullptr, ",", &restOfString); - } - } - - if (!zones.empty()) { - ManeuverAssistList *&mas = spat->intersections.list.array[0]->maneuverAssistList; - mas = (ManeuverAssistList *) calloc(1, sizeof(ManeuverAssistList)); - std::sort(zones.begin(), zones.end()); - // add a connection maneuver for each ped zone and set the ped detect flag - std::for_each(zones.begin(), zones.end(), [&mas](LaneConnectionID_t connectionId) - { - auto connectionManManeuverAssist = (ConnectionManeuverAssist *) calloc(1, sizeof(ConnectionManeuverAssist)); - connectionManManeuverAssist->connectionID = connectionId; - connectionManManeuverAssist->pedBicycleDetect = (PedestrianBicycleDetect_t *) calloc(1, sizeof(PedestrianBicycleDetect_t)); - *(connectionManManeuverAssist->pedBicycleDetect) = 1; - ASN_SEQUENCE_ADD(mas, connectionManManeuverAssist); - } - ); - } - } - - MessageFrameMessage frame(_spatMessage->get_j2735_data()); - spatEncodedMsg.set_data(TmxJ2735EncodedMessage::encode_j2735_message>(frame)); - //Free the memory allocated for MessageFrame - free(frame.get_j2735_data().get()); -} diff --git a/src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.h b/src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.h deleted file mode 100644 index 3d9ed440f..000000000 --- a/src/v2i-hub/SpatPlugin/src/PedestrianDetectionForSPAT.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -/** - * A helper class to add pedestrian detection elements to a SPAT message. -*/ -class PedestrianDetectionForSPAT -{ -public: - void updateEncodedSpat(tmx::messages::SpatEncodedMessage & spatEncodedMsg, - std::shared_ptr _spatMessage, - const std::string & currentPedLanes) const; -}; diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index c47a4863f..b510b9e90 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -10,7 +10,6 @@ namespace SpatPlugin { SpatPlugin::SpatPlugin(string name) : PluginClientClockAware(name), sc(getClock()), intersectionId(0) { - AddMessageFilter(this, &SpatPlugin::HandlePedestrianDetection); SubscribeToMessages(); } @@ -46,17 +45,12 @@ void SpatPlugin::OnStateChange(IvpPluginState state) { } } -void SpatPlugin::HandlePedestrianDetection(PedestrianMessage &pedMsg, routeable_message &routeableMsg) { - lock_guard lock(data_lock); - _pedMessage = pedMsg; -} int SpatPlugin::Main() { int iCounter = 0; PLOG(logINFO) << "Waiting for clock initialization"; - // wait for the clock to be initialized and record the time when it is ready getClock()->wait_for_initialization(); auto nextSpatTime = getClock()->nowInMilliseconds(); @@ -68,35 +62,17 @@ int SpatPlugin::Main() { if (isConfigurationLoaded) { if (!isConfigured) { - usleep(200000); - - int action = sc.getActionNumber(); - - /*pthread_mutex_lock(&gSettingsMutex); - std::cout << "Get PTLM file specified by configuration settings" << std::endl; - std::string ptlmFile = GetPtlmFile(action); - pthread_mutex_unlock(&gSettingsMutex); - */ - //if (!ptlmFile.empty()) - //{ - _actionNumber = action; - - // sc.spat_message_mutex does not need locked because the thread is not running yet. - { std::lock_guard lock(data_lock); string ptlm = ""; sc.setConfigs(localIp, localUdpPort, tscIp, - tscRemoteSnmpPort, ptlm, intersectionName, + tscRemoteSnmpPort, intersectionName, intersectionId); } // Start the signal controller thread. sc.Start(signalGroupMappingJson); // Give the spatdata pointer to the message class - //smr41.setSpatData(sc.getSpatData()); - isConfigured = true; - //} } // SPaT must be sent exactly every 100 ms. So adjust for how long it took to do the last send. @@ -107,22 +83,6 @@ int SpatPlugin::Main() { bool messageSent = false; - // Update PTLM file if the action number has changed. - int actionNumber = sc.getActionNumber(); - if (_actionNumber != actionNumber) { - _actionNumber = actionNumber; - - //pthread_mutex_lock(&gSettingsMutex); - //std::string ptlmFile = GetPtlmFile(_actionNumber); - //pthread_mutex_unlock(&gSettingsMutex); - - /*if (!ptlmFile.empty()) - { - pthread_mutex_lock(&sc.spat_message_mutex); - sc.updatePtlmFile(ptlmFile.c_str()); - pthread_mutex_unlock(&sc.spat_message_mutex); - }*/ - } if (sc.getIsConnected()) { SetStatus("TSC Connection", "Connected"); @@ -137,36 +97,25 @@ int SpatPlugin::Main() { } SpatEncodedMessage spatEncodedMsg; - sc.getEncodedSpat(&spatEncodedMsg, pedZones); + spatEncodedMsg.set_flags(IvpMsgFlags_RouteDSRC); spatEncodedMsg.addDsrcMetadata(0x8002); - //PLOG(logDEBUG) << spatEncodedMsg; BroadcastMessage(static_cast(spatEncodedMsg)); - if (iCounter % 20 == 0) { - iCounter = 0; - // Action Number - IvpMessage *actionMsg = ivpSigCont_createMsg( - sc.getActionNumber()); - if (actionMsg != NULL) { - ivp_broadcastMessage(_plugin, actionMsg); - ivpMsg_destroy(actionMsg); - } - } } else { SetStatus("TSC Connection", "Disconnected"); } } } - } catch (exception &ex) { + } catch (const exception &ex) { stringstream ss; - ss << "SpatPlugin terminating from unhandled exception: " << ex.what(); + PLOG(logERROR) << "SpatPlugin terminating from unhandled exception: " << ex.what(); - ivp_addEventLog(_plugin, IvpLogLevel_error, ss.str().c_str()); + ivp_addEventLog(_plugin, IvpLogLevel_error, ex.what().c_str()); std::terminate(); } diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index 0d0b9987f..caec33093 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -52,7 +52,6 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { unsigned int derEncodedBytes = 0; SignalController sc; - int _actionNumber = -1; std::mutex data_lock; std::string localIp; diff --git a/src/v2i-hub/SpatPlugin/src/signalController.cpp b/src/v2i-hub/SpatPlugin/src/signalController.cpp index c75ef757a..f8af563a8 100644 --- a/src/v2i-hub/SpatPlugin/src/signalController.cpp +++ b/src/v2i-hub/SpatPlugin/src/signalController.cpp @@ -12,11 +12,7 @@ #include #include -#include #include -#ifndef __CYGWIN__ -#include -#endif #include #include #include @@ -59,7 +55,7 @@ void *SignalController::get_in_addr(struct sockaddr *sa) return &(((struct sockaddr_in6*)sa)->sin6_addr); } -void SignalController::setConfigs(std::string localIp, std::string localUdpPort, std::string tscIp, std::string tscRemoteSnmpPort, std::string ptlmFile, std::string intersectionName, int intersectionId) +void SignalController::setConfigs(std::string localIp, std::string localUdpPort, std::string tscIp, std::string tscRemoteSnmpPort, std::string intersectionName, int intersectionId) { _localIp = strdup(localIp.c_str()); _localUdpPort = strdup(localUdpPort.c_str()); @@ -71,12 +67,7 @@ void SignalController::setConfigs(std::string localIp, std::string localUdpPort, void SignalController::start_signalController() { -#ifndef __CYGWIN__ - prctl(PR_SET_NAME, "SpatGenSC", 0, 0, 0); -#endif - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,nullptr); - pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,nullptr); int maxDataSize = 1000; @@ -107,7 +98,6 @@ void SignalController::start_signalController() IsReceiving = 0; while (1) { - PLOG(logDEBUG) << "Top of While Loop"; if ((rv = getaddrinfo(_localIp, _localUdpPort, &hints, &servinfo)) != 0) { PLOG(logERROR) << "Getaddrinfo Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; return; @@ -169,20 +159,10 @@ void SignalController::start_signalController() pthread_mutex_lock(&spat_message_mutex); auto ntcip1202 = std::make_shared(clock); ntcip1202->setSignalGroupMappingList(_signalGroupMappingJson); - //printf("Signal Controller calling ntcip1202 copyBytesIntoNtcip1202"); ntcip1202->copyBytesIntoNtcip1202(buf, numbytes); - //printf("Signal Controller calling ntcip1202 ToJ2735r41SPAT"); SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); ntcip1202->ToJ2735r41SPAT(_spat, _intersectionName, _intersectionId); - - //printf("Signal Controller calling _spatMessage set_j2735_data\n"); - //_spatMessage.set_j2735_data(_spat); - if (_spatMessage != nullptr) - { - _spatMessage = nullptr; - } - _spatMessage = std::make_shared(_spat); pthread_mutex_unlock(&spat_message_mutex); PLOG(logDEBUG) << *_spatMessage; @@ -193,18 +173,6 @@ void SignalController::start_signalController() } } -void SignalController::getEncodedSpat(SpatEncodedMessage* spatEncodedMsg, std::string currentPedLanes) -{ - pthread_mutex_lock(&spat_message_mutex); - - if (_spatMessage != nullptr) { - PedestrianDetectionForSPAT pedDetect; - pedDetect.updateEncodedSpat(*spatEncodedMsg, _spatMessage, currentPedLanes); - } - - pthread_mutex_unlock(&spat_message_mutex); - -} int SignalController::getIsConnected() { @@ -213,7 +181,7 @@ int SignalController::getIsConnected() int SignalController::getActionNumber() { - return 1;//sd.actionNumber; + return 1; } void SignalController::SNMPOpenSession() diff --git a/src/v2i-hub/SpatPlugin/src/signalController.h b/src/v2i-hub/SpatPlugin/src/signalController.h index a31fdce69..347a69cad 100644 --- a/src/v2i-hub/SpatPlugin/src/signalController.h +++ b/src/v2i-hub/SpatPlugin/src/signalController.h @@ -29,8 +29,7 @@ class SignalController void spat_load(); void start_signalController(); int getActionNumber(); - void setConfigs(std::string ip, std::string udpPort, std::string snmpIP, std::string snmpPort, std::string ptlmFile, std::string intersectionName, int intersectionId); - void updatePtlmFile(const char* ptlmFile); + void setConfigs(std::string ip, std::string udpPort, std::string snmpIP, std::string snmpPort, std::string intersectionName, int intersectionId); int getIsConnected(); //int getDerEncodedSpat(unsigned char* derEncodedBuffer); @@ -59,7 +58,6 @@ class SignalController uint32_t _tscRemoteSnmpPort; std::string _signalGroupMappingJson; - std::shared_ptr _spatMessage; int counter; unsigned long normalstate; unsigned long crossstate; From 529518faf686338730f38d381b2f6b61127db84c Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 10 Jun 2024 10:43:59 -0400 Subject: [PATCH 02/31] Save point --- .devcontainer/docker-compose-vscode.yml | 4 +- src/tmx/Messages/test/Main.cpp | 14 + src/tmx/Messages/test/TestTimeSyncMessage.cpp | 10 + src/v2i-hub/SpatPlugin/CMakeLists.txt | 2 +- src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 35 +-- src/v2i-hub/SpatPlugin/src/NTCIP1202.h | 2 +- .../SpatPlugin/src/SignalController.cpp | 246 ++++++++++++++++++ src/v2i-hub/SpatPlugin/src/SignalController.h | 54 ++++ .../SpatPlugin/src/SignalControllerConfig.h | 35 +++ .../test/testPedestrianDetectionForSPAT.cpp | 80 ------ 10 files changed, 367 insertions(+), 115 deletions(-) create mode 100644 src/tmx/Messages/test/Main.cpp create mode 100644 src/tmx/Messages/test/TestTimeSyncMessage.cpp create mode 100644 src/v2i-hub/SpatPlugin/src/SignalController.cpp create mode 100644 src/v2i-hub/SpatPlugin/src/SignalController.h create mode 100644 src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h delete mode 100644 src/v2i-hub/SpatPlugin/test/testPedestrianDetectionForSPAT.cpp diff --git a/.devcontainer/docker-compose-vscode.yml b/.devcontainer/docker-compose-vscode.yml index ca174735d..dcfa29e73 100755 --- a/.devcontainer/docker-compose-vscode.yml +++ b/.devcontainer/docker-compose-vscode.yml @@ -22,8 +22,8 @@ services: - SIM_V2X_PORT=5757 - SIM_INTERACTION_PORT=7576 - V2X_PORT=8686 - - INFRASTRUCTURE_ID=rsu_ - - INFRASTRUCTURE_NAME= + - INFRASTRUCTURE_ID=rsu_123 + - INFRASTRUCTURE_NAME=SOmething - SENSOR_JSON_FILE_PATH=/var/www/plugins/MAP/sensors.json secrets: - mysql_password diff --git a/src/tmx/Messages/test/Main.cpp b/src/tmx/Messages/test/Main.cpp new file mode 100644 index 000000000..75163d417 --- /dev/null +++ b/src/tmx/Messages/test/Main.cpp @@ -0,0 +1,14 @@ +/* + * Main.cpp + * + * Created on: May 10, 2016 + * Author: ivp + */ + +#include + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/tmx/Messages/test/TestTimeSyncMessage.cpp b/src/tmx/Messages/test/TestTimeSyncMessage.cpp new file mode 100644 index 000000000..c05b46087 --- /dev/null +++ b/src/tmx/Messages/test/TestTimeSyncMessage.cpp @@ -0,0 +1,10 @@ +#include +#include "TimeSyncMessage.h" +namespace tmx::messages { + + TEST(TestTimeSyncMessage, to_string) { + TimeSyncMessage msg(20, 30); + std::string json = "{ \"timestep\":20, \"seq\":30}"; + ASSERT_EQ( json, msg.to_string()); + } +} \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/CMakeLists.txt b/src/v2i-hub/SpatPlugin/CMakeLists.txt index bdff7a0b8..53c386bf2 100644 --- a/src/v2i-hub/SpatPlugin/CMakeLists.txt +++ b/src/v2i-hub/SpatPlugin/CMakeLists.txt @@ -16,7 +16,7 @@ TARGET_LINK_LIBRARIES ( ${PROJECT_NAME} tmxutils ::carma-clock rdkafka++ jsoncpp ################################ enable_testing() -add_library(${PROJECT_NAME}_spat_lib src/NTCIP1202.cpp src/signalController.cpp src/PedestrianDetectionForSPAT.cpp) +add_library(${PROJECT_NAME}_spat_lib src/NTCIP1202.cpp) target_include_directories( ${PROJECT_NAME}_spat_lib PUBLIC ${PROJECT_SOURCE_DIR}/src ${XercesC_INCLUDE_DIRS} ${NETSNMP_INCLUDE_DIRS}) diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index 77cac309c..9f7600f58 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -186,23 +186,6 @@ bool Ntcip1202::isPhaseFlashing() void Ntcip1202::printDebug() { - //printf("phase %d spatVehMinTimeToChange: %02x\r\n",1, ntcip1202Data.phaseTimes[1].spatVehMinTimeToChange); - - //printf("header: %02x\r\n", ntcip1202Data.header); - //printf("phases: %02x\r\n", ntcip1202Data.numOfPhases); - - /*for(int i=0; i<16; i++) - { - printf("phase %d number: %02x\r\n",i, ntcip1202Data.phaseTimes[i].phaseNumber); - - printf("phase %d spatVehMinTimeToChange: %02x\r\n",i, ntcip1202Data.phaseTimes[i].spatVehMinTimeToChange); - printf("phase %d spatVehMaxTimeToChange: %02x\r\n",i, ntcip1202Data.phaseTimes[i].spatVehMaxTimeToChange); - printf("phase %d spatPedMinTimeToChange: %02x\r\n",i, ntcip1202Data.phaseTimes[i].spatPedMinTimeToChange); - printf("phase %d spatPedMaxTimeToChange: %02x\r\n",i, ntcip1202Data.phaseTimes[i].spatPedMaxTimeToChange); - printf("phase %d spatOvlpMinTimeToChange: %02x\r\n",i, ntcip1202Data.phaseTimes[i].spatOvlpMinTimeToChange); - printf("phase %d spatOvpMaxTimeToChange: %02x\r\n",i, ntcip1202Data.phaseTimes[i].spatOvpMaxTimeToChange); - } -*/ for(int i=0; i<16; i++) { int phaseNum = i+1; @@ -216,7 +199,7 @@ void Ntcip1202::printDebug() } } -bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, char* intersectionName, IntersectionID_t intersectionId) +bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, IntersectionID_t intersectionId) { time_t epochSec = clock->nowInSeconds(); struct tm utctime; @@ -242,9 +225,9 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, char* intersectionName, IntersectionI intersection->name = (DescriptiveName_t *) calloc(1, sizeof(DescriptiveName_t)); - intersection->name->size = strlen(intersectionName); - intersection->name->buf = (uint8_t *) calloc(1, strlen(intersectionName)); - memcpy(intersection->name->buf, intersectionName, strlen(intersectionName)); + intersection->name->size = strlen(intersectionName.c_str()); + intersection->name->buf = (uint8_t *) calloc(1, strlen(intersectionName.c_str())); + memcpy(intersection->name->buf, intersectionName.c_str(), strlen(intersectionName.c_str())); intersection->id.id = intersectionId; intersection->revision = (MsgCount_t) 1; @@ -373,16 +356,6 @@ void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase) } //we only get a phase number 1-16 from ped detect, assume its a ped phase -// if(getSpatPedestrianDetect(phase)) -// { -// movement->maneuverAssistList = (ManeuverAssistList *) calloc(1, sizeof(ManeuverAssistList)); -// ConnectionManeuverAssist *pedDetect = (ConnectionManeuverAssist *) calloc(1, sizeof(ConnectionManeuverAssist)); -// pedDetect->connectionID = 0; -// pedDetect->pedBicycleDetect = (PedestrianBicycleDetect_t *) calloc(1, sizeof(PedestrianBicycleDetect_t)); -// -// *(pedDetect->pedBicycleDetect) = 1; -// ASN_SEQUENCE_ADD(&movement->maneuverAssistList->list, pedDetect); -// } ASN_SEQUENCE_ADD(&movement->state_time_speed.list, stateTimeSpeed); } diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h index b3bc9a549..ecea7fb34 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h @@ -105,7 +105,7 @@ class Ntcip1202 bool isFlashingStatus(); bool isPhaseFlashing(); - bool ToJ2735r41SPAT(SPAT* spat, char* intersectionName, IntersectionID_t intersectionId); + bool ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, IntersectionID_t intersectionId); void printDebug(); private: diff --git a/src/v2i-hub/SpatPlugin/src/SignalController.cpp b/src/v2i-hub/SpatPlugin/src/SignalController.cpp new file mode 100644 index 000000000..55b4ee1b3 --- /dev/null +++ b/src/v2i-hub/SpatPlugin/src/SignalController.cpp @@ -0,0 +1,246 @@ +/* + ============================================================================ + Name : snmpClient.cpp + Author : William Gibbs + Version : + Copyright : Battelle + Description : Query Signal Controller and populate the SPaT message + ============================================================================ + */ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "signalController.h" + +#include "NTCIP1202.h" + +using namespace tmx::messages; +using namespace tmx::utils; +using namespace std; + +SignalController::~SignalController() { + SNMPCloseSession(); +} + +void SignalController::Start(std::string signalGroupMappingJson) +{ + _signalGroupMappingJson = signalGroupMappingJson; + // launch update thread + auto sigcon_thread_id = boost::thread(&SignalController::start_signalController, this); + sigcon_thread_id.join(); +} + +// get sockaddr, IPv4 or IPv6: +void *SignalController::get_in_addr(struct sockaddr *sa) +{ + if (sa->sa_family == AF_INET) { + return &(((struct sockaddr_in*)sa)->sin_addr); + } + + return &(((struct sockaddr_in6*)sa)->sin6_addr); +} + +void SignalController::setConfigs(std::string localIp,const std::string localUdpPort, std::string tscIp, std::string tscRemoteSnmpPort, std::string intersectionName, int intersectionId) +{ + _localIp = localIp; + _localUdpPort = std::stoi(localUdpPort); + _intersectionId = intersectionId; + _intersectionName = strdup(intersectionName.c_str()); + _tscIp = tscIp; + _tscRemoteSnmpPort = stoi(tscRemoteSnmpPort); +} + +void SignalController::start_signalController() +{ + + + int maxDataSize = 1000; + + int sockfd, numbytes; + char buf[maxDataSize]; + struct addrinfo hints, *servinfo; + int rv; + struct timeval tv; + int on = 1; + int errnoVal; + + //Enable SPAT + // 0 = disable + // 2 = enable SPAT + // 6 = enable SPAT wit pedestrian data + PLOG(logINFO) << "Enable SPAT Sent"; + SNMPSet("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", 2); + SNMPCloseSession(); + + // Create UDP Socket + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; + + EthernetIsConnected = 0; + IsReceiving = 0; + + while (1) { + if ((rv = getaddrinfo(_localIp, _localUdpPort, &hints, &servinfo)) != 0) { + PLOG(logERROR) << "Getaddrinfo Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; + return; + } + PLOG(logDEBUG) << "Getting Socket"; + if ((sockfd = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)) == -1) { + PLOG(logERROR) << "Get Socket Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; + return; + } + + rv = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); + + // Set the socket to time out on reads if no data comes in during the timeout value then the socket will close and + // then try to re-open during the normal execution + // Wait up to 10 seconds. + tv.tv_sec = 10; + tv.tv_usec = 0; + setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,(struct timeval *)&tv,sizeof(struct timeval)); + + if (bind(sockfd,servinfo->ai_addr,servinfo->ai_addrlen)==-1) { + PLOG(logERROR) << "Could not bind to Socket " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; + return; + } + + if (servinfo == nullptr) { + PLOG(logERROR) << "Could not connect"; + EthernetIsConnected = 0; + } + else { + PLOG(logDEBUG) << "Connected"; + EthernetIsConnected = 1; + } + + if (EthernetIsConnected) { + //printf("Signal Controller UDP Client Connected to %s:%s\n",_localIp, _localUdpPort); + freeaddrinfo(servinfo); // all done with this structure +// client_socket = sockfd; + // Receive Packets and process until disconnected + while(EthernetIsConnected) { + //printf("Signal Controller ethernet connected, reading data\n"); + numbytes = recv(sockfd, buf, maxDataSize-1, 0); + errnoVal = errno; + //printf("Signal Controller read %d bytes\n", numbytes); + //TODO - Check the start byte for 0xcd, then check for len of 245. + //TODO - store in temp space if less than 245, send only from 0xcd (byte 0) to byte 245 to new processing function + if ((numbytes == -1) || (numbytes == 0)){ + if (numbytes == 0 || errnoVal == EAGAIN || errnoVal == EWOULDBLOCK) { + PLOG(logINFO) << "Signal Controller Timed out"; + } else { + PLOG(logINFO) << "Signal Controller Client closed"; + EthernetIsConnected = 0; + close(sockfd); + } + IsReceiving = 0; + } + else { + + IsReceiving = 1; + auto ntcip1202 = std::make_shared(clock); + ntcip1202->setSignalGroupMappingList(_signalGroupMappingJson); + ntcip1202->copyBytesIntoNtcip1202(buf, numbytes); + + SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); + ntcip1202->ToJ2735r41SPAT(_spat, _intersectionName, _intersectionId); + + pthread_mutex_unlock(&spat_message_mutex); + PLOG(logDEBUG) << *_spatMessage; + } + } + } + sleep(3); + } +} + + +int SignalController::getIsConnected() +{ + return EthernetIsConnected && IsReceiving; +} + + +void SignalController::SNMPOpenSession() +{ + //check for valid TSC info + if (_tscIp == "" || _tscRemoteSnmpPort == 0) + return; + //open snmp session + snmp_sess_init(&_session_info); + string peername = _tscIp; + peername.append(":"); + peername.append(to_string(_tscRemoteSnmpPort)); + _session_info.peername = (char*)peername.c_str(); + _session_info.version = SNMP_VERSION_1; + _session_info.community = (u_char*)"public"; + _session_info.community_len = strlen("public"); + _session = snmp_open(&_session_info); + if (_session) + _snmpSessionOpen = true; +} + +void SignalController::SNMPCloseSession() +{ + //close session + if (_snmpSessionOpen) + snmp_close(_session); + _snmpSessionOpen = false; +} + +bool SignalController::SNMPSet(string targetOid, int32_t value) +{ + return SNMPSet(targetOid, ASN_INTEGER, (const void *)&value, sizeof(value)); +} + +bool SignalController::SNMPSet(string targetOid, u_char type, const void *value, size_t len) +{ + struct snmp_pdu *pdu; + struct snmp_pdu *response; + oid anOID[MAX_OID_LEN]; + size_t anOID_len = MAX_OID_LEN; + int status; + bool rc = true; + + //check is snmp session open + if (!_snmpSessionOpen) + { + SNMPOpenSession(); + if (!_snmpSessionOpen) + return false; + _snmpDestinationChanged = false; + } + //check destination change + if (_snmpDestinationChanged) + { + SNMPCloseSession(); + SNMPOpenSession(); + if (!_snmpSessionOpen) + return false; + _snmpDestinationChanged = false; + } + + pdu = snmp_pdu_create(SNMP_MSG_SET); + read_objid(targetOid.c_str(), anOID, &anOID_len); + snmp_pdu_add_variable(pdu, anOID, anOID_len, type, value, len); + status = snmp_synch_response(_session, pdu, &response); + if (status != STAT_SUCCESS || response->errstat != SNMP_ERR_NOERROR) + rc = false; + if (response) + snmp_free_pdu(response); + + return rc; +} + diff --git a/src/v2i-hub/SpatPlugin/src/SignalController.h b/src/v2i-hub/SpatPlugin/src/SignalController.h new file mode 100644 index 000000000..c14b7062a --- /dev/null +++ b/src/v2i-hub/SpatPlugin/src/SignalController.h @@ -0,0 +1,54 @@ +/* + * snmpClient.h + * + * Created on: Aug 22, 2014 + * Author: gibbsw + */ + +#ifndef SIGNALCONTROLLER_H_ +#define SIGNALCONTROLLER_H_ + +#include +#include + +#include +#include +#include +#include "carma-clock/carma_clock.h" + +class SignalController +{ + public: + inline explicit SignalController(std::shared_ptr clock) : clock(clock) {}; + ~SignalController(); + + void Start(std::string signalGroupMappingJson); + void start_signalController(); + void pollSignalControllerConfiguration(); + void setConfigs(std::string ip, std::string udpPort, std::string snmpIP, std::string snmpPort, std::string intersectionName, int intersectionId); + int getIsConnected(); + + + + private: + + std::shared_ptr clock; + std::unique_ptr snmpClent; + std::unique_ptr udpServer; + // Local IP address and UDP port for reception of SPAT from the TSC. + std::string _localIp; + uint _localUdpPort; + std::string _intersectionName; + uint _intersectionId; + std::string _tscIp; + uint32_t _tscRemoteSnmpPort; + + std::string _signalGroupMappingJson; + int EthernetIsConnected; + int IsReceiving; + + + +}; + +#endif /* SIGNALCONTROLLER_H_ */ diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h new file mode 100644 index 000000000..dfa5ee35d --- /dev/null +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h @@ -0,0 +1,35 @@ +#include + +namespace SignalControllerConfig { + /*The signal group state stores the traffic signal controller configuration parameters for a J2735 signal group + * The parameters within it define the phase mapped to the signal group and other data necessary to modify a given phase*/ + struct SignalGroupConfig + { + /* signal group id identifier for J2735 which is mapped to phase number in NTCIP */ + int signal_group_id; + /*Phase associated with signal group*/ + int phase_num; + /*Minimum green durations for each active vehicle phase in milliseconds*/ + int min_green; + /*Maximum green durations for each active vehicle phase in milliseconds*/ + int max_green; + /*Default Green duration for phase in milliseconds*/ + int green_duration; + /*Yellow signal duration for phase in milliseconds*/ + int yellow_duration; + /* Red clearace time for phase in milliseconds*/ + int red_clearance; + /*Red signal duration for phase. This is the total time a phase is predicted to be red before its next green. In milliseconds*/ + int red_duration; + /*Phase sequence in ring. Stores the sequence starting from the current phase*/ + std::vector phase_seq; + /*Phases in the same barrier or concurrent group excluding phases from same ring*/ + std::vector concurrent_signal_groups; + }; + + struct SignalControllerConfigState + { + /* data */ + }; + +} \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/test/testPedestrianDetectionForSPAT.cpp b/src/v2i-hub/SpatPlugin/test/testPedestrianDetectionForSPAT.cpp deleted file mode 100644 index e404307b2..000000000 --- a/src/v2i-hub/SpatPlugin/test/testPedestrianDetectionForSPAT.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include -#include - -using namespace tmx::messages; - -static std::shared_ptr createSPAT() { - auto spatPtr = std::make_shared(); - auto intersectionState = (IntersectionState *)calloc(1, sizeof(IntersectionState)); - intersectionState->id.id = 1; - ASN_SEQUENCE_ADD(&(spatPtr->intersections), intersectionState); - - for (int i = 1; i <= 2; i++) { - auto movementState = (MovementState *)calloc(1, sizeof(MovementState)); - movementState->signalGroup = i; - ASN_SEQUENCE_ADD(&(intersectionState->states), movementState); - - auto movementEvent = (MovementEvent* )calloc(1, sizeof(MovementEvent)); - movementEvent->eventState = - i % 2 == 0 ? MovementPhaseState_protected_Movement_Allowed : MovementPhaseState_stop_And_Remain; - ASN_SEQUENCE_ADD(&(movementState->state_time_speed), movementEvent); - } - return spatPtr; -} - -TEST(PedestrianDetectionForSPAT, updateEncodedSpat_testIncompleteSpat) -{ - PedestrianDetectionForSPAT pedSPAT; - SpatEncodedMessage spatEncoded; - // create incomplete message - auto spatPtr = std::make_shared(); - auto spatMessage = std::make_shared(spatPtr); - bool execptionCaught = false; - try { - pedSPAT.updateEncodedSpat(spatEncoded, spatMessage, ""); - } catch (std::exception & e) { - // should not encode but should get here - execptionCaught = true; - } - EXPECT_EQ(execptionCaught, true); -} - -TEST(PedestrianDetectionForSPAT, updateEncodedSpat) -{ - PedestrianDetectionForSPAT pedSPAT; - // set up a J2735 SPAT to use and add to - auto spatPtr = createSPAT(); - - // first encode the message as is - tmx::messages::SpatEncodedMessage spatEncoded; - tmx::messages::MessageFrameMessage frame(spatPtr); - spatEncoded.set_data(tmx::messages::TmxJ2735EncodedMessage::encode_j2735_message>(frame)); - auto originalSpatHex = spatEncoded.get_payload_str(); - EXPECT_STRNE(originalSpatHex.c_str(), ""); - - // test once with no ped zones and ensure it is the same - { - SpatEncodedMessage spatEncoded; - auto spatMessage = std::make_shared(spatPtr); - pedSPAT.updateEncodedSpat(spatEncoded, spatMessage, ""); - EXPECT_STREQ(spatEncoded.get_payload_str().c_str(), originalSpatHex.c_str()); - } - - // test with ped zones to see that it updates - { - SpatEncodedMessage spatEncoded; - auto spatMessage = std::make_shared(spatPtr); - pedSPAT.updateEncodedSpat(spatEncoded, spatMessage, "2"); - // check hex is not equal to orginal - EXPECT_STRNE(spatEncoded.get_payload_str().c_str(), originalSpatHex.c_str()); - // check that the ped detect was added - ASSERT_EQ(spatPtr->intersections.list.count, 1); - ASSERT_EQ(spatPtr->intersections.list.array[0]->states.list.count, 2); - EXPECT_EQ(spatPtr->intersections.list.array[0]->states.list.array[0]->signalGroup, 1); - EXPECT_EQ(spatPtr->intersections.list.array[0]->states.list.array[1]->signalGroup, 2); - ASSERT_NE(spatPtr->intersections.list.array[0]->maneuverAssistList, nullptr); - EXPECT_NE(spatPtr->intersections.list.array[0]->maneuverAssistList->list.count, 0); - ASSERT_NE(spatPtr->intersections.list.array[0]->maneuverAssistList->list.array[0]->pedBicycleDetect, nullptr); - EXPECT_EQ(*(spatPtr->intersections.list.array[0]->maneuverAssistList->list.array[0]->pedBicycleDetect), 1); - } -} From 19f07458f2c5038f32dcbcdfcc28de276a5e993b Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 27 Jun 2024 16:03:48 -0400 Subject: [PATCH 03/31] Update --- .devcontainer/docker-compose-vscode.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/docker-compose-vscode.yml b/.devcontainer/docker-compose-vscode.yml index dcfa29e73..7900b9dea 100755 --- a/.devcontainer/docker-compose-vscode.yml +++ b/.devcontainer/docker-compose-vscode.yml @@ -13,7 +13,7 @@ services: command: /bin/sh -c "while sleep 1000; do :; done" environment: - MYSQL_PASSWORD=/run/secrets/mysql_password - - SIMULATION_MODE=true + - SIMULATION_MODE=false - SIMULATION_IP=127.0.0.1 - SIMULATION_REGISTRATION_PORT=6767 - LOCAL_IP=127.0.0.1 From 8d3802970443492fcd5a01919dc41f4a6c534b51 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 1 Jul 2024 16:07:24 -0400 Subject: [PATCH 04/31] Updating SPAT Plugin --- src/v2i-hub/SpatPlugin/CMakeLists.txt | 30 +- .../SpatPlugin/src/SignalController.cpp | 246 ----------------- src/v2i-hub/SpatPlugin/src/SignalController.h | 54 ---- .../SpatPlugin/src/SignalControllerConfig.h | 35 --- .../src/SignalControllerConnection.cpp | 8 + .../src/SignalControllerConnection.h | 33 +++ src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 141 +++------- src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 38 +-- .../SpatPlugin/src/signalController.cpp | 257 ------------------ src/v2i-hub/SpatPlugin/src/signalController.h | 75 ----- .../SpatPlugin/src/utils/PerformanceTimer.h | 36 --- 11 files changed, 95 insertions(+), 858 deletions(-) delete mode 100644 src/v2i-hub/SpatPlugin/src/SignalController.cpp delete mode 100644 src/v2i-hub/SpatPlugin/src/SignalController.h delete mode 100644 src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h create mode 100644 src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp create mode 100644 src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h delete mode 100644 src/v2i-hub/SpatPlugin/src/signalController.cpp delete mode 100644 src/v2i-hub/SpatPlugin/src/signalController.h delete mode 100644 src/v2i-hub/SpatPlugin/src/utils/PerformanceTimer.h diff --git a/src/v2i-hub/SpatPlugin/CMakeLists.txt b/src/v2i-hub/SpatPlugin/CMakeLists.txt index 53c386bf2..55e1c5192 100644 --- a/src/v2i-hub/SpatPlugin/CMakeLists.txt +++ b/src/v2i-hub/SpatPlugin/CMakeLists.txt @@ -2,29 +2,22 @@ PROJECT ( SpatPlugin VERSION 7.6.0 LANGUAGES CXX ) SET (TMX_PLUGIN_NAME "SPAT") -FIND_PACKAGE (XercesC REQUIRED) -FIND_PACKAGE (NetSNMP REQUIRED) FIND_PACKAGE (carma-clock REQUIRED) BuildTmxPlugin () -TARGET_INCLUDE_DIRECTORIES ( ${PROJECT_NAME} PUBLIC ${XercesC_INCLUDE_DIRS} ${NETSNMP_INCLUDE_DIRS}) -TARGET_LINK_LIBRARIES ( ${PROJECT_NAME} tmxutils ::carma-clock rdkafka++ jsoncpp ${XercesC_LIBRARY} ${NETSNMP_LIBRARIES}) +TARGET_LINK_LIBRARIES ( ${PROJECT_NAME} tmxutils ::carma-clock jsoncpp) + -################################ -# GTest -################################ -enable_testing() -add_library(${PROJECT_NAME}_spat_lib src/NTCIP1202.cpp) -target_include_directories( ${PROJECT_NAME}_spat_lib PUBLIC - ${PROJECT_SOURCE_DIR}/src - ${XercesC_INCLUDE_DIRS} ${NETSNMP_INCLUDE_DIRS}) -target_link_libraries(${PROJECT_NAME}_spat_lib PUBLIC - tmxutils ::carma-clock ${XercesC_LIBRARY} ${NETSNMP_LIBRARIES}) +add_library(${PROJECT_NAME}_lib src/NTCIP1202.cpp) +target_link_libraries(${PROJECT_NAME}_lib PUBLIC + tmxutils ::carma-clock ) ############# ## Testing ## ############# +enable_testing() + set(BINARY ${PROJECT_NAME}_test) file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false test/*.h test/*.cpp) @@ -35,10 +28,7 @@ set(SOURCES ${TEST_SOURCES} add_executable(${BINARY} ${TEST_SOURCES}) add_test(NAME ${BINARY} COMMAND ${BINARY}) -target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME}_spat_lib ${TMXAPI_LIBRARIES} - ${ASN_J2735_LIBRARIES} - ${MYSQL_LIBRARIES} - ${MYSQLCPPCONN_LIBRARIES} - tmxutils - ${UUID_LIBRARY} +target_include_directories(${BINARY} PUBLIC src/) + +target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME}_lib gtest) \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalController.cpp b/src/v2i-hub/SpatPlugin/src/SignalController.cpp deleted file mode 100644 index 55b4ee1b3..000000000 --- a/src/v2i-hub/SpatPlugin/src/SignalController.cpp +++ /dev/null @@ -1,246 +0,0 @@ -/* - ============================================================================ - Name : snmpClient.cpp - Author : William Gibbs - Version : - Copyright : Battelle - Description : Query Signal Controller and populate the SPaT message - ============================================================================ - */ -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include "signalController.h" - -#include "NTCIP1202.h" - -using namespace tmx::messages; -using namespace tmx::utils; -using namespace std; - -SignalController::~SignalController() { - SNMPCloseSession(); -} - -void SignalController::Start(std::string signalGroupMappingJson) -{ - _signalGroupMappingJson = signalGroupMappingJson; - // launch update thread - auto sigcon_thread_id = boost::thread(&SignalController::start_signalController, this); - sigcon_thread_id.join(); -} - -// get sockaddr, IPv4 or IPv6: -void *SignalController::get_in_addr(struct sockaddr *sa) -{ - if (sa->sa_family == AF_INET) { - return &(((struct sockaddr_in*)sa)->sin_addr); - } - - return &(((struct sockaddr_in6*)sa)->sin6_addr); -} - -void SignalController::setConfigs(std::string localIp,const std::string localUdpPort, std::string tscIp, std::string tscRemoteSnmpPort, std::string intersectionName, int intersectionId) -{ - _localIp = localIp; - _localUdpPort = std::stoi(localUdpPort); - _intersectionId = intersectionId; - _intersectionName = strdup(intersectionName.c_str()); - _tscIp = tscIp; - _tscRemoteSnmpPort = stoi(tscRemoteSnmpPort); -} - -void SignalController::start_signalController() -{ - - - int maxDataSize = 1000; - - int sockfd, numbytes; - char buf[maxDataSize]; - struct addrinfo hints, *servinfo; - int rv; - struct timeval tv; - int on = 1; - int errnoVal; - - //Enable SPAT - // 0 = disable - // 2 = enable SPAT - // 6 = enable SPAT wit pedestrian data - PLOG(logINFO) << "Enable SPAT Sent"; - SNMPSet("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", 2); - SNMPCloseSession(); - - // Create UDP Socket - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; - - EthernetIsConnected = 0; - IsReceiving = 0; - - while (1) { - if ((rv = getaddrinfo(_localIp, _localUdpPort, &hints, &servinfo)) != 0) { - PLOG(logERROR) << "Getaddrinfo Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; - return; - } - PLOG(logDEBUG) << "Getting Socket"; - if ((sockfd = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)) == -1) { - PLOG(logERROR) << "Get Socket Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; - return; - } - - rv = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); - - // Set the socket to time out on reads if no data comes in during the timeout value then the socket will close and - // then try to re-open during the normal execution - // Wait up to 10 seconds. - tv.tv_sec = 10; - tv.tv_usec = 0; - setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,(struct timeval *)&tv,sizeof(struct timeval)); - - if (bind(sockfd,servinfo->ai_addr,servinfo->ai_addrlen)==-1) { - PLOG(logERROR) << "Could not bind to Socket " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; - return; - } - - if (servinfo == nullptr) { - PLOG(logERROR) << "Could not connect"; - EthernetIsConnected = 0; - } - else { - PLOG(logDEBUG) << "Connected"; - EthernetIsConnected = 1; - } - - if (EthernetIsConnected) { - //printf("Signal Controller UDP Client Connected to %s:%s\n",_localIp, _localUdpPort); - freeaddrinfo(servinfo); // all done with this structure -// client_socket = sockfd; - // Receive Packets and process until disconnected - while(EthernetIsConnected) { - //printf("Signal Controller ethernet connected, reading data\n"); - numbytes = recv(sockfd, buf, maxDataSize-1, 0); - errnoVal = errno; - //printf("Signal Controller read %d bytes\n", numbytes); - //TODO - Check the start byte for 0xcd, then check for len of 245. - //TODO - store in temp space if less than 245, send only from 0xcd (byte 0) to byte 245 to new processing function - if ((numbytes == -1) || (numbytes == 0)){ - if (numbytes == 0 || errnoVal == EAGAIN || errnoVal == EWOULDBLOCK) { - PLOG(logINFO) << "Signal Controller Timed out"; - } else { - PLOG(logINFO) << "Signal Controller Client closed"; - EthernetIsConnected = 0; - close(sockfd); - } - IsReceiving = 0; - } - else { - - IsReceiving = 1; - auto ntcip1202 = std::make_shared(clock); - ntcip1202->setSignalGroupMappingList(_signalGroupMappingJson); - ntcip1202->copyBytesIntoNtcip1202(buf, numbytes); - - SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); - ntcip1202->ToJ2735r41SPAT(_spat, _intersectionName, _intersectionId); - - pthread_mutex_unlock(&spat_message_mutex); - PLOG(logDEBUG) << *_spatMessage; - } - } - } - sleep(3); - } -} - - -int SignalController::getIsConnected() -{ - return EthernetIsConnected && IsReceiving; -} - - -void SignalController::SNMPOpenSession() -{ - //check for valid TSC info - if (_tscIp == "" || _tscRemoteSnmpPort == 0) - return; - //open snmp session - snmp_sess_init(&_session_info); - string peername = _tscIp; - peername.append(":"); - peername.append(to_string(_tscRemoteSnmpPort)); - _session_info.peername = (char*)peername.c_str(); - _session_info.version = SNMP_VERSION_1; - _session_info.community = (u_char*)"public"; - _session_info.community_len = strlen("public"); - _session = snmp_open(&_session_info); - if (_session) - _snmpSessionOpen = true; -} - -void SignalController::SNMPCloseSession() -{ - //close session - if (_snmpSessionOpen) - snmp_close(_session); - _snmpSessionOpen = false; -} - -bool SignalController::SNMPSet(string targetOid, int32_t value) -{ - return SNMPSet(targetOid, ASN_INTEGER, (const void *)&value, sizeof(value)); -} - -bool SignalController::SNMPSet(string targetOid, u_char type, const void *value, size_t len) -{ - struct snmp_pdu *pdu; - struct snmp_pdu *response; - oid anOID[MAX_OID_LEN]; - size_t anOID_len = MAX_OID_LEN; - int status; - bool rc = true; - - //check is snmp session open - if (!_snmpSessionOpen) - { - SNMPOpenSession(); - if (!_snmpSessionOpen) - return false; - _snmpDestinationChanged = false; - } - //check destination change - if (_snmpDestinationChanged) - { - SNMPCloseSession(); - SNMPOpenSession(); - if (!_snmpSessionOpen) - return false; - _snmpDestinationChanged = false; - } - - pdu = snmp_pdu_create(SNMP_MSG_SET); - read_objid(targetOid.c_str(), anOID, &anOID_len); - snmp_pdu_add_variable(pdu, anOID, anOID_len, type, value, len); - status = snmp_synch_response(_session, pdu, &response); - if (status != STAT_SUCCESS || response->errstat != SNMP_ERR_NOERROR) - rc = false; - if (response) - snmp_free_pdu(response); - - return rc; -} - diff --git a/src/v2i-hub/SpatPlugin/src/SignalController.h b/src/v2i-hub/SpatPlugin/src/SignalController.h deleted file mode 100644 index c14b7062a..000000000 --- a/src/v2i-hub/SpatPlugin/src/SignalController.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * snmpClient.h - * - * Created on: Aug 22, 2014 - * Author: gibbsw - */ - -#ifndef SIGNALCONTROLLER_H_ -#define SIGNALCONTROLLER_H_ - -#include -#include - -#include -#include -#include -#include "carma-clock/carma_clock.h" - -class SignalController -{ - public: - inline explicit SignalController(std::shared_ptr clock) : clock(clock) {}; - ~SignalController(); - - void Start(std::string signalGroupMappingJson); - void start_signalController(); - void pollSignalControllerConfiguration(); - void setConfigs(std::string ip, std::string udpPort, std::string snmpIP, std::string snmpPort, std::string intersectionName, int intersectionId); - int getIsConnected(); - - - - private: - - std::shared_ptr clock; - std::unique_ptr snmpClent; - std::unique_ptr udpServer; - // Local IP address and UDP port for reception of SPAT from the TSC. - std::string _localIp; - uint _localUdpPort; - std::string _intersectionName; - uint _intersectionId; - std::string _tscIp; - uint32_t _tscRemoteSnmpPort; - - std::string _signalGroupMappingJson; - int EthernetIsConnected; - int IsReceiving; - - - -}; - -#endif /* SIGNALCONTROLLER_H_ */ diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h deleted file mode 100644 index dfa5ee35d..000000000 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConfig.h +++ /dev/null @@ -1,35 +0,0 @@ -#include - -namespace SignalControllerConfig { - /*The signal group state stores the traffic signal controller configuration parameters for a J2735 signal group - * The parameters within it define the phase mapped to the signal group and other data necessary to modify a given phase*/ - struct SignalGroupConfig - { - /* signal group id identifier for J2735 which is mapped to phase number in NTCIP */ - int signal_group_id; - /*Phase associated with signal group*/ - int phase_num; - /*Minimum green durations for each active vehicle phase in milliseconds*/ - int min_green; - /*Maximum green durations for each active vehicle phase in milliseconds*/ - int max_green; - /*Default Green duration for phase in milliseconds*/ - int green_duration; - /*Yellow signal duration for phase in milliseconds*/ - int yellow_duration; - /* Red clearace time for phase in milliseconds*/ - int red_clearance; - /*Red signal duration for phase. This is the total time a phase is predicted to be red before its next green. In milliseconds*/ - int red_duration; - /*Phase sequence in ring. Stores the sequence starting from the current phase*/ - std::vector phase_seq; - /*Phases in the same barrier or concurrent group excluding phases from same ring*/ - std::vector concurrent_signal_groups; - }; - - struct SignalControllerConfigState - { - /* data */ - }; - -} \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp new file mode 100644 index 000000000..3828d5cfc --- /dev/null +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -0,0 +1,8 @@ +#include "SignalControllerConnection.h" + +namespace SpatPlugin { + + SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_unique(localIp, localPort)) ,scSNMPClient(std::make_unique(scIp, scSNMPPort ,"", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { + + }; +} \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h new file mode 100644 index 000000000..e129f24bc --- /dev/null +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +namespace SpatPlugin { + enum class SPAT_MODE + { + J2735_HEX, + BINARY + }; + + class SignalControllerConnection + { + private: + // UDP Server socket listening for SPAT + std::unique_ptr spatPacketReceiver; + + std::unique_ptr scSNMPClient; + + std::string signalGroupMapping; + std::string intersectionName; + unsigned int intersectionId; + void initializeSignalControllerConnection(); + + public: + SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); + + tmx::messages::SpatEncodedMessage receiveSPAT(uint64_t timeMs , const SPAT_MODE &spat_mode = SPAT_MODE::BINARY); + + }; +} \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index b510b9e90..91c803734 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -1,6 +1,5 @@ #include "SpatPlugin.h" -#include using namespace std; using namespace tmx::messages; @@ -8,119 +7,55 @@ using namespace tmx::utils; namespace SpatPlugin { -SpatPlugin::SpatPlugin(string name) : - PluginClientClockAware(name), sc(getClock()), intersectionId(0) { - SubscribeToMessages(); -} - -SpatPlugin::~SpatPlugin() { - -} - -void SpatPlugin::UpdateConfigSettings() { - - GetConfigValue("SignalGroupMapping", signalGroupMappingJson, &data_lock); - GetConfigValue("Local_IP", localIp, &data_lock); - GetConfigValue("Local_UDP_Port", localUdpPort, &data_lock); - GetConfigValue("TSC_IP", tscIp, &data_lock); - GetConfigValue("TSC_Remote_SNMP_Port", tscRemoteSnmpPort, - &data_lock); - GetConfigValue("Intersection_Name", intersectionName, - &data_lock); - GetConfigValue("Intersection_Id", intersectionId, &data_lock); + SpatPlugin::SpatPlugin(string name) :PluginClientClockAware(name) { + if ( PluginClientClockAware::isSimulationMode() ) { + SubscribeToMessages(); + } + } - isConfigurationLoaded = true; -} + SpatPlugin::~SpatPlugin() { -void SpatPlugin::OnConfigChanged(const char *key, const char *value) { - PluginClient::OnConfigChanged(key, value); - UpdateConfigSettings(); -} + } -void SpatPlugin::OnStateChange(IvpPluginState state) { - PluginClientClockAware::OnStateChange(state); + void SpatPlugin::UpdateConfigSettings() { + + if (this->IsPluginState(IvpPluginState_registered)) { + std::string signalGroupMappingJson; + std::string ip_address; + unsigned int port; + std::string scIp; + unsigned int scSNMPPort; + std::string intersectionName; + unsigned int intersectionId; + GetConfigValue("SignalGroupMapping", signalGroupMappingJson, &data_lock); + GetConfigValue("Local_IP", ip_address, &data_lock); + GetConfigValue("Local_UDP_Port", port, &data_lock); + GetConfigValue("TSC_IP", scIp, &data_lock); + GetConfigValue("TSC_Remote_SNMP_Port", scSNMPPort,&data_lock); + GetConfigValue("Intersection_Name", intersectionName,&data_lock); + GetConfigValue("Intersection_Id", intersectionId, &data_lock); + if (scConnection) { + scConnection.reset(new SignalControllerConnection(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId)); + } + else { + scConnection = std::make_unique(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId); + } + } + } - if (state == IvpPluginState_registered) { + void SpatPlugin::OnConfigChanged(const char *key, const char *value) { + PluginClientClockAware::OnConfigChanged(key, value); UpdateConfigSettings(); } -} - -int SpatPlugin::Main() { + void SpatPlugin::OnStateChange(IvpPluginState state) { + PluginClientClockAware::OnStateChange(state); - int iCounter = 0; - - PLOG(logINFO) << "Waiting for clock initialization"; - // wait for the clock to be initialized and record the time when it is ready - getClock()->wait_for_initialization(); - auto nextSpatTime = getClock()->nowInMilliseconds(); - PLOG(logINFO) << "Initial nextSpatTime=" << nextSpatTime; - - try { - while (_plugin->state != IvpPluginState_error) { - // wait to send next message - if (isConfigurationLoaded) { - if (!isConfigured) { - - { - std::lock_guard lock(data_lock); - string ptlm = ""; - sc.setConfigs(localIp, localUdpPort, tscIp, - tscRemoteSnmpPort, intersectionName, - intersectionId); - } - // Start the signal controller thread. - sc.Start(signalGroupMappingJson); - // Give the spatdata pointer to the message class - isConfigured = true; - } - - // SPaT must be sent exactly every 100 ms. So adjust for how long it took to do the last send. - nextSpatTime += 100; - getClock()->sleep_until(nextSpatTime); - - iCounter++; - - bool messageSent = false; - - if (sc.getIsConnected()) { - SetStatus("TSC Connection", "Connected"); - - // Add pedestrian detection - string pedZones; - { - lock_guard lock(data_lock); - pedZones = _pedMessage.get_DetectionZones(); - } - if (!pedZones.empty()) { - PLOG(logDEBUG) << "Pedestrians detected in lanes " << pedZones; - } - - SpatEncodedMessage spatEncodedMsg; - - - spatEncodedMsg.set_flags(IvpMsgFlags_RouteDSRC); - spatEncodedMsg.addDsrcMetadata(0x8002); - - - BroadcastMessage(static_cast(spatEncodedMsg)); - - - } else { - SetStatus("TSC Connection", "Disconnected"); - } - } + if (state == IvpPluginState_registered) { + UpdateConfigSettings(); } - } catch (const exception &ex) { - stringstream ss; - PLOG(logERROR) << "SpatPlugin terminating from unhandled exception: " << ex.what(); - - ivp_addEventLog(_plugin, IvpLogLevel_error, ex.what().c_str()); - std::terminate(); } - return EXIT_SUCCESS; -} } /* End namespace SpatPlugin */ diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index caec33093..c2084d0a4 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -13,16 +13,11 @@ #include #include #include -#include "PluginClientClockAware.h" -#include "UdpClient.h" -#include "signalController.h" - -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include "SignalControllerConnection.h" namespace SpatPlugin { @@ -32,9 +27,7 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { SpatPlugin(std::string name); virtual ~SpatPlugin(); - virtual int Main(); - void HandlePedestrianDetection(tmx::messages::PedestrianMessage &pedMsg, tmx::routeable_message &routeableMsg); protected: @@ -44,33 +37,14 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { void OnConfigChanged(const char *key, const char *value); void OnStateChange(IvpPluginState state); - private: - - unsigned char derEncoded[4000]; - unsigned int derEncodedBytes = 0; - - SignalController sc; - std::mutex data_lock; - std::string localIp; - std::string localUdpPort; - std::string tscIp; - std::string tscRemoteSnmpPort; - std::string signalGroupMappingJson; - - std::string intersectionName; - int intersectionId; + std::unique_ptr scConnection; - bool isConfigurationLoaded = false; - bool isConfigured = false; - bool encodeSpat(); - bool createUPERframe_DERencoded_msg(); - tmx::messages::PedestrianMessage _pedMessage; }; } /* namespace SpatPlugin */ diff --git a/src/v2i-hub/SpatPlugin/src/signalController.cpp b/src/v2i-hub/SpatPlugin/src/signalController.cpp deleted file mode 100644 index f8af563a8..000000000 --- a/src/v2i-hub/SpatPlugin/src/signalController.cpp +++ /dev/null @@ -1,257 +0,0 @@ -/* - ============================================================================ - Name : snmpClient.cpp - Author : William Gibbs - Version : - Copyright : Battelle - Description : Query Signal Controller and populate the SPaT message - ============================================================================ - */ -#include -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include "signalController.h" - -#include "NTCIP1202.h" -#include "PedestrianDetectionForSPAT.h" - -using namespace tmx::messages; -using namespace tmx::utils; -using namespace std; - -SignalController::~SignalController() { - SNMPCloseSession(); -} - -void SignalController::Start(std::string signalGroupMappingJson) -{ - _signalGroupMappingJson = signalGroupMappingJson; - - // Create mutex for the Spat message - pthread_mutex_init(&spat_message_mutex, nullptr); - // launch update thread - sigcon_thread_id = boost::thread(&SignalController::start_signalController, this); - // test code - normalstate = 0x01; - crossstate = 0x04; -} - -// get sockaddr, IPv4 or IPv6: -void *SignalController::get_in_addr(struct sockaddr *sa) -{ - if (sa->sa_family == AF_INET) { - return &(((struct sockaddr_in*)sa)->sin_addr); - } - - return &(((struct sockaddr_in6*)sa)->sin6_addr); -} - -void SignalController::setConfigs(std::string localIp, std::string localUdpPort, std::string tscIp, std::string tscRemoteSnmpPort, std::string intersectionName, int intersectionId) -{ - _localIp = strdup(localIp.c_str()); - _localUdpPort = strdup(localUdpPort.c_str()); - _intersectionId = intersectionId; - _intersectionName = strdup(intersectionName.c_str()); - _tscIp = tscIp; - _tscRemoteSnmpPort = stoi(tscRemoteSnmpPort); -} - -void SignalController::start_signalController() -{ - - - int maxDataSize = 1000; - - int sockfd, numbytes; - char buf[maxDataSize]; - struct addrinfo hints, *servinfo; - int rv; - struct timeval tv; - int on = 1; - int errnoVal; - - //Enable SPAT - // 0 = disable - // 2 = enable SPAT - // 6 = enable SPAT wit pedestrian data - PLOG(logINFO) << "Enable SPAT Sent"; - SNMPSet("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", 2); - SNMPCloseSession(); - - // Create UDP Socket - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; - - EthernetIsConnected = 0; - IsReceiving = 0; - - while (1) { - if ((rv = getaddrinfo(_localIp, _localUdpPort, &hints, &servinfo)) != 0) { - PLOG(logERROR) << "Getaddrinfo Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; - return; - } - PLOG(logDEBUG) << "Getting Socket"; - if ((sockfd = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)) == -1) { - PLOG(logERROR) << "Get Socket Failed " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; - return; - } - - rv = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); - - // Set the socket to time out on reads if no data comes in during the timeout value then the socket will close and - // then try to re-open during the normal execution - // Wait up to 10 seconds. - tv.tv_sec = 10; - tv.tv_usec = 0; - setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,(struct timeval *)&tv,sizeof(struct timeval)); - - if (bind(sockfd,servinfo->ai_addr,servinfo->ai_addrlen)==-1) { - PLOG(logERROR) << "Could not bind to Socket " << _localIp << " " << _localUdpPort << ". Exiting thread!!!"; - return; - } - - if (servinfo == nullptr) { - PLOG(logERROR) << "Could not connect"; - EthernetIsConnected = 0; - } - else { - PLOG(logDEBUG) << "Connected"; - EthernetIsConnected = 1; - } - - if (EthernetIsConnected) { - //printf("Signal Controller UDP Client Connected to %s:%s\n",_localIp, _localUdpPort); - freeaddrinfo(servinfo); // all done with this structure -// client_socket = sockfd; - // Receive Packets and process until disconnected - while(EthernetIsConnected) { - //printf("Signal Controller ethernet connected, reading data\n"); - numbytes = recv(sockfd, buf, maxDataSize-1, 0); - errnoVal = errno; - //printf("Signal Controller read %d bytes\n", numbytes); - //TODO - Check the start byte for 0xcd, then check for len of 245. - //TODO - store in temp space if less than 245, send only from 0xcd (byte 0) to byte 245 to new processing function - if ((numbytes == -1) || (numbytes == 0)){ - if (numbytes == 0 || errnoVal == EAGAIN || errnoVal == EWOULDBLOCK) { - PLOG(logINFO) << "Signal Controller Timed out"; - } else { - PLOG(logINFO) << "Signal Controller Client closed"; - EthernetIsConnected = 0; - close(sockfd); - } - IsReceiving = 0; - } - else { - - IsReceiving = 1; - pthread_mutex_lock(&spat_message_mutex); - auto ntcip1202 = std::make_shared(clock); - ntcip1202->setSignalGroupMappingList(_signalGroupMappingJson); - ntcip1202->copyBytesIntoNtcip1202(buf, numbytes); - - SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); - ntcip1202->ToJ2735r41SPAT(_spat, _intersectionName, _intersectionId); - - pthread_mutex_unlock(&spat_message_mutex); - PLOG(logDEBUG) << *_spatMessage; - } - } - } - sleep(3); - } -} - - -int SignalController::getIsConnected() -{ - return EthernetIsConnected && IsReceiving; -} - -int SignalController::getActionNumber() -{ - return 1; -} - -void SignalController::SNMPOpenSession() -{ - //check for valid TSC info - if (_tscIp == "" || _tscRemoteSnmpPort == 0) - return; - //open snmp session - snmp_sess_init(&_session_info); - string peername = _tscIp; - peername.append(":"); - peername.append(to_string(_tscRemoteSnmpPort)); - _session_info.peername = (char*)peername.c_str(); - _session_info.version = SNMP_VERSION_1; - _session_info.community = (u_char*)"public"; - _session_info.community_len = strlen("public"); - _session = snmp_open(&_session_info); - if (_session) - _snmpSessionOpen = true; -} - -void SignalController::SNMPCloseSession() -{ - //close session - if (_snmpSessionOpen) - snmp_close(_session); - _snmpSessionOpen = false; -} - -bool SignalController::SNMPSet(string targetOid, int32_t value) -{ - return SNMPSet(targetOid, ASN_INTEGER, (const void *)&value, sizeof(value)); -} - -bool SignalController::SNMPSet(string targetOid, u_char type, const void *value, size_t len) -{ - struct snmp_pdu *pdu; - struct snmp_pdu *response; - oid anOID[MAX_OID_LEN]; - size_t anOID_len = MAX_OID_LEN; - int status; - bool rc = true; - - //check is snmp session open - if (!_snmpSessionOpen) - { - SNMPOpenSession(); - if (!_snmpSessionOpen) - return false; - _snmpDestinationChanged = false; - } - //check destination change - if (_snmpDestinationChanged) - { - SNMPCloseSession(); - SNMPOpenSession(); - if (!_snmpSessionOpen) - return false; - _snmpDestinationChanged = false; - } - - pdu = snmp_pdu_create(SNMP_MSG_SET); - read_objid(targetOid.c_str(), anOID, &anOID_len); - snmp_pdu_add_variable(pdu, anOID, anOID_len, type, value, len); - status = snmp_synch_response(_session, pdu, &response); - if (status != STAT_SUCCESS || response->errstat != SNMP_ERR_NOERROR) - rc = false; - if (response) - snmp_free_pdu(response); - - return rc; -} - diff --git a/src/v2i-hub/SpatPlugin/src/signalController.h b/src/v2i-hub/SpatPlugin/src/signalController.h deleted file mode 100644 index 347a69cad..000000000 --- a/src/v2i-hub/SpatPlugin/src/signalController.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * snmpClient.h - * - * Created on: Aug 22, 2014 - * Author: gibbsw - */ - -#ifndef SIGNALCONTROLLER_H_ -#define SIGNALCONTROLLER_H_ - -#include -#include - -#include - -#include -#include - -#include "carma-clock/carma_clock.h" - -class SignalController -{ - public: - inline explicit SignalController(std::shared_ptr clock) : - clock(clock) {}; - ~SignalController(); - - void Start(std::string signalGroupMappingJson); - void spat_load(); - void start_signalController(); - int getActionNumber(); - void setConfigs(std::string ip, std::string udpPort, std::string snmpIP, std::string snmpPort, std::string intersectionName, int intersectionId); - int getIsConnected(); - - //int getDerEncodedSpat(unsigned char* derEncodedBuffer); - - void getEncodedSpat(tmx::messages::SpatEncodedMessage* spatEncodedMsg, std::string currentPedLanes = ""); - - pthread_mutex_t spat_message_mutex; - boost::thread sigcon_thread_id; - - void SNMPOpenSession(); - void SNMPCloseSession(); - bool SNMPSet(std::string targetOid, int32_t value); - bool SNMPSet(std::string targetOid, u_char type, const void *value, size_t len); - - private: - void *get_in_addr(struct sockaddr *); - - std::shared_ptr clock; - - // Local IP address and UDP port for reception of SPAT dSPaTDataata from the TSC. - char* _localIp; - char* _localUdpPort; - char* _intersectionName; - int _intersectionId; - std::string _tscIp; - uint32_t _tscRemoteSnmpPort; - - std::string _signalGroupMappingJson; - int counter; - unsigned long normalstate; - unsigned long crossstate; - int EthernetIsConnected; - int IsReceiving; - - //snmp - struct snmp_session _session_info; - struct snmp_session *_session{NULL}; - bool _snmpSessionOpen{false}; - bool _snmpDestinationChanged{false}; - -}; - -#endif /* SIGNALCONTROLLER_H_ */ diff --git a/src/v2i-hub/SpatPlugin/src/utils/PerformanceTimer.h b/src/v2i-hub/SpatPlugin/src/utils/PerformanceTimer.h deleted file mode 100644 index b7952aa69..000000000 --- a/src/v2i-hub/SpatPlugin/src/utils/PerformanceTimer.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * PerformanceTimer.h - * - * Created on: Dec 5, 2014 - * Author: ivp - */ - -#ifndef PERFORMANCETIMER_H_ -#define PERFORMANCETIMER_H_ - -#include - -class PerformanceTimer -{ -public: - // Returns the current high-resolution system time in UTC. - static boost::posix_time::ptime Now() { return boost::posix_time::microsec_clock::universal_time(); } - - // Construct and start the timer. - PerformanceTimer() : _startTime( Now() ) {}; - - // Reset the timer to the current time. - void Reset() { _startTime = Now(); } - - // Returns the elapsed time. - boost::posix_time::time_duration const Elapsed() - { - return Now() - _startTime; - } - -private: - // The time of class construction or when last Reset(). - boost::posix_time::ptime _startTime; -}; - -#endif /* PERFORMANCETIMER_H_ */ From 8322a3e6797e240f65707dd400d241247cb04b72 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 1 Jul 2024 17:52:50 -0400 Subject: [PATCH 05/31] Updates --- .../src/SignalControllerConnection.cpp | 28 +++++++++++++++++++ .../src/SignalControllerConnection.h | 1 + 2 files changed, 29 insertions(+) diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index 3828d5cfc..a0bf11794 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -5,4 +5,32 @@ namespace SpatPlugin { SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_unique(localIp, localPort)) ,scSNMPClient(std::make_unique(scIp, scSNMPPort ,"", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; + + tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(uint64_t timeMs , const SPAT_MODE &spatMode = SPAT_MODE::BINARY) { + if ( spatMode == SPAT_MODE::BINARY ) { + char buf[1000]; + auto ntcip1202 = std::make_unique(clock); + auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 3); + if ( numBytes > 0 ) { + // TODO: Revist this implementation. See if we can make SPAT a shared pointer + // and skipe the SPAT to SpatMessage conversion. + ntcip1202->copyBytesIntoNtcip1202(buf, numBytes); + SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); + ntcip1202->ToJ2735r41SPAT(_spat, intersectionName, intersectionId); + auto _spatMessage = std::make_unique(_spat); + tmx::messages::MessageFrameMessage frame(_spatMessage->get_j2735_data()); + tmx::messages::SpatEncodedMessage spatEncodedMsg; + spatEncodedMsg.set_data(tmx::messages::TmxJ2735EncodedMessage::encode_j2735_message>(frame)); + //Free the memory allocated for MessageFrame + free(frame.get_j2735_data().get()); + + }else { + throw std::runtime_error("Something went wrong"); + } + } + else if ( spatMode == SPAT_MODE::J2735_HEX ) { + + } + } + } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index e129f24bc..96c9ce6a4 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -3,6 +3,7 @@ #include #include #include +#include "NTCIP1202.h" namespace SpatPlugin { enum class SPAT_MODE From b09b90a76470c69bc3d43d0828653624cfc43c5e Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 2 Jul 2024 10:37:10 -0400 Subject: [PATCH 06/31] Updates --- src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 34 +++++++++---------- src/v2i-hub/SpatPlugin/src/NTCIP1202.h | 14 ++++---- .../src/SignalControllerConnection.cpp | 26 ++++++++++---- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 14 ++++++++ src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 5 +-- .../SpatPlugin/test/test_NTCIP1202.cpp | 18 +++++----- 6 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index 9f7600f58..9af2da18c 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -199,9 +199,9 @@ void Ntcip1202::printDebug() } } -bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, IntersectionID_t intersectionId) +bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, unsigned long msEpoch , const std::string &intersectionName, IntersectionID_t intersectionId) { - time_t epochSec = clock->nowInSeconds(); + time_t epochSec = msEpoch/1000; struct tm utctime; gmtime_r( &epochSec, &utctime ); @@ -210,7 +210,7 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, long minOfYear = utctime.tm_min + (utctime.tm_hour * 60) + (utctime.tm_yday * 24 * 60); // Calculate the millisecond of the minute - auto epochMs = clock->nowInMilliseconds(); + auto epochMs = msEpoch; long msOfMin = 1000 * (epochSec % 60) + (epochMs % 1000); std::lock_guard lock(_spat_lock); @@ -259,7 +259,7 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, MovementState *movement = (MovementState *) calloc(1, sizeof(MovementState)); movement->signalGroup = it->SignalGroupId; - populateVehicleSignalGroup(movement, phase); + populateVehicleSignalGroup(movement, phase, msEpoch); ASN_SEQUENCE_ADD(&intersection->states.list, movement); } @@ -276,7 +276,7 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, MovementState *movement = (MovementState *) calloc(1, sizeof(MovementState)); movement->signalGroup = it->SignalGroupId; - populatePedestrianSignalGroup(movement, phase); + populatePedestrianSignalGroup(movement, phase, msEpoch); ASN_SEQUENCE_ADD(&intersection->states.list, movement); } @@ -292,7 +292,7 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, MovementState *movement = (MovementState *) calloc(1, sizeof(MovementState)); movement->signalGroup = it->SignalGroupId; - populateOverlapSignalGroup(movement, phase); + populateOverlapSignalGroup(movement, phase, msEpoch); ASN_SEQUENCE_ADD(&intersection->states.list, movement); } @@ -308,7 +308,7 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, return true; } -void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase) +void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase, unsigned long msEpoch) { MovementEvent *stateTimeSpeed = (MovementEvent *) calloc(1, sizeof(MovementEvent)); @@ -347,12 +347,12 @@ void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase) } stateTimeSpeed->timing = (TimeChangeDetails * ) calloc(1, sizeof(TimeChangeDetails)); - stateTimeSpeed->timing->minEndTime = getAdjustedTime(getVehicleMinTime(phase)); + stateTimeSpeed->timing->minEndTime = getAdjustedTime(getVehicleMinTime(phase),msEpoch); if (getVehicleMaxTime(phase) > 0) { stateTimeSpeed->timing->maxEndTime = (TimeMark_t *) calloc(1, sizeof(TimeMark_t)); - *(stateTimeSpeed->timing->maxEndTime) = getAdjustedTime(getVehicleMaxTime(phase)); + *(stateTimeSpeed->timing->maxEndTime) = getAdjustedTime(getVehicleMaxTime(phase), msEpoch); } //we only get a phase number 1-16 from ped detect, assume its a ped phase @@ -360,7 +360,7 @@ void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase) ASN_SEQUENCE_ADD(&movement->state_time_speed.list, stateTimeSpeed); } -void Ntcip1202::populatePedestrianSignalGroup(MovementState *movement, int phase) +void Ntcip1202::populatePedestrianSignalGroup(MovementState *movement, int phase, unsigned long msEpoch) { MovementEvent *stateTimeSpeed = (MovementEvent *) calloc(1, sizeof(MovementEvent)); @@ -382,12 +382,12 @@ void Ntcip1202::populatePedestrianSignalGroup(MovementState *movement, int phase } stateTimeSpeed->timing = (TimeChangeDetails * ) calloc(1, sizeof(TimeChangeDetails)); - stateTimeSpeed->timing->minEndTime = getAdjustedTime(getPedMinTime(phase)); + stateTimeSpeed->timing->minEndTime = getAdjustedTime(getPedMinTime(phase), msEpoch); if (ntcip1202Data.phaseTimes[phase].spatPedMaxTimeToChange > 0) { stateTimeSpeed->timing->maxEndTime = (TimeMark_t *) calloc(1, sizeof(TimeMark_t)); - *(stateTimeSpeed->timing->maxEndTime) = getAdjustedTime(getPedMaxTime(phase)); + *(stateTimeSpeed->timing->maxEndTime) = getAdjustedTime(getPedMaxTime(phase), msEpoch); } if(getSpatPedestrianDetect(phase)) @@ -403,7 +403,7 @@ void Ntcip1202::populatePedestrianSignalGroup(MovementState *movement, int phase ASN_SEQUENCE_ADD(&movement->state_time_speed.list, stateTimeSpeed); } -void Ntcip1202::populateOverlapSignalGroup(MovementState *movement, int phase) +void Ntcip1202::populateOverlapSignalGroup(MovementState *movement, int phase, unsigned long msEpoch) { MovementEvent *stateTimeSpeed = (MovementEvent *) calloc(1, sizeof(MovementEvent)); @@ -438,12 +438,12 @@ void Ntcip1202::populateOverlapSignalGroup(MovementState *movement, int phase) } stateTimeSpeed->timing = (TimeChangeDetails * ) calloc(1, sizeof(TimeChangeDetails)); - stateTimeSpeed->timing->minEndTime = getAdjustedTime(getOverlapMinTime(phase)); + stateTimeSpeed->timing->minEndTime = getAdjustedTime(getOverlapMinTime(phase), msEpoch); if (getOverlapMaxTime(phase) > 0) { stateTimeSpeed->timing->maxEndTime = (TimeMark_t *) calloc(1, sizeof(TimeMark_t)); - *(stateTimeSpeed->timing->maxEndTime) = getAdjustedTime(getOverlapMaxTime(phase)); + *(stateTimeSpeed->timing->maxEndTime) = getAdjustedTime(getOverlapMaxTime(phase), msEpoch); } //we only get a phase number 1-16 from ped detect, assume its a ped phase @@ -491,13 +491,13 @@ int Ntcip1202::getPedestrianSignalGroupForPhase(int phase) return signalGroupId; } -long Ntcip1202::getAdjustedTime(unsigned int offset_tenthofSec) +long Ntcip1202::getAdjustedTime(unsigned int offset_tenthofSec, unsigned long msEpoch) { // generate J2735 TimeMark which is: // Tenths of a second in the current or next hour // In units of 1/10th second from UTC time // first get new time is absolute milliseconds - auto epochMs = clock->nowInMilliseconds() + (offset_tenthofSec * 100); + auto epochMs = msEpoch + (offset_tenthofSec * 100); // get minute and second of hour from UTC time time_t epochSec = epochMs / 1000; struct tm utctime; diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h index ecea7fb34..7c23c1674 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h @@ -68,8 +68,7 @@ struct SignalGroupMapping class Ntcip1202 { public: - inline explicit Ntcip1202(std::shared_ptr clock) : - clock(clock) {}; + inline explicit Ntcip1202() {}; void setSignalGroupMappingList(string json); void copyBytesIntoNtcip1202(char* buff, int numBytes); @@ -100,16 +99,15 @@ class Ntcip1202 uint16_t getOverlapMinTime(int phaseNumber); uint16_t getOverlapMaxTime(int phaseNumber); - long getAdjustedTime(unsigned int offset); + long getAdjustedTime(unsigned int offset_tenthofSec, unsigned long msEpoch); bool isFlashingStatus(); bool isPhaseFlashing(); - bool ToJ2735r41SPAT(SPAT* spat, const std::string &intersectionName, IntersectionID_t intersectionId); + bool ToJ2735r41SPAT(SPAT* spat, unsigned long msEpoch , const std::string &intersectionName, IntersectionID_t intersectionId); void printDebug(); private: - std::shared_ptr clock; Ntcip1202Ext ntcip1202Data; std::map _phaseToIndexMapping; @@ -121,9 +119,9 @@ class Ntcip1202 int getVehicleSignalGroupForPhase(int phase); int getPedestrianSignalGroupForPhase(int phase); - void populateVehicleSignalGroup(MovementState *movement, int phase); - void populatePedestrianSignalGroup(MovementState *movement, int phase); - void populateOverlapSignalGroup(MovementState *movement, int phase); + void populateVehicleSignalGroup(MovementState *movement, int phase, unsigned long msEpoch); + void populatePedestrianSignalGroup(MovementState *movement, int phase, unsigned long msEpoch); + void populateOverlapSignalGroup(MovementState *movement, int phase, unsigned long msEpoch); }; diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index a0bf11794..cdf9d8198 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -6,31 +6,43 @@ namespace SpatPlugin { }; - tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(uint64_t timeMs , const SPAT_MODE &spatMode = SPAT_MODE::BINARY) { + tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(uint64_t timeMs , const SPAT_MODE &spatMode) { + if ( spatMode == SPAT_MODE::BINARY ) { char buf[1000]; - auto ntcip1202 = std::make_unique(clock); auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 3); + auto ntcip1202 = std::make_unique(); + if ( numBytes > 0 ) { // TODO: Revist this implementation. See if we can make SPAT a shared pointer // and skipe the SPAT to SpatMessage conversion. ntcip1202->copyBytesIntoNtcip1202(buf, numBytes); SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); - ntcip1202->ToJ2735r41SPAT(_spat, intersectionName, intersectionId); + ntcip1202->ToJ2735r41SPAT(_spat,timeMs, intersectionName, intersectionId); auto _spatMessage = std::make_unique(_spat); tmx::messages::MessageFrameMessage frame(_spatMessage->get_j2735_data()); tmx::messages::SpatEncodedMessage spatEncodedMsg; spatEncodedMsg.set_data(tmx::messages::TmxJ2735EncodedMessage::encode_j2735_message>(frame)); //Free the memory allocated for MessageFrame - free(frame.get_j2735_data().get()); - + free(_spat); + return spatEncodedMsg; }else { throw std::runtime_error("Something went wrong"); } } - else if ( spatMode == SPAT_MODE::J2735_HEX ) { + else { + tmx::messages::SpatEncodedMessage spatEncodedMsg; + tmx::byte_stream buf(4000); + int numBytes = spatPacketReceiver->TimedReceive((char *)buf.data(), buf.size(), 3); - } + if ( numBytes > 0 ) { + spatEncodedMsg.set_payload_bytes(buf); + return spatEncodedMsg; + } + else { + throw std::runtime_error("Something went wrong"); + } + } } } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 91c803734..9c80020a0 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -40,9 +40,23 @@ namespace SpatPlugin { else { scConnection = std::make_unique(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId); } + spatReceiverThread->AddPeriodicTick([this]() { + this->processSpat(); + + } // end of lambda expression + , std::chrono::milliseconds(5) ); + spatReceiverThread->Start(); } } + void SpatPlugin::processSpat() { + if (this->scConnection ) { + auto spatMessage = scConnection->receiveSPAT(PluginClientClockAware::getClock()->nowInMilliseconds()); + spatMessage.set_flags(IvpMsgFlags_RouteDSRC); + spatMessage.addDsrcMetadata(0x8002); + BroadcastMessage(static_cast(spatMessage)); + } + } void SpatPlugin::OnConfigChanged(const char *key, const char *value) { PluginClientClockAware::OnConfigChanged(key, value); UpdateConfigSettings(); diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index c2084d0a4..8e253d94a 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -41,10 +41,11 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { std::mutex data_lock; - std::unique_ptr scConnection; - + std::unique_ptr spatReceiverThread; + std::unique_ptr scConnection; + void processSpat(); }; } /* namespace SpatPlugin */ diff --git a/src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp index cc9c671a8..19e912ecc 100644 --- a/src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp @@ -19,7 +19,7 @@ TEST(NTCIP1202Test, copyBytesIntoNtcip1202) auto clock = std::make_shared(); clock->wait_for_initialization(); - auto ntcip1202_p = std::make_shared(clock); + auto ntcip1202_p = std::make_shared(); unsigned int raw_data[] = {4294967245, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 118, 0, 118, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4294967208, 0, 4294967208, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 118, 0, 118, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 4294967208, 0, 4294967208, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294967295, 4294967261, 0, 0, 0, 34, 4294967295, 4294967295, 0, 0, 0, 0, 4294967295, 4294967295, 0, 0, 0, 0, 0, 0, 0, 0, 4294967168, 0, 8, 103, 1, 10, 4294967237, 0, 0}; int numBytes = sizeof(raw_data)/sizeof(unsigned int); char buf[ numBytes] = {}; @@ -32,7 +32,7 @@ TEST(NTCIP1202Test, copyBytesIntoNtcip1202) ntcip1202_p->copyBytesIntoNtcip1202(buf, numBytes); SPAT *spat_ptr = (SPAT *)calloc(1, sizeof(SPAT)); - ntcip1202_p->ToJ2735r41SPAT(spat_ptr, reinterpret_cast(update_to_intersection_name->buf), update_to_intersection_id->id); + ntcip1202_p->ToJ2735r41SPAT(spat_ptr,clock->nowInMilliseconds(), "test intersection name", update_to_intersection_id->id); ASSERT_EQ(3, spat_ptr->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->eventState); free(spat_ptr); } @@ -41,7 +41,7 @@ TEST(NTCIP1202Test, ToJ2735r41SPAT) { auto clock = std::make_shared(); clock->wait_for_initialization(); - auto ntcip1202_p = std::make_shared(clock); + auto ntcip1202_p = std::make_shared(); SPAT *spat_ptr = (SPAT *)calloc(1, sizeof(SPAT)); char *my_string = (char*)"test intersection name"; @@ -56,7 +56,7 @@ TEST(NTCIP1202Test, ToJ2735r41SPAT) update_to_intersection_id->id = 9012; - bool transform_status = ntcip1202_p->ToJ2735r41SPAT(spat_ptr, reinterpret_cast(update_to_intersection_name->buf), update_to_intersection_id->id); + bool transform_status = ntcip1202_p->ToJ2735r41SPAT(spat_ptr, clock->nowInMilliseconds(), reinterpret_cast(update_to_intersection_name->buf), update_to_intersection_id->id); auto _spatMessage = std::make_shared(spat_ptr); auto spat = _spatMessage->get_j2735_data(); ASSERT_EQ(transform_status, true); @@ -69,15 +69,15 @@ TEST(NTCIP1202Test, TestAdjustedTime) timeStampMilliseconds tsMsec = ((uint64_t)1677775434 * 1000) + 400; auto baseTenthsOfSeconds = 43 * 600 + 54 * 10 + 4; clock->update(tsMsec); - auto ntcip1202_p = std::make_shared(clock); - auto result = ntcip1202_p->getAdjustedTime(0); + auto ntcip1202_p = std::make_shared(); + auto result = ntcip1202_p->getAdjustedTime(0, clock->nowInMilliseconds()); EXPECT_EQ(baseTenthsOfSeconds, result); - result = ntcip1202_p->getAdjustedTime(46); + result = ntcip1202_p->getAdjustedTime(46, clock->nowInMilliseconds()); EXPECT_EQ(baseTenthsOfSeconds + 46, result); // cross minute boundary - result = ntcip1202_p->getAdjustedTime(200); + result = ntcip1202_p->getAdjustedTime(200, clock->nowInMilliseconds()); EXPECT_EQ(baseTenthsOfSeconds + 200, result); // cross hour boundary - result = ntcip1202_p->getAdjustedTime(10200); + result = ntcip1202_p->getAdjustedTime(10200, clock->nowInMilliseconds()); EXPECT_EQ((baseTenthsOfSeconds + 10200) % 36000, result); } \ No newline at end of file From 54dfc3a4ed01e715dcc819a3a44388a16bee406b Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 3 Jul 2024 17:57:07 -0400 Subject: [PATCH 07/31] Updates --- src/tmx/TmxUtils/src/SNMPClient.cpp | 51 ++++++++++++------ src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 13 +---- src/v2i-hub/SpatPlugin/src/NTCIP1202.h | 3 +- .../src/SignalControllerConnection.cpp | 25 ++++++--- .../src/SignalControllerConnection.h | 4 +- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 23 ++++++-- src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 3 ++ .../{test_NTCIP1202.cpp => TestNTCIP1202.cpp} | 53 ++++++------------- .../test/TestSignalControllerConnection.cpp | 0 9 files changed, 93 insertions(+), 82 deletions(-) rename src/v2i-hub/SpatPlugin/test/{test_NTCIP1202.cpp => TestNTCIP1202.cpp} (58%) create mode 100644 src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp diff --git a/src/tmx/TmxUtils/src/SNMPClient.cpp b/src/tmx/TmxUtils/src/SNMPClient.cpp index e3398ebbd..5c8a5ff52 100644 --- a/src/tmx/TmxUtils/src/SNMPClient.cpp +++ b/src/tmx/TmxUtils/src/SNMPClient.cpp @@ -165,28 +165,47 @@ namespace tmx::utils PLOG(logINFO) << "Response request status: " << status << " (=" << (status == STAT_SUCCESS ? "SUCCESS" : "FAILED") << ")"; // Check GET response - if (status == STAT_SUCCESS && response && response->errstat == SNMP_ERR_NOERROR && request_type == request_type::GET) + if (status == STAT_SUCCESS && response && response->errstat == SNMP_ERR_NOERROR ) { - for (auto vars = response->variables; vars; vars = vars->next_variable) - { - // Get value of variable depending on ASN.1 type - // Variable could be a integer, string, bitstring, ojbid, counter : defined here https://github.com/net-snmp/net-snmp/blob/master/include/net-snmp/types.h - // get Integer value - if (vars->type == ASN_INTEGER && vars->val.integer) - { - val.type = snmp_response_obj::response_type::INTEGER; - val.val_int = *vars->val.integer; - } - else if (vars->type == ASN_OCTET_STR && vars->val.string) + if ( request_type == request_type::GET ) { + for (auto vars = response->variables; vars; vars = vars->next_variable) { - size_t str_len = vars->val_len; - for (size_t i = 0; i < str_len; ++i) + // Get value of variable depending on ASN.1 type + // Variable could be a integer, string, bitstring, ojbid, counter : defined here https://github.com/net-snmp/net-snmp/blob/master/include/net-snmp/types.h + // get Integer value + if (vars->type == ASN_INTEGER && vars->val.integer) { - val.val_string.push_back(vars->val.string[i]); + val.type = snmp_response_obj::response_type::INTEGER; + val.val_int = *vars->val.integer; + } + else if (vars->type == ASN_OCTET_STR && vars->val.string) + { + size_t str_len = vars->val_len; + for (size_t i = 0; i < str_len; ++i) + { + val.val_string.push_back(vars->val.string[i]); + } + val.type = snmp_response_obj::response_type::STRING; } - val.type = snmp_response_obj::response_type::STRING; } } + else if( request_type == request_type::SET){ + + if(val.type == snmp_response_obj::response_type::INTEGER){ + FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value: " << val.val_int << std::endl; + } + + else if(val.type == snmp_response_obj::response_type::STRING){ + FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value:" << std::endl; + for(auto data : val.val_string){ + FILE_LOG(logDEBUG) << data ; + } + } + } + else { + log_error(status, request_type, response); + return false; + } } else { diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index 9af2da18c..c82c2da93 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -57,7 +57,6 @@ void Ntcip1202::setSignalGroupMappingList(string json) void Ntcip1202::copyBytesIntoNtcip1202(char* buff, int numBytes) { - std::lock_guard lock(_spat_lock); std::memcpy(&ntcip1202Data, buff, numBytes); @@ -199,7 +198,7 @@ void Ntcip1202::printDebug() } } -bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, unsigned long msEpoch , const std::string &intersectionName, IntersectionID_t intersectionId) +void Ntcip1202::ToJ2735SPAT(SPAT* spat, unsigned long msEpoch , const std::string &intersectionName, IntersectionID_t intersectionId) { time_t epochSec = msEpoch/1000; struct tm utctime; @@ -213,8 +212,6 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, unsigned long msEpoch , const std::st auto epochMs = msEpoch; long msOfMin = 1000 * (epochSec % 60) + (epochMs % 1000); - std::lock_guard lock(_spat_lock); - ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_SPAT, spat); #if SAEJ2735_SPEC < 63 @@ -303,9 +300,6 @@ bool Ntcip1202::ToJ2735r41SPAT(SPAT* spat, unsigned long msEpoch , const std::st } ASN_SEQUENCE_ADD(&(spat->intersections.list), intersection); - - - return true; } void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase, unsigned long msEpoch) @@ -317,10 +311,7 @@ void Ntcip1202::populateVehicleSignalGroup(MovementState *movement, int phase, u if(getPhaseRedStatus(phase)) { - PLOG(logDEBUG3) << "Phase " << phase << - " Red " << getPhaseRedStatus(phase) << - ", isFlashing " << isFlashing << - ", forceFlashing " << forceFlashing ; + PLOG(logDEBUG3) << "Phase " << phase << " Red " << getPhaseRedStatus(phase) << ", isFlashing " << isFlashing << ", forceFlashing " << forceFlashing ; if(isFlashing) stateTimeSpeed->eventState = MovementPhaseState_stop_Then_Proceed; else diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h index 7c23c1674..90b67cc88 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h @@ -104,7 +104,7 @@ class Ntcip1202 bool isFlashingStatus(); bool isPhaseFlashing(); - bool ToJ2735r41SPAT(SPAT* spat, unsigned long msEpoch , const std::string &intersectionName, IntersectionID_t intersectionId); + void ToJ2735SPAT(SPAT* spat, unsigned long msEpoch , const std::string &intersectionName, IntersectionID_t intersectionId); void printDebug(); private: @@ -112,7 +112,6 @@ class Ntcip1202 Ntcip1202Ext ntcip1202Data; std::map _phaseToIndexMapping; - std::mutex _spat_lock; list signalGroupMappingList; diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index cdf9d8198..931e65f1d 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -2,15 +2,20 @@ namespace SpatPlugin { - SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_unique(localIp, localPort)) ,scSNMPClient(std::make_unique(scIp, scSNMPPort ,"", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { + SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_unique(localIp, localPort)) ,scSNMPClient(std::make_unique(scIp, scSNMPPort ,"administrator", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; - - tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(uint64_t timeMs , const SPAT_MODE &spatMode) { - + bool SignalControllerConnection::initializeSignalControllerConnection() { + tmx::utils::snmp_response_obj resp; + resp.val_int = 2; + return scSNMPClient->process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, resp); + }; + tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(uint64_t timeMs, const SPAT_MODE &spatMode) + { if ( spatMode == SPAT_MODE::BINARY ) { + FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT..." << std::endl; char buf[1000]; - auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 3); + auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); auto ntcip1202 = std::make_unique(); if ( numBytes > 0 ) { @@ -18,7 +23,10 @@ namespace SpatPlugin { // and skipe the SPAT to SpatMessage conversion. ntcip1202->copyBytesIntoNtcip1202(buf, numBytes); SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); - ntcip1202->ToJ2735r41SPAT(_spat,timeMs, intersectionName, intersectionId); + if ( tmx::utils::FILELog::ReportingLevel() == tmx::utils::logDEBUG) { + xer_fprint(stdout, &asn_DEF_SPAT, _spat); + } + ntcip1202->ToJ2735SPAT(_spat,timeMs, intersectionName, intersectionId); auto _spatMessage = std::make_unique(_spat); tmx::messages::MessageFrameMessage frame(_spatMessage->get_j2735_data()); tmx::messages::SpatEncodedMessage spatEncodedMsg; @@ -31,6 +39,8 @@ namespace SpatPlugin { } } else { + FILE_LOG(tmx::utils::logDEBUG) << "Receiving J2725 HEX SPAT ..." << std::endl; + tmx::messages::SpatEncodedMessage spatEncodedMsg; tmx::byte_stream buf(4000); int numBytes = spatPacketReceiver->TimedReceive((char *)buf.data(), buf.size(), 3); @@ -42,7 +52,6 @@ namespace SpatPlugin { else { throw std::runtime_error("Something went wrong"); } - } + } } - } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 96c9ce6a4..c907a289b 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -4,6 +4,7 @@ #include #include #include "NTCIP1202.h" +#include namespace SpatPlugin { enum class SPAT_MODE @@ -23,11 +24,10 @@ namespace SpatPlugin { std::string signalGroupMapping; std::string intersectionName; unsigned int intersectionId; - void initializeSignalControllerConnection(); public: SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); - + bool initializeSignalControllerConnection(); tmx::messages::SpatEncodedMessage receiveSPAT(uint64_t timeMs , const SPAT_MODE &spat_mode = SPAT_MODE::BINARY); }; diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 9c80020a0..f79651b0a 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -8,6 +8,8 @@ using namespace tmx::utils; namespace SpatPlugin { SpatPlugin::SpatPlugin(string name) :PluginClientClockAware(name) { + spatReceiverThread = std::make_unique(std::chrono::milliseconds(5)); + if ( PluginClientClockAware::isSimulationMode() ) { SubscribeToMessages(); } @@ -40,21 +42,32 @@ namespace SpatPlugin { else { scConnection = std::make_unique(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId); } - spatReceiverThread->AddPeriodicTick([this]() { - this->processSpat(); + auto connected = scConnection->initializeSignalControllerConnection(); + if ( connected ) { + SetStatus(keyConnectionStatus, "IDLE"); + + spatReceiverThread->AddPeriodicTick([this]() + { + this->processSpat(); + }, // end of lambda expression + std::chrono::milliseconds(5)); + spatReceiverThread->Start(); + } + else { - } // end of lambda expression - , std::chrono::milliseconds(5) ); - spatReceiverThread->Start(); + } } } void SpatPlugin::processSpat() { if (this->scConnection ) { + PLOG(tmx::utils::logDEBUG) << "Processing SPAT ... " << std::endl; auto spatMessage = scConnection->receiveSPAT(PluginClientClockAware::getClock()->nowInMilliseconds()); spatMessage.set_flags(IvpMsgFlags_RouteDSRC); spatMessage.addDsrcMetadata(0x8002); BroadcastMessage(static_cast(spatMessage)); + SetStatus(keyConnectionStatus, "CONNECTED"); + } } void SpatPlugin::OnConfigChanged(const char *key, const char *value) { diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index 8e253d94a..168b3f8fd 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -45,6 +45,9 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { std::unique_ptr scConnection; + const char* keyConnectionStatus = "Connection Status"; + + void processSpat(); }; } /* namespace SpatPlugin */ diff --git a/src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp similarity index 58% rename from src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp rename to src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp index 19e912ecc..7b7f64a49 100644 --- a/src/v2i-hub/SpatPlugin/test/test_NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp @@ -7,18 +7,8 @@ using namespace fwha_stol::lib::time; TEST(NTCIP1202Test, copyBytesIntoNtcip1202) { - DescriptiveName_t *update_to_intersection_name = (DescriptiveName_t *)calloc(1, sizeof(DescriptiveName_t)); - char *my_string = (char*) "test intersection name"; - stringstream ss; - update_to_intersection_name->buf = reinterpret_cast(my_string); - ss << update_to_intersection_name->buf; - ASSERT_EQ(ss.str(), "test intersection name"); + uint64_t tsMsec = 1677775434400; - IntersectionReferenceID_t *update_to_intersection_id = (IntersectionReferenceID_t *)calloc(1, sizeof(IntersectionReferenceID_t)); - update_to_intersection_id->id = 9012; - - auto clock = std::make_shared(); - clock->wait_for_initialization(); auto ntcip1202_p = std::make_shared(); unsigned int raw_data[] = {4294967245, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 118, 0, 118, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4294967208, 0, 4294967208, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 118, 0, 118, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 4294967208, 0, 4294967208, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294967295, 4294967261, 0, 0, 0, 34, 4294967295, 4294967295, 0, 0, 0, 0, 4294967295, 4294967295, 0, 0, 0, 0, 0, 0, 0, 0, 4294967168, 0, 8, 103, 1, 10, 4294967237, 0, 0}; int numBytes = sizeof(raw_data)/sizeof(unsigned int); @@ -32,52 +22,39 @@ TEST(NTCIP1202Test, copyBytesIntoNtcip1202) ntcip1202_p->copyBytesIntoNtcip1202(buf, numBytes); SPAT *spat_ptr = (SPAT *)calloc(1, sizeof(SPAT)); - ntcip1202_p->ToJ2735r41SPAT(spat_ptr,clock->nowInMilliseconds(), "test intersection name", update_to_intersection_id->id); + ntcip1202_p->ToJ2735SPAT(spat_ptr,tsMsec, "test intersection name", 9012); + xer_fprint(stdout, &asn_DEF_SPAT, spat_ptr); + ASSERT_EQ(3, spat_ptr->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->eventState); + free(spat_ptr); } -TEST(NTCIP1202Test, ToJ2735r41SPAT) +TEST(NTCIP1202Test, ToJ2735SPAT) { - auto clock = std::make_shared(); - clock->wait_for_initialization(); + uint64_t tsMsec = 1677775434400; + auto ntcip1202_p = std::make_shared(); SPAT *spat_ptr = (SPAT *)calloc(1, sizeof(SPAT)); - - char *my_string = (char*)"test intersection name"; - stringstream ss; - - DescriptiveName_t *update_to_intersection_name = (DescriptiveName_t *)calloc(1, sizeof(DescriptiveName_t)); - update_to_intersection_name->buf = reinterpret_cast(my_string); - ss << update_to_intersection_name->buf; - ASSERT_EQ(ss.str(), "test intersection name"); - - IntersectionReferenceID_t *update_to_intersection_id = (IntersectionReferenceID_t *)calloc(1, sizeof(IntersectionReferenceID_t)); - update_to_intersection_id->id = 9012; - - - bool transform_status = ntcip1202_p->ToJ2735r41SPAT(spat_ptr, clock->nowInMilliseconds(), reinterpret_cast(update_to_intersection_name->buf), update_to_intersection_id->id); + ntcip1202_p->ToJ2735SPAT(spat_ptr, tsMsec, "test intersection name", 9012); auto _spatMessage = std::make_shared(spat_ptr); auto spat = _spatMessage->get_j2735_data(); - ASSERT_EQ(transform_status, true); } TEST(NTCIP1202Test, TestAdjustedTime) { - auto clock = std::make_shared(true); - // 1677775434 = 2023-02-03 16:43:54 - timeStampMilliseconds tsMsec = ((uint64_t)1677775434 * 1000) + 400; + // 1677775434400 = 2023-02-03 16:43:54.400 + uint64_t tsMsec = 1677775434400; auto baseTenthsOfSeconds = 43 * 600 + 54 * 10 + 4; - clock->update(tsMsec); auto ntcip1202_p = std::make_shared(); - auto result = ntcip1202_p->getAdjustedTime(0, clock->nowInMilliseconds()); + auto result = ntcip1202_p->getAdjustedTime(0, tsMsec); EXPECT_EQ(baseTenthsOfSeconds, result); - result = ntcip1202_p->getAdjustedTime(46, clock->nowInMilliseconds()); + result = ntcip1202_p->getAdjustedTime(46, tsMsec); EXPECT_EQ(baseTenthsOfSeconds + 46, result); // cross minute boundary - result = ntcip1202_p->getAdjustedTime(200, clock->nowInMilliseconds()); + result = ntcip1202_p->getAdjustedTime(200, tsMsec); EXPECT_EQ(baseTenthsOfSeconds + 200, result); // cross hour boundary - result = ntcip1202_p->getAdjustedTime(10200, clock->nowInMilliseconds()); + result = ntcip1202_p->getAdjustedTime(10200, tsMsec); EXPECT_EQ((baseTenthsOfSeconds + 10200) % 36000, result); } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp new file mode 100644 index 000000000..e69de29bb From 47256d8b25ae7c8df0af93a7659c91cac16d5e2e Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 8 Jul 2024 17:37:33 -0400 Subject: [PATCH 08/31] Updates --- .../TmxUtils/src/PluginClientClockAware.cpp | 4 +-- src/tmx/TmxUtils/test/J2735MessageTest.cpp | 2 +- src/v2i-hub/SpatPlugin/manifest.json | 5 +++ src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 2 +- .../src/SignalControllerConnection.cpp | 35 ++++++++++++------- .../src/SignalControllerConnection.h | 2 +- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 31 ++++++++++++---- src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 2 ++ 8 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/tmx/TmxUtils/src/PluginClientClockAware.cpp b/src/tmx/TmxUtils/src/PluginClientClockAware.cpp index 4cf87fe6b..a3dae0c2f 100644 --- a/src/tmx/TmxUtils/src/PluginClientClockAware.cpp +++ b/src/tmx/TmxUtils/src/PluginClientClockAware.cpp @@ -21,9 +21,9 @@ namespace tmx::utils { void PluginClientClockAware::HandleTimeSyncMessage(tmx::messages::TimeSyncMessage &msg, routeable_message &routeableMsg ) { - PLOG(logDEBUG) << "Message Received " << msg.to_string() << std::endl; - this->getClock()->update( msg.get_timestep() ); if (sim::is_simulation_mode() ) { + PLOG(logDEBUG) << "Message Received " << msg.to_string() << std::endl; + this->getClock()->update( msg.get_timestep() ); SetStatus(Key_Simulation_Time_Step, Clock::ToUtcPreciseTimeString(msg.get_timestep())); } } diff --git a/src/tmx/TmxUtils/test/J2735MessageTest.cpp b/src/tmx/TmxUtils/test/J2735MessageTest.cpp index 1bc605656..609cf456d 100644 --- a/src/tmx/TmxUtils/test/J2735MessageTest.cpp +++ b/src/tmx/TmxUtils/test/J2735MessageTest.cpp @@ -814,7 +814,7 @@ TEST_F(J2735MessageTest, EncodeSDSM) tmx::messages::SdsmEncodedMessage SdsmEncodeMessage; auto _sdsmMessage = new tmx::messages::SdsmMessage(message); tmx::messages::MessageFrameMessage frame_msg(_sdsmMessage->get_j2735_data()); - SdsmEncodeMessage.set_data(TmxJ2735EncodedMessage::encode_j2735_message>(frame_msg)); + SdsmEncodeMessage.set_data(TmxJ2735EncodedMessage::encode_j2735_message>(frame_msg)); free(message); free(frame_msg.get_j2735_data().get()); ASSERT_EQ(41, SdsmEncodeMessage.get_msgId()); diff --git a/src/v2i-hub/SpatPlugin/manifest.json b/src/v2i-hub/SpatPlugin/manifest.json index 2d8b957f4..58dccbb51 100644 --- a/src/v2i-hub/SpatPlugin/manifest.json +++ b/src/v2i-hub/SpatPlugin/manifest.json @@ -53,6 +53,11 @@ "default":"", "description":"The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP communication." }, + { + "key":"SPAT_Mode", + "default":"BINARY", + "description":"The format of received SPAT from Traffic Signal Controller (TSC). Acceptance values are BINARY and J2735_HEX." + }, { "key":"LogLevel", "default":"INFO", diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index c82c2da93..5b31a6bb5 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -188,7 +188,7 @@ void Ntcip1202::printDebug() for(int i=0; i<16; i++) { int phaseNum = i+1; - PLOG(logDEBUG3) << "Phase " << phaseNum << + PLOG(logDEBUG) << "Phase " << phaseNum << ", Green " << getPhaseGreensStatus(phaseNum) << ", Yellow " << getPhaseYellowStatus(phaseNum) << ", Red " << getPhaseRedStatus(phaseNum) << diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index 931e65f1d..f8d9fd357 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -10,31 +10,38 @@ namespace SpatPlugin { resp.val_int = 2; return scSNMPClient->process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, resp); }; - tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(uint64_t timeMs, const SPAT_MODE &spatMode) + tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(SPAT *spat, uint64_t timeMs, const SPAT_MODE &spatMode) { if ( spatMode == SPAT_MODE::BINARY ) { - FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT..." << std::endl; + FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; char buf[1000]; auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); auto ntcip1202 = std::make_unique(); - + ntcip1202->setSignalGroupMappingList(this->signalGroupMapping); + if ( numBytes > 0 ) { // TODO: Revist this implementation. See if we can make SPAT a shared pointer // and skipe the SPAT to SpatMessage conversion. + FILE_LOG(tmx::utils::logDEBUG) << "Decoding binary SPAT from " << numBytes << " bytes ..." << std::endl; ntcip1202->copyBytesIntoNtcip1202(buf, numBytes); - SPAT *_spat = (SPAT *) calloc(1, sizeof(SPAT)); - if ( tmx::utils::FILELog::ReportingLevel() == tmx::utils::logDEBUG) { - xer_fprint(stdout, &asn_DEF_SPAT, _spat); + FILE_LOG(tmx::utils::logDEBUG) << "Read SPAT Bytes ..." << std::endl; + + + ntcip1202->ToJ2735SPAT(spat,timeMs, intersectionName, intersectionId); + FILE_LOG(tmx::utils::logDEBUG) << "Copied into SPAT object ..." << std::endl; + if ( tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) { + xer_fprint(stdout, &asn_DEF_SPAT, spat); } - ntcip1202->ToJ2735SPAT(_spat,timeMs, intersectionName, intersectionId); - auto _spatMessage = std::make_unique(_spat); - tmx::messages::MessageFrameMessage frame(_spatMessage->get_j2735_data()); + tmx::messages::SpatMessage _spatMessage(spat); tmx::messages::SpatEncodedMessage spatEncodedMsg; - spatEncodedMsg.set_data(tmx::messages::TmxJ2735EncodedMessage::encode_j2735_message>(frame)); - //Free the memory allocated for MessageFrame - free(_spat); + spatEncodedMsg.initialize(_spatMessage); + // tmx::messages::MessageFrameMessage frame(_spatMessage.get_j2735_data()); + // FILE_LOG(tmx::utils::logDEBUG) << "Copied into SPAT Message Frame..." << std::endl; + // spatEncodedMsg.set_data(tmx::messages::TmxJ2735EncodedMessage::encode_j2735_message>(frame)); + FILE_LOG(tmx::utils::logDEBUG) << "Copied into encoded SPAT Message" << std::endl; return spatEncodedMsg; - }else { + } + else { throw std::runtime_error("Something went wrong"); } } @@ -47,6 +54,8 @@ namespace SpatPlugin { if ( numBytes > 0 ) { spatEncodedMsg.set_payload_bytes(buf); + xer_fprint(stdout, &asn_DEF_SPAT, spatEncodedMsg.decode_j2735_message().get_j2735_data().get()); + return spatEncodedMsg; } else { diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index c907a289b..32f189d3d 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -28,7 +28,7 @@ namespace SpatPlugin { public: SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); bool initializeSignalControllerConnection(); - tmx::messages::SpatEncodedMessage receiveSPAT(uint64_t timeMs , const SPAT_MODE &spat_mode = SPAT_MODE::BINARY); + tmx::messages::SpatEncodedMessage receiveSPAT(SPAT *spat, uint64_t timeMs , const SPAT_MODE &spat_mode = SPAT_MODE::BINARY); }; } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index f79651b0a..20f271dfb 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -29,13 +29,15 @@ namespace SpatPlugin { unsigned int scSNMPPort; std::string intersectionName; unsigned int intersectionId; - GetConfigValue("SignalGroupMapping", signalGroupMappingJson, &data_lock); + GetConfigValue("SignalGroupMapping", signalGroupMappingJson, &data_lock); GetConfigValue("Local_IP", ip_address, &data_lock); GetConfigValue("Local_UDP_Port", port, &data_lock); - GetConfigValue("TSC_IP", scIp, &data_lock); + GetConfigValue("TSC_IP", scIp, &data_lock); GetConfigValue("TSC_Remote_SNMP_Port", scSNMPPort,&data_lock); - GetConfigValue("Intersection_Name", intersectionName,&data_lock); + GetConfigValue("Intersection_Name", intersectionName,&data_lock); GetConfigValue("Intersection_Id", intersectionId, &data_lock); + GetConfigValue("SPAT_Mode", spatMode, &data_lock); + if (scConnection) { scConnection.reset(new SignalControllerConnection(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId)); } @@ -51,6 +53,7 @@ namespace SpatPlugin { this->processSpat(); }, // end of lambda expression std::chrono::milliseconds(5)); + PluginClientClockAware::getClock()->wait_for_initialization(); spatReceiverThread->Start(); } else { @@ -62,12 +65,28 @@ namespace SpatPlugin { void SpatPlugin::processSpat() { if (this->scConnection ) { PLOG(tmx::utils::logDEBUG) << "Processing SPAT ... " << std::endl; - auto spatMessage = scConnection->receiveSPAT(PluginClientClockAware::getClock()->nowInMilliseconds()); + SPAT_MODE mode; + if (spatMode == "BINARY") + { + mode = SPAT_MODE::BINARY; + } + else if (spatMode == "J2735_HEX") { + mode = SPAT_MODE::J2735_HEX; + } + else { + PLOG(tmx::utils::logWARNING) << "SPAT Mode " << spatMode << " is unrecognized. Defaulting to BINARY." << std::endl; + } + // auto spat_ptr = std::make_shared(); + SPAT *spat_ptr = (SPAT *) calloc(1, sizeof(SPAT)); + auto spatMessage = scConnection->receiveSPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds(), mode); + spatMessage.set_flags(IvpMsgFlags_RouteDSRC); - spatMessage.addDsrcMetadata(0x8002); + spatMessage.addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); + BroadcastMessage(static_cast(spatMessage)); - SetStatus(keyConnectionStatus, "CONNECTED"); + PLOG(tmx::utils::logDEBUG) << "Broadcasting SPAT" << std::endl; + SetStatus(keyConnectionStatus, "CONNECTED"); } } void SpatPlugin::OnConfigChanged(const char *key, const char *value) { diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index 168b3f8fd..802fa331f 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -45,6 +45,8 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { std::unique_ptr scConnection; + std::string spatMode = ""; + const char* keyConnectionStatus = "Connection Status"; From 1173dbbffdcc8bcbc7a65045e364022307aef5ae Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 9 Jul 2024 19:07:27 -0400 Subject: [PATCH 09/31] Updates --- src/v2i-hub/SpatPlugin/CMakeLists.txt | 11 ++++--- .../src/SignalControllerConnection.cpp | 33 ++++++++++++------- .../src/SignalControllerConnection.h | 2 ++ src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 1 - 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/CMakeLists.txt b/src/v2i-hub/SpatPlugin/CMakeLists.txt index 55e1c5192..d75cd037c 100644 --- a/src/v2i-hub/SpatPlugin/CMakeLists.txt +++ b/src/v2i-hub/SpatPlugin/CMakeLists.txt @@ -1,12 +1,13 @@ -PROJECT ( SpatPlugin VERSION 7.6.0 LANGUAGES CXX ) +project( SpatPlugin VERSION 7.6.0 LANGUAGES CXX ) -SET (TMX_PLUGIN_NAME "SPAT") +set(TMX_PLUGIN_NAME "SPAT") +set(CMAKE_CXX_STANDARD 17) -FIND_PACKAGE (carma-clock REQUIRED) +find_package(carma-clock REQUIRED) -BuildTmxPlugin () +BuildTmxPlugin() -TARGET_LINK_LIBRARIES ( ${PROJECT_NAME} tmxutils ::carma-clock jsoncpp) +target_link_libraries(${PROJECT_NAME} tmxutils ::carma-clock jsoncpp) diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index f8d9fd357..fb8cb771c 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -24,21 +24,16 @@ namespace SpatPlugin { // and skipe the SPAT to SpatMessage conversion. FILE_LOG(tmx::utils::logDEBUG) << "Decoding binary SPAT from " << numBytes << " bytes ..." << std::endl; ntcip1202->copyBytesIntoNtcip1202(buf, numBytes); - FILE_LOG(tmx::utils::logDEBUG) << "Read SPAT Bytes ..." << std::endl; ntcip1202->ToJ2735SPAT(spat,timeMs, intersectionName, intersectionId); - FILE_LOG(tmx::utils::logDEBUG) << "Copied into SPAT object ..." << std::endl; + FILE_LOG(tmx::utils::logDEBUG) << "Sending SPAT ..." << std::endl; if ( tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) { xer_fprint(stdout, &asn_DEF_SPAT, spat); } tmx::messages::SpatMessage _spatMessage(spat); tmx::messages::SpatEncodedMessage spatEncodedMsg; spatEncodedMsg.initialize(_spatMessage); - // tmx::messages::MessageFrameMessage frame(_spatMessage.get_j2735_data()); - // FILE_LOG(tmx::utils::logDEBUG) << "Copied into SPAT Message Frame..." << std::endl; - // spatEncodedMsg.set_data(tmx::messages::TmxJ2735EncodedMessage::encode_j2735_message>(frame)); - FILE_LOG(tmx::utils::logDEBUG) << "Copied into encoded SPAT Message" << std::endl; return spatEncodedMsg; } else { @@ -49,14 +44,28 @@ namespace SpatPlugin { FILE_LOG(tmx::utils::logDEBUG) << "Receiving J2725 HEX SPAT ..." << std::endl; tmx::messages::SpatEncodedMessage spatEncodedMsg; - tmx::byte_stream buf(4000); - int numBytes = spatPacketReceiver->TimedReceive((char *)buf.data(), buf.size(), 3); + auto payload = spatPacketReceiver->stringTimedReceive( 1000 ); + auto index = payload.find("Payload="); + FILE_LOG(tmx::utils::logDEBUG) << "Found Payload at index " << index << std::endl; - if ( numBytes > 0 ) { - spatEncodedMsg.set_payload_bytes(buf); - xer_fprint(stdout, &asn_DEF_SPAT, spatEncodedMsg.decode_j2735_message().get_j2735_data().get()); + if ( index != std::string::npos ) { + auto hex = payload.substr(index + 8); + hex.erase(std::remove(hex.begin(), hex.end(), '\n'), hex.end()); + hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); - return spatEncodedMsg; + FILE_LOG(tmx::utils::logDEBUG) << "Reading HEX String " << hex << std::endl; + tmx::byte_stream bytes = tmx::byte_stream_decode(hex); + + FILE_LOG(tmx::utils::logDEBUG) << "Reading Bytes " << tmx::byte_stream_encode(bytes) ; + tmx::messages::J2735MessageFactory myFactory; + auto spatEncodedMsg = dynamic_cast(myFactory.NewMessage(bytes)); + if (tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) + { + xer_fprint(stdout, &asn_DEF_SPAT, spatEncodedMsg->decode_j2735_message().get_j2735_data().get()); + PLOG(tmx::utils::logDEBUG) << "Message is "<< spatEncodedMsg->get_payload_str(); + } + + return *spatEncodedMsg; } else { throw std::runtime_error("Something went wrong"); diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 32f189d3d..037cdcda1 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -5,6 +5,8 @@ #include #include "NTCIP1202.h" #include +#include +#include namespace SpatPlugin { enum class SPAT_MODE diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 20f271dfb..052879e03 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -76,7 +76,6 @@ namespace SpatPlugin { else { PLOG(tmx::utils::logWARNING) << "SPAT Mode " << spatMode << " is unrecognized. Defaulting to BINARY." << std::endl; } - // auto spat_ptr = std::make_shared(); SPAT *spat_ptr = (SPAT *) calloc(1, sizeof(SPAT)); auto spatMessage = scConnection->receiveSPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds(), mode); From f49a8e17f7581d71dcb1c777cc7d2073de07ad98 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 11 Jul 2024 15:00:40 -0400 Subject: [PATCH 10/31] Updates --- .../src/SignalControllerConnection.cpp | 96 ++++++++----------- .../src/SignalControllerConnection.h | 4 + src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 78 +++++++++------ src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 3 + 4 files changed, 98 insertions(+), 83 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index fb8cb771c..e7854ad29 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -8,68 +8,52 @@ namespace SpatPlugin { bool SignalControllerConnection::initializeSignalControllerConnection() { tmx::utils::snmp_response_obj resp; resp.val_int = 2; + resp.type = tmx::utils::snmp_response_obj::response_type::INTEGER; return scSNMPClient->process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, resp); }; - tmx::messages::SpatEncodedMessage SignalControllerConnection::receiveSPAT(SPAT *spat, uint64_t timeMs, const SPAT_MODE &spatMode) - { - if ( spatMode == SPAT_MODE::BINARY ) { - FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; - char buf[1000]; - auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); - auto ntcip1202 = std::make_unique(); - ntcip1202->setSignalGroupMappingList(this->signalGroupMapping); - if ( numBytes > 0 ) { - // TODO: Revist this implementation. See if we can make SPAT a shared pointer - // and skipe the SPAT to SpatMessage conversion. - FILE_LOG(tmx::utils::logDEBUG) << "Decoding binary SPAT from " << numBytes << " bytes ..." << std::endl; - ntcip1202->copyBytesIntoNtcip1202(buf, numBytes); - - - ntcip1202->ToJ2735SPAT(spat,timeMs, intersectionName, intersectionId); - FILE_LOG(tmx::utils::logDEBUG) << "Sending SPAT ..." << std::endl; - if ( tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) { - xer_fprint(stdout, &asn_DEF_SPAT, spat); - } - tmx::messages::SpatMessage _spatMessage(spat); - tmx::messages::SpatEncodedMessage spatEncodedMsg; - spatEncodedMsg.initialize(_spatMessage); - return spatEncodedMsg; - } - else { - throw std::runtime_error("Something went wrong"); - } + void SignalControllerConnection::receiveBinarySPAT(std::shared_ptr spat, uint64_t timeMs ) { + FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; + char buf[1000]; + auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); + + if ( numBytes > 0 ) { + // Convert Binary buffer to SPAT pointer + Ntcip1202 ntcip1202; + ntcip1202.setSignalGroupMappingList(this->signalGroupMapping); + ntcip1202.copyBytesIntoNtcip1202(buf, numBytes); + ntcip1202.ToJ2735SPAT(spat.get(),timeMs, intersectionName, intersectionId); + } else { - FILE_LOG(tmx::utils::logDEBUG) << "Receiving J2725 HEX SPAT ..." << std::endl; - - tmx::messages::SpatEncodedMessage spatEncodedMsg; - auto payload = spatPacketReceiver->stringTimedReceive( 1000 ); - auto index = payload.find("Payload="); - FILE_LOG(tmx::utils::logDEBUG) << "Found Payload at index " << index << std::endl; - - if ( index != std::string::npos ) { - auto hex = payload.substr(index + 8); - hex.erase(std::remove(hex.begin(), hex.end(), '\n'), hex.end()); - hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); - - FILE_LOG(tmx::utils::logDEBUG) << "Reading HEX String " << hex << std::endl; - tmx::byte_stream bytes = tmx::byte_stream_decode(hex); - - FILE_LOG(tmx::utils::logDEBUG) << "Reading Bytes " << tmx::byte_stream_encode(bytes) ; - tmx::messages::J2735MessageFactory myFactory; - auto spatEncodedMsg = dynamic_cast(myFactory.NewMessage(bytes)); - if (tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) - { - xer_fprint(stdout, &asn_DEF_SPAT, spatEncodedMsg->decode_j2735_message().get_j2735_data().get()); - PLOG(tmx::utils::logDEBUG) << "Message is "<< spatEncodedMsg->get_payload_str(); - } - - return *spatEncodedMsg; - } - else { - throw std::runtime_error("Something went wrong"); + throw tmx::utils::UdpServerRuntimeError("UDP Server error occured or socket time out."); + } + } + + void SignalControllerConnection::receiveUPERSPAT(std::shared_ptr spatEncoded_ptr) { + FILE_LOG(tmx::utils::logDEBUG) << "Receiving J2725 HEX SPAT ..." << std::endl; + auto payload = spatPacketReceiver->stringTimedReceive( 1000 ); + auto index = payload.find("Payload="); + if ( index != std::string::npos ) { + // Retreive hex string payload + auto hex = payload.substr(index + 8); + // Remove new lines and empty space + hex.erase(std::remove(hex.begin(), hex.end(), '\n'), hex.end()); + hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); + FILE_LOG(tmx::utils::logDEBUG) << "Reading HEX String " << hex << std::endl; + // Convert to byte stream + tmx::byte_stream bytes = tmx::byte_stream_decode(hex); + // Read SpateEncodedMessage from bytes + tmx::messages::J2735MessageFactory myFactory; + spatEncoded_ptr.reset(dynamic_cast(myFactory.NewMessage(bytes))); + if (tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) + { + xer_fprint(stdout, &asn_DEF_SPAT, spatEncoded_ptr->decode_j2735_message().get_j2735_data().get()); } } + else { + throw tmx::TmxException("Could not find UPER Payload in received SPAT UDP Packet!"); + } } + } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 037cdcda1..0da434fba 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace SpatPlugin { enum class SPAT_MODE @@ -32,5 +33,8 @@ namespace SpatPlugin { bool initializeSignalControllerConnection(); tmx::messages::SpatEncodedMessage receiveSPAT(SPAT *spat, uint64_t timeMs , const SPAT_MODE &spat_mode = SPAT_MODE::BINARY); + void receiveBinarySPAT(std::shared_ptr spat, uint64_t timeMs); + + void receiveUPERSPAT(std::shared_ptr spatEncoded_ptr); }; } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 052879e03..0c0676be2 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -47,16 +47,32 @@ namespace SpatPlugin { auto connected = scConnection->initializeSignalControllerConnection(); if ( connected ) { SetStatus(keyConnectionStatus, "IDLE"); - - spatReceiverThread->AddPeriodicTick([this]() - { - this->processSpat(); - }, // end of lambda expression - std::chrono::milliseconds(5)); - PluginClientClockAware::getClock()->wait_for_initialization(); - spatReceiverThread->Start(); + try { + spatReceiverThread->AddPeriodicTick([this]() + { + this->processSpat(); + if (!this->isConnected) { + SetStatus(keyConnectionStatus, "CONNECTED"); + this->isConnected = true; + } + }, // end of lambda expression + std::chrono::milliseconds(5) + ); + PluginClientClockAware::getClock()->wait_for_initialization(); + spatReceiverThread->Start(); + } + catch (const TmxException &e) { + PLOG(tmx::utils::logERROR) << "Encountered error " << e.what() << " during SPAT Processing." << std::endl + << e.GetBacktrace(); + SetStatus(keyConnectionStatus, "ERROR"); + this->isConnected = false; + + } } else { + PLOG(tmx::utils::logERROR) << "Traffic Signal Controller at " << scIp << ":" << scSNMPPort << " failed!"; + SetStatus(keyConnectionStatus, "DISCONNECTED"); + this->isConnected = false; } } @@ -65,27 +81,35 @@ namespace SpatPlugin { void SpatPlugin::processSpat() { if (this->scConnection ) { PLOG(tmx::utils::logDEBUG) << "Processing SPAT ... " << std::endl; - SPAT_MODE mode; - if (spatMode == "BINARY") - { - mode = SPAT_MODE::BINARY; - } - else if (spatMode == "J2735_HEX") { - mode = SPAT_MODE::J2735_HEX; + try { + if (spatMode == "BINARY") + { + auto spat_ptr = std::make_shared(); + scConnection->receiveBinarySPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds()); + tmx::messages::SpatMessage _spatMessage(spat_ptr); + tmx::messages::SpatEncodedMessage spatEncodedMsg; + spatEncodedMsg.initialize(_spatMessage,"", 0U, IvpMsgFlags_RouteDSRC); + spatEncodedMsg.addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); + + PLOG(tmx::utils::logDEBUG) << "Broadcasting SPAT" << std::endl; + BroadcastMessage(static_cast(spatEncodedMsg)); + } + else if (spatMode == "J2735_HEX") { + auto spatEncoded_ptr = std::make_shared(); + scConnection->receiveUPERSPAT(spatEncoded_ptr); + spatEncoded_ptr->set_flags(IvpMsgFlags_RouteDSRC); + spatEncoded_ptr->addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); + auto rMsg = dynamic_cast(spatEncoded_ptr.get()); + BroadcastMessage(*rMsg); + } + else { + throw TmxException("SPAT Mode " + spatMode + " is not supported. Support SPAT Modes are J2735_HEX and BINARY."); + } } - else { - PLOG(tmx::utils::logWARNING) << "SPAT Mode " << spatMode << " is unrecognized. Defaulting to BINARY." << std::endl; + catch (const tmx::J2735Exception &e) { + PLOG(tmx::utils::logERROR) << "Encountered J2735 Exception " << e.what() << " attempting to process SPAT." << std::endl + << e.GetBacktrace(); } - SPAT *spat_ptr = (SPAT *) calloc(1, sizeof(SPAT)); - auto spatMessage = scConnection->receiveSPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds(), mode); - - spatMessage.set_flags(IvpMsgFlags_RouteDSRC); - spatMessage.addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); - - BroadcastMessage(static_cast(spatMessage)); - PLOG(tmx::utils::logDEBUG) << "Broadcasting SPAT" << std::endl; - - SetStatus(keyConnectionStatus, "CONNECTED"); } } void SpatPlugin::OnConfigChanged(const char *key, const char *value) { diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index 802fa331f..a2e618e08 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -49,6 +49,9 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { const char* keyConnectionStatus = "Connection Status"; + const char* keySkippedMessages = "Skipped Messages"; + + bool isConnected = false; void processSpat(); }; From ff4e4f18fa4da26249795f43f8fd16e7244d7f14 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 11 Jul 2024 17:41:45 -0400 Subject: [PATCH 11/31] PR updates --- src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 2 +- src/v2i-hub/SpatPlugin/src/NTCIP1202.h | 3 +-- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 7 +++++-- src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 4 +++- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index 5b31a6bb5..236220479 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -482,7 +482,7 @@ int Ntcip1202::getPedestrianSignalGroupForPhase(int phase) return signalGroupId; } -long Ntcip1202::getAdjustedTime(unsigned int offset_tenthofSec, unsigned long msEpoch) +long Ntcip1202::getAdjustedTime(unsigned int offset_tenthofSec, unsigned long msEpoch) const { // generate J2735 TimeMark which is: // Tenths of a second in the current or next hour diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h index 90b67cc88..3fbff0411 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h @@ -68,7 +68,6 @@ struct SignalGroupMapping class Ntcip1202 { public: - inline explicit Ntcip1202() {}; void setSignalGroupMappingList(string json); void copyBytesIntoNtcip1202(char* buff, int numBytes); @@ -99,7 +98,7 @@ class Ntcip1202 uint16_t getOverlapMinTime(int phaseNumber); uint16_t getOverlapMaxTime(int phaseNumber); - long getAdjustedTime(unsigned int offset_tenthofSec, unsigned long msEpoch); + long getAdjustedTime(unsigned int offset_tenthofSec, unsigned long msEpoch) const; bool isFlashingStatus(); bool isPhaseFlashing(); diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 0c0676be2..fcda7f12d 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -7,7 +7,7 @@ using namespace tmx::utils; namespace SpatPlugin { - SpatPlugin::SpatPlugin(string name) :PluginClientClockAware(name) { + SpatPlugin::SpatPlugin(const std::string &name) :PluginClientClockAware(name) { spatReceiverThread = std::make_unique(std::chrono::milliseconds(5)); if ( PluginClientClockAware::isSimulationMode() ) { @@ -92,7 +92,8 @@ namespace SpatPlugin { spatEncodedMsg.addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); PLOG(tmx::utils::logDEBUG) << "Broadcasting SPAT" << std::endl; - BroadcastMessage(static_cast(spatEncodedMsg)); + auto rMsg = static_cast(spatEncodedMsg); + BroadcastMessage(rMsg); } else if (spatMode == "J2735_HEX") { auto spatEncoded_ptr = std::make_shared(); @@ -109,6 +110,8 @@ namespace SpatPlugin { catch (const tmx::J2735Exception &e) { PLOG(tmx::utils::logERROR) << "Encountered J2735 Exception " << e.what() << " attempting to process SPAT." << std::endl << e.GetBacktrace(); + + SetStatus(keySkippedMessages, skippedMessages++); } } } diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index a2e618e08..744f88276 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -25,7 +25,7 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { public: - SpatPlugin(std::string name); + SpatPlugin(const std::string &name); virtual ~SpatPlugin(); @@ -50,6 +50,8 @@ class SpatPlugin: public tmx::utils::PluginClientClockAware { const char* keyConnectionStatus = "Connection Status"; const char* keySkippedMessages = "Skipped Messages"; + + uint skippedMessages = 0; bool isConnected = false; From 4a3e5733effe616fa2584d2d734c6eb31b6133ae Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 15 Jul 2024 07:47:01 -0400 Subject: [PATCH 12/31] Updates --- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index fcda7f12d..e181e7f26 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -87,13 +87,11 @@ namespace SpatPlugin { auto spat_ptr = std::make_shared(); scConnection->receiveBinarySPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds()); tmx::messages::SpatMessage _spatMessage(spat_ptr); - tmx::messages::SpatEncodedMessage spatEncodedMsg; - spatEncodedMsg.initialize(_spatMessage,"", 0U, IvpMsgFlags_RouteDSRC); - spatEncodedMsg.addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); - - PLOG(tmx::utils::logDEBUG) << "Broadcasting SPAT" << std::endl; - auto rMsg = static_cast(spatEncodedMsg); - BroadcastMessage(rMsg); + auto spatEncoded_ptr = std::make_shared(); + spatEncoded_ptr->initialize(_spatMessage,"", 0U, IvpMsgFlags_RouteDSRC); + spatEncoded_ptr->addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); + auto rMsg = dynamic_cast(spatEncoded_ptr.get()); + BroadcastMessage(*rMsg); } else if (spatMode == "J2735_HEX") { auto spatEncoded_ptr = std::make_shared(); From d636052d4d20a5a37b0bffc9bfafeded71aeb12d Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 15 Jul 2024 07:48:55 -0400 Subject: [PATCH 13/31] Updates --- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index e181e7f26..055cd5aa7 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -108,8 +108,8 @@ namespace SpatPlugin { catch (const tmx::J2735Exception &e) { PLOG(tmx::utils::logERROR) << "Encountered J2735 Exception " << e.what() << " attempting to process SPAT." << std::endl << e.GetBacktrace(); - - SetStatus(keySkippedMessages, skippedMessages++); + skippedMessages++; + SetStatus(keySkippedMessages, skippedMessages); } } } From d971ccd4bafa650355ba4a3679c8eba92a1f4b12 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 15 Jul 2024 07:56:27 -0400 Subject: [PATCH 14/31] Updates --- src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index 236220479..bbfb5b19f 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -38,13 +38,13 @@ void Ntcip1202::setSignalGroupMappingList(string json) ptree root; read_json(ss, root); - for(auto & signalGroup : root.get_child("SignalGroups")) + for( const auto &[key, value]: root.get_child("SignalGroups")) { - int signalGroupId = signalGroup.second.get("SignalGroupId"); - int phaseNumber = signalGroup.second.get("Phase", 0); - string typeName = signalGroup.second.get("Type"); + int signalGroupId = value.get("SignalGroupId"); + int phaseNumber = value.get("Phase", 0); + string typeName = value.get("Type"); - PLOG(logDEBUG) <<"signalGroupId: "< Date: Tue, 16 Jul 2024 19:42:27 -0400 Subject: [PATCH 15/31] Updates --- .../TmxUtils/{test => src}/MockSNMPClient.h | 0 src/tmx/TmxUtils/src/MockUdpServer.h | 2 +- src/tmx/TmxUtils/test/test_SNMPClient.cpp | 6 +- src/v2i-hub/SpatPlugin/CMakeLists.txt | 3 +- .../src/SignalControllerConnection.cpp | 8 +- .../src/SignalControllerConnection.h | 14 +- .../test/TestSignalControllerConnection.cpp | 312 ++++++++++++++++++ 7 files changed, 331 insertions(+), 14 deletions(-) rename src/tmx/TmxUtils/{test => src}/MockSNMPClient.h (100%) diff --git a/src/tmx/TmxUtils/test/MockSNMPClient.h b/src/tmx/TmxUtils/src/MockSNMPClient.h similarity index 100% rename from src/tmx/TmxUtils/test/MockSNMPClient.h rename to src/tmx/TmxUtils/src/MockSNMPClient.h diff --git a/src/tmx/TmxUtils/src/MockUdpServer.h b/src/tmx/TmxUtils/src/MockUdpServer.h index 533965711..0197f54bb 100644 --- a/src/tmx/TmxUtils/src/MockUdpServer.h +++ b/src/tmx/TmxUtils/src/MockUdpServer.h @@ -24,6 +24,6 @@ namespace tmx::utils { MOCK_METHOD(std::string, GetAddress, (), (const, override)); MOCK_METHOD(int, Receive, (char *msg, size_t maxSize), (override)); MOCK_METHOD(int, GetSocket, (), (override, const)); + MOCK_METHOD(std::string, stringTimedReceive, (int maxWait_ms), (override)); }; - } \ No newline at end of file diff --git a/src/tmx/TmxUtils/test/test_SNMPClient.cpp b/src/tmx/TmxUtils/test/test_SNMPClient.cpp index 3e775945c..55871477d 100644 --- a/src/tmx/TmxUtils/test/test_SNMPClient.cpp +++ b/src/tmx/TmxUtils/test/test_SNMPClient.cpp @@ -1,7 +1,7 @@ -#include "MockSNMPClient.h" -#include "gtest/gtest.h" -#include "RSU_MIB_4_1.h" +#include +#include +#include using namespace tmx::utils; using namespace std; diff --git a/src/v2i-hub/SpatPlugin/CMakeLists.txt b/src/v2i-hub/SpatPlugin/CMakeLists.txt index d75cd037c..202a952d9 100644 --- a/src/v2i-hub/SpatPlugin/CMakeLists.txt +++ b/src/v2i-hub/SpatPlugin/CMakeLists.txt @@ -11,7 +11,8 @@ target_link_libraries(${PROJECT_NAME} tmxutils ::carma-clock jsoncpp) -add_library(${PROJECT_NAME}_lib src/NTCIP1202.cpp) +add_library(${PROJECT_NAME}_lib src/NTCIP1202.cpp + src/SignalControllerConnection.cpp) target_link_libraries(${PROJECT_NAME}_lib PUBLIC tmxutils ::carma-clock ) ############# diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index e7854ad29..a69c238e8 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -12,7 +12,7 @@ namespace SpatPlugin { return scSNMPClient->process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, resp); }; - void SignalControllerConnection::receiveBinarySPAT(std::shared_ptr spat, uint64_t timeMs ) { + void SignalControllerConnection::receiveBinarySPAT(std::shared_ptr &spat, uint64_t timeMs ) { FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; char buf[1000]; auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); @@ -30,8 +30,8 @@ namespace SpatPlugin { } } - void SignalControllerConnection::receiveUPERSPAT(std::shared_ptr spatEncoded_ptr) { - FILE_LOG(tmx::utils::logDEBUG) << "Receiving J2725 HEX SPAT ..." << std::endl; + void SignalControllerConnection::receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr) { + FILE_LOG(tmx::utils::logDEBUG1) << "Receiving J2725 HEX SPAT ..." << std::endl; auto payload = spatPacketReceiver->stringTimedReceive( 1000 ); auto index = payload.find("Payload="); if ( index != std::string::npos ) { @@ -40,7 +40,7 @@ namespace SpatPlugin { // Remove new lines and empty space hex.erase(std::remove(hex.begin(), hex.end(), '\n'), hex.end()); hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); - FILE_LOG(tmx::utils::logDEBUG) << "Reading HEX String " << hex << std::endl; + FILE_LOG(tmx::utils::logDEBUG1) << "Reading HEX String " << hex << std::endl; // Convert to byte stream tmx::byte_stream bytes = tmx::byte_stream_decode(hex); // Read SpateEncodedMessage from bytes diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 0da434fba..521caa0be 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -8,6 +8,8 @@ #include #include #include +#include + namespace SpatPlugin { enum class SPAT_MODE @@ -20,21 +22,23 @@ namespace SpatPlugin { { private: // UDP Server socket listening for SPAT - std::unique_ptr spatPacketReceiver; + std::shared_ptr spatPacketReceiver; - std::unique_ptr scSNMPClient; + std::shared_ptr scSNMPClient; std::string signalGroupMapping; std::string intersectionName; unsigned int intersectionId; + friend class TestSignalControllerConnection; + FRIEND_TEST(TestSignalControllerConnection, initialize); public: SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); + bool initializeSignalControllerConnection(); - tmx::messages::SpatEncodedMessage receiveSPAT(SPAT *spat, uint64_t timeMs , const SPAT_MODE &spat_mode = SPAT_MODE::BINARY); - void receiveBinarySPAT(std::shared_ptr spat, uint64_t timeMs); + void receiveBinarySPAT(std::shared_ptr &spat, uint64_t timeMs); - void receiveUPERSPAT(std::shared_ptr spatEncoded_ptr); + void receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr); }; } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp index e69de29bb..22b50a704 100644 --- a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp @@ -0,0 +1,312 @@ +#include +#include +#include +#include +#include + +using testing::_; +using testing::Action; +using testing::DoDefault; +using testing::Return; +using testing::SetArgReferee; +using testing::Throw; +namespace SpatPlugin { + class TestSignalControllerConnection : public ::testing::Test + { + public: + TestSignalControllerConnection() { + + } + void SetUp() { + std::string signalGroupMapping = R"( + {\"SignalGroups\": + [ + {\"SignalGroupId\":1,\"Phase\":1,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":2,\"Phase\":2,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":3,\"Phase\":3,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":4,\"Phase\":4,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":5,\"Phase\":5,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":6,\"Phase\":6,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":7,\"Phase\":7,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":8,\"Phase\":8,\"Type\":\"vehicle\"}, + {\"SignalGroupId\":22,\"Phase\":2,\"Type\":\"pedestrian\"}, + {\"SignalGroupId\":24,\"Phase\":4,\"Type\":\"pedestrian\"}, + {\"SignalGroupId\":26,\"Phase\":6,\"Type\":\"pedestrian\"}, + {\"SignalGroupId\":28,\"Phase\":8,\"Type\":\"pedestrian\"} + ] + } + )"; + signalControllerConnection = std::make_unique("127.0.0.1", 5000, signalGroupMapping, "", 5020, "someIntersection", 9001); + mockSnmpClient = std::make_shared("127.0.0.1", 6045, "administrator", "", "", ""); + mockUdpServer = std::make_shared(); + signalControllerConnection->scSNMPClient = mockSnmpClient; + signalControllerConnection->spatPacketReceiver = mockUdpServer; + } + + std::shared_ptr mockSnmpClient; + std::shared_ptr mockUdpServer; + + std::unique_ptr signalControllerConnection; + }; + + TEST_F(TestSignalControllerConnection, initialize) { + tmx::utils::snmp_response_obj set_value; + set_value.type = tmx::utils::snmp_response_obj::response_type::INTEGER; + set_value.val_int = 2; + EXPECT_CALL(*mockSnmpClient, process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, set_value)).WillOnce(testing::DoAll(SetArgReferee<2>(set_value), Return(true))); + EXPECT_TRUE(signalControllerConnection->initializeSignalControllerConnection()); + } + + TEST_F(TestSignalControllerConnection, receiveBinarySPAT) { + } + + TEST_F(TestSignalControllerConnection, receiveUPERSPAT) { + std::string uper_hex = R"( + Version=0.7 + Type=SPAT + PSID=0x8002 + Priority=7 + TxMode=CONT + TxChannel=172 + TxInterval=0 + DeliveryStart= + DeliveryStop= + Signature=True + Encryption=False + Payload=00136b4457f20180000000208457f2c7c20b0010434162bc650001022a0b0be328000c10d058af194000808682c578ca00050434162bc650003022a0b0be328001c10d058af194001008682c578ca000904341617c650005021a0b15e328002c10d0585f194001808682c578ca00 + )"; + EXPECT_CALL(*mockUdpServer, stringTimedReceive(1000)).WillOnce(testing::DoAll(Return(uper_hex))); + + /** + * + 284658 + + + + 0 + + 0 + + 0000001000001000 + + 284658 + 51138 + + + 1 + + + + + 11351 + 36000 + + + + + + 2 + + + + + 11311 + 36000 + + + + + + 3 + + + + + 11351 + 36000 + + + + + + 4 + + + + + 11351 + 36000 + + + + + + 5 + + + + + 11351 + 36000 + + + + + + 6 + + + + + 11311 + 36000 + + + + + + 7 + + + + + 11351 + 36000 + + + + + + 8 + + + + + 11351 + 36000 + + + + + + 9 + + + + + 11311 + 36000 + + + + + + 10 + + + + + 11351 + 36000 + + + + + + 11 + + + + + 11311 + 36000 + + + + + + 12 + + + + + 11351 + 36000 + + + + + + + + + */ + auto spatEncoded_ptr = std::make_shared(); + signalControllerConnection->receiveUPERSPAT(spatEncoded_ptr); + auto spat = spatEncoded_ptr->decode_j2735_message().get_j2735_data(); + EXPECT_EQ(284658L, *spat->timeStamp); + EXPECT_EQ(0, spat->intersections.list.array[0]->id.id); + EXPECT_EQ(284658L, *spat->intersections.list.array[0]->moy); + EXPECT_EQ(51138, *spat->intersections.list.array[0]->timeStamp); + // Signal Group 1 + EXPECT_EQ(1, spat->intersections.list.array[0]->states.list.array[0]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 2 + EXPECT_EQ(2, spat->intersections.list.array[0]->states.list.array[1]->signalGroup); + EXPECT_EQ(5, spat->intersections.list.array[0]->states.list.array[1]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11311, spat->intersections.list.array[0]->states.list.array[1]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[1]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 3 + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[2]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[2]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[2]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[2]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 4 + EXPECT_EQ(4, spat->intersections.list.array[0]->states.list.array[3]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[3]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[3]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[3]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 5 + EXPECT_EQ(5, spat->intersections.list.array[0]->states.list.array[4]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[4]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[4]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[4]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 6 + EXPECT_EQ(6, spat->intersections.list.array[0]->states.list.array[5]->signalGroup); + EXPECT_EQ(5, spat->intersections.list.array[0]->states.list.array[5]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11311, spat->intersections.list.array[0]->states.list.array[5]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[5]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 7 + EXPECT_EQ(7, spat->intersections.list.array[0]->states.list.array[6]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[6]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[6]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[6]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 8 + EXPECT_EQ(8, spat->intersections.list.array[0]->states.list.array[7]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[7]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[7]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[7]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 9 + EXPECT_EQ(9, spat->intersections.list.array[0]->states.list.array[8]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[8]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11311, spat->intersections.list.array[0]->states.list.array[8]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[8]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 10 + EXPECT_EQ(10, spat->intersections.list.array[0]->states.list.array[9]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[9]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[9]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[9]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 11 + EXPECT_EQ(11, spat->intersections.list.array[0]->states.list.array[10]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[10]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11311, spat->intersections.list.array[0]->states.list.array[10]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[10]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 12 + EXPECT_EQ(12, spat->intersections.list.array[0]->states.list.array[11]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->timing->maxEndTime); + } +} \ No newline at end of file From 58130d051b592bfeac3d98ebf0d9d8c58116c3d9 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 17 Jul 2024 17:29:19 -0400 Subject: [PATCH 16/31] Updates --- src/v2i-hub/SpatPlugin/CMakeLists.txt | 2 +- .../src/SignalControllerConnection.cpp | 9 +- .../src/SignalControllerConnection.h | 7 +- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 36 +- src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp | 1 - .../test/TestSignalControllerConnection.cpp | 311 +++++++++++++++++- .../test_spat_binaries/spat_1721238398773.bin | Bin 0 -> 241 bytes 7 files changed, 326 insertions(+), 40 deletions(-) create mode 100644 src/v2i-hub/SpatPlugin/test/test_spat_binaries/spat_1721238398773.bin diff --git a/src/v2i-hub/SpatPlugin/CMakeLists.txt b/src/v2i-hub/SpatPlugin/CMakeLists.txt index 202a952d9..b92eafb52 100644 --- a/src/v2i-hub/SpatPlugin/CMakeLists.txt +++ b/src/v2i-hub/SpatPlugin/CMakeLists.txt @@ -7,7 +7,7 @@ find_package(carma-clock REQUIRED) BuildTmxPlugin() -target_link_libraries(${PROJECT_NAME} tmxutils ::carma-clock jsoncpp) +target_link_libraries(${PROJECT_NAME} tmxutils ::carma-clock) diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index a69c238e8..e50b9d201 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -16,14 +16,17 @@ namespace SpatPlugin { FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; char buf[1000]; auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); - - if ( numBytes > 0 ) { + if (numBytes > 0) + { // Convert Binary buffer to SPAT pointer Ntcip1202 ntcip1202; ntcip1202.setSignalGroupMappingList(this->signalGroupMapping); ntcip1202.copyBytesIntoNtcip1202(buf, numBytes); ntcip1202.ToJ2735SPAT(spat.get(),timeMs, intersectionName, intersectionId); - + if (tmx::utils::FILELog::ReportingLevel() >= tmx::utils::logDEBUG) + { + xer_fprint(stdout, &asn_DEF_SPAT, spat.get()); + } } else { throw tmx::utils::UdpServerRuntimeError("UDP Server error occured or socket time out."); diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 521caa0be..a18a12f0a 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -11,12 +11,8 @@ #include + namespace SpatPlugin { - enum class SPAT_MODE - { - J2735_HEX, - BINARY - }; class SignalControllerConnection { @@ -30,7 +26,6 @@ namespace SpatPlugin { std::string intersectionName; unsigned int intersectionId; friend class TestSignalControllerConnection; - FRIEND_TEST(TestSignalControllerConnection, initialize); public: SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 055cd5aa7..e9c16538b 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -9,7 +9,6 @@ namespace SpatPlugin { SpatPlugin::SpatPlugin(const std::string &name) :PluginClientClockAware(name) { spatReceiverThread = std::make_unique(std::chrono::milliseconds(5)); - if ( PluginClientClockAware::isSimulationMode() ) { SubscribeToMessages(); } @@ -82,18 +81,8 @@ namespace SpatPlugin { if (this->scConnection ) { PLOG(tmx::utils::logDEBUG) << "Processing SPAT ... " << std::endl; try { - if (spatMode == "BINARY") - { - auto spat_ptr = std::make_shared(); - scConnection->receiveBinarySPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds()); - tmx::messages::SpatMessage _spatMessage(spat_ptr); - auto spatEncoded_ptr = std::make_shared(); - spatEncoded_ptr->initialize(_spatMessage,"", 0U, IvpMsgFlags_RouteDSRC); - spatEncoded_ptr->addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); - auto rMsg = dynamic_cast(spatEncoded_ptr.get()); - BroadcastMessage(*rMsg); - } - else if (spatMode == "J2735_HEX") { + + if (spatMode == "J2735_HEX") { auto spatEncoded_ptr = std::make_shared(); scConnection->receiveUPERSPAT(spatEncoded_ptr); spatEncoded_ptr->set_flags(IvpMsgFlags_RouteDSRC); @@ -102,15 +91,30 @@ namespace SpatPlugin { BroadcastMessage(*rMsg); } else { - throw TmxException("SPAT Mode " + spatMode + " is not supported. Support SPAT Modes are J2735_HEX and BINARY."); + if ( spatMode != "BINARY"){ + PLOG(tmx::utils::logWARNING) << spatMode << " is an unsupport SPAT MODE. Defaulting to BINARY. Supported options are BINARY and J2735_HEX"; + } + auto spat_ptr = std::make_shared(); + scConnection->receiveBinarySPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds()); + tmx::messages::SpatMessage _spatMessage(spat_ptr); + auto spatEncoded_ptr = std::make_shared(); + spatEncoded_ptr->initialize(_spatMessage,"", 0U, IvpMsgFlags_RouteDSRC); + spatEncoded_ptr->addDsrcMetadata(tmx::messages::api::msgPSID::signalPhaseAndTimingMessage_PSID); + auto rMsg = dynamic_cast(spatEncoded_ptr.get()); + BroadcastMessage(*rMsg); } } - catch (const tmx::J2735Exception &e) { - PLOG(tmx::utils::logERROR) << "Encountered J2735 Exception " << e.what() << " attempting to process SPAT." << std::endl + catch (const UdpServerRuntimeError &e) { + PLOG(tmx::utils::logWARNING) << "Encountered UDP Server Runtime Error" << e.what() << " attempting to process SPAT." << std::endl + << e.GetBacktrace(); + } + catch (const tmx::TmxException &e) { + PLOG(tmx::utils::logERROR) << "Encountered Tmx Exception " << e.what() << " attempting to process SPAT." << std::endl << e.GetBacktrace(); skippedMessages++; SetStatus(keySkippedMessages, skippedMessages); } + } } void SpatPlugin::OnConfigChanged(const char *key, const char *value) { diff --git a/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp index 7b7f64a49..2f1b8cbd6 100644 --- a/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp @@ -23,7 +23,6 @@ TEST(NTCIP1202Test, copyBytesIntoNtcip1202) SPAT *spat_ptr = (SPAT *)calloc(1, sizeof(SPAT)); ntcip1202_p->ToJ2735SPAT(spat_ptr,tsMsec, "test intersection name", 9012); - xer_fprint(stdout, &asn_DEF_SPAT, spat_ptr); ASSERT_EQ(3, spat_ptr->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->eventState); diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp index 22b50a704..424a5430c 100644 --- a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp @@ -6,9 +6,12 @@ using testing::_; using testing::Action; +using testing::ByRef; using testing::DoDefault; using testing::Return; +using testing::SetArgPointee; using testing::SetArgReferee; +using testing::SetArrayArgument; using testing::Throw; namespace SpatPlugin { class TestSignalControllerConnection : public ::testing::Test @@ -19,20 +22,20 @@ namespace SpatPlugin { } void SetUp() { std::string signalGroupMapping = R"( - {\"SignalGroups\": + {"SignalGroups": [ - {\"SignalGroupId\":1,\"Phase\":1,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":2,\"Phase\":2,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":3,\"Phase\":3,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":4,\"Phase\":4,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":5,\"Phase\":5,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":6,\"Phase\":6,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":7,\"Phase\":7,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":8,\"Phase\":8,\"Type\":\"vehicle\"}, - {\"SignalGroupId\":22,\"Phase\":2,\"Type\":\"pedestrian\"}, - {\"SignalGroupId\":24,\"Phase\":4,\"Type\":\"pedestrian\"}, - {\"SignalGroupId\":26,\"Phase\":6,\"Type\":\"pedestrian\"}, - {\"SignalGroupId\":28,\"Phase\":8,\"Type\":\"pedestrian\"} + {"SignalGroupId":1,"Phase":1,"Type":"vehicle"}, + {"SignalGroupId":2,"Phase":2,"Type":"vehicle"}, + {"SignalGroupId":3,"Phase":3,"Type":"vehicle"}, + {"SignalGroupId":4,"Phase":4,"Type":"vehicle"}, + {"SignalGroupId":5,"Phase":5,"Type":"vehicle"}, + {"SignalGroupId":6,"Phase":6,"Type":"vehicle"}, + {"SignalGroupId":7,"Phase":7,"Type":"vehicle"}, + {"SignalGroupId":8,"Phase":8,"Type":"vehicle"}, + {"SignalGroupId":9,"Phase":2,"Type":"pedestrian"}, + {"SignalGroupId":10,"Phase":4,"Type":"pedestrian"}, + {"SignalGroupId":11,"Phase":6,"Type":"pedestrian"}, + {"SignalGroupId":12,"Phase":8,"Type":"pedestrian"} ] } )"; @@ -43,6 +46,28 @@ namespace SpatPlugin { signalControllerConnection->spatPacketReceiver = mockUdpServer; } + std::vector read_binary_file(std::string name) + { + std::ifstream file(name.c_str(), std::ios::binary); + std::vector buf; + + if (!file.good()) + { + throw runtime_error("Could not open file " + name); + } + + file.unsetf(std::ios::skipws); + file.seekg(0, std::ios::end); + const size_t size = file.tellg(); + + file.seekg(0, std::ios::beg); + buf.resize(size); + file.read(buf.data(), size); + file.close(); + + return buf; + } + std::shared_ptr mockSnmpClient; std::shared_ptr mockUdpServer; @@ -58,6 +83,246 @@ namespace SpatPlugin { } TEST_F(TestSignalControllerConnection, receiveBinarySPAT) { + auto spat_binary_buf = read_binary_file("../../SpatPlugin/test/test_spat_binaries/spat_1721238398773.bin"); + EXPECT_CALL(*mockUdpServer, TimedReceive(_, 1000, 1000)).WillOnce(testing::DoAll(SetArrayArgument<0>(spat_binary_buf.begin(), spat_binary_buf.end()), Return(spat_binary_buf.size()))); + auto spat = std::make_shared(); + signalControllerConnection->receiveBinarySPAT(spat, 1721238398773); + /** + * + + + someIntersection + + 9001 + + 1 + + 0000000000000000 + + 286186 + 38773 + + + 1 + + + + + 28027 + 21522 + + + + + + 2 + + + + + 27987 + 21522 + + + + + + 9 + + + + + 27987 + + + + + + 3 + + + + + 28027 + 21522 + + + + + + 4 + + + + + 28027 + 21522 + + + + + + 10 + + + + + 28027 + + + + + + 5 + + + + + 28027 + 21522 + + + + + + 6 + + + + + 27987 + 21522 + + + + + + 11 + + + + + 27987 + + + + + + 0 + + + + + + 7 + + + + + 28027 + 21522 + + + + + + 8 + + + + + 28027 + 21522 + + + + + + 12 + + + + + 28027 + + + + + + 0 + + + + + + + + + */ + EXPECT_EQ(9001, spat->intersections.list.array[0]->id.id); + EXPECT_EQ(286186, *spat->intersections.list.array[0]->moy); + EXPECT_EQ(38773, *spat->intersections.list.array[0]->timeStamp); + // Signal Group 1 + EXPECT_EQ(1, spat->intersections.list.array[0]->states.list.array[0]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[0]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 2 + EXPECT_EQ(2, spat->intersections.list.array[0]->states.list.array[1]->signalGroup); + EXPECT_EQ(6, spat->intersections.list.array[0]->states.list.array[1]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(27987, spat->intersections.list.array[0]->states.list.array[1]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[1]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 9 + EXPECT_EQ(9, spat->intersections.list.array[0]->states.list.array[2]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[2]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(27987, spat->intersections.list.array[0]->states.list.array[2]->state_time_speed.list.array[0]->timing->minEndTime); + // Signal Group 3 + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[3]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[3]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[3]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[3]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 4 + EXPECT_EQ(4, spat->intersections.list.array[0]->states.list.array[4]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[4]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[4]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[4]->state_time_speed.list.array[0]->timing->maxEndTime); + // // Signal Group 10 + EXPECT_EQ(10, spat->intersections.list.array[0]->states.list.array[5]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[5]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[5]->state_time_speed.list.array[0]->timing->minEndTime); + // // Signal Group 5 + EXPECT_EQ(5, spat->intersections.list.array[0]->states.list.array[6]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[6]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[6]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[6]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 6 + EXPECT_EQ(6, spat->intersections.list.array[0]->states.list.array[7]->signalGroup); + EXPECT_EQ(6, spat->intersections.list.array[0]->states.list.array[7]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(27987, spat->intersections.list.array[0]->states.list.array[7]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[7]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 11 + EXPECT_EQ(11, spat->intersections.list.array[0]->states.list.array[8]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[8]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(27987, spat->intersections.list.array[0]->states.list.array[8]->state_time_speed.list.array[0]->timing->minEndTime); + // Signal Group 7 + EXPECT_EQ(7, spat->intersections.list.array[0]->states.list.array[9]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[9]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[9]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[9]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 8 + EXPECT_EQ(8, spat->intersections.list.array[0]->states.list.array[10]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[10]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[10]->state_time_speed.list.array[0]->timing->minEndTime); + EXPECT_EQ(21522, *spat->intersections.list.array[0]->states.list.array[10]->state_time_speed.list.array[0]->timing->maxEndTime); + // Signal Group 12 + EXPECT_EQ(12, spat->intersections.list.array[0]->states.list.array[11]->signalGroup); + EXPECT_EQ(3, spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->eventState); + EXPECT_EQ(28027, spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->timing->minEndTime); + } + TEST_F(TestSignalControllerConnection, receiveBinarySPATException) { + EXPECT_CALL(*mockUdpServer, TimedReceive(_, 1000, 1000)).WillOnce(testing::DoAll( Return(0))); + auto spat = std::make_shared(); + EXPECT_THROW(signalControllerConnection->receiveBinarySPAT(spat, 1721238398773), tmx::utils::UdpServerRuntimeError); } TEST_F(TestSignalControllerConnection, receiveUPERSPAT) { @@ -309,4 +574,24 @@ namespace SpatPlugin { EXPECT_EQ(11351, spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->timing->minEndTime); EXPECT_EQ(36000, *spat->intersections.list.array[0]->states.list.array[11]->state_time_speed.list.array[0]->timing->maxEndTime); } + + TEST_F(TestSignalControllerConnection, receiveUPERSPATException) { + std::string without_paylod = R"( + Version=0.7 + Type=SPAT + PSID=0x8002 + Priority=7 + TxMode=CONT + TxChannel=172 + TxInterval=0 + DeliveryStart= + DeliveryStop= + Signature=True + Encryption=False + )"; + EXPECT_CALL(*mockUdpServer, stringTimedReceive(1000)).WillOnce(testing::DoAll(Return(without_paylod))); + + auto spatEncoded_ptr = std::make_shared(); + EXPECT_THROW(signalControllerConnection->receiveUPERSPAT(spatEncoded_ptr), tmx::TmxException); + } } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/test/test_spat_binaries/spat_1721238398773.bin b/src/v2i-hub/SpatPlugin/test/test_spat_binaries/spat_1721238398773.bin new file mode 100644 index 0000000000000000000000000000000000000000..694c2e4b560c87a0b5001e5081b46b4ab0ddcdd7 GIT binary patch literal 241 zcmX>rz{sHS|33p1FflNII1mA5WEmDP2db16S%wW+4?D692eKYcm>nRR3z@@>%;7=i j@FH{gkU9Lw906p`|GPk*62u1xk1;TCurMkpGB5xD@01|c literal 0 HcmV?d00001 From 0330cfd4e7c4d6e3f19e44a65581b15018b99853 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 17 Jul 2024 18:32:03 -0400 Subject: [PATCH 17/31] Updates --- src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 28 ++-- src/v2i-hub/SpatPlugin/src/NTCIP1202.h | 32 +++-- .../src/SignalControllerConnection.cpp | 21 ++- .../src/SignalControllerConnection.h | 74 ++++++++-- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 15 ++ src/v2i-hub/SpatPlugin/src/SpatPlugin.h | 129 ++++++++++++------ src/v2i-hub/SpatPlugin/test/Main.cpp | 17 ++- src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp | 16 ++- .../test/TestSignalControllerConnection.cpp | 16 +++ 9 files changed, 263 insertions(+), 85 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index bbfb5b19f..2c4663746 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -1,20 +1,20 @@ -/* - * NTCIP1202.cpp +/** + * Copyright (C) 2024 LEIDOS. * - * Created on: Apr 3, 2017 - * Author: ivp + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ -#include -#include #include "NTCIP1202.h" -#include -#include -#include -#include - -#include -#include -#include + using namespace std; using namespace boost::property_tree; diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h index 3fbff0411..b92a12a26 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h @@ -1,18 +1,35 @@ -/* - * NTCIP1202.h +/** + * Copyright (C) 2024 LEIDOS. * - * Created on: Apr 3, 2017 - * Author: ivp + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ -#ifndef SRC_NTCIP1202_H_ -#define SRC_NTCIP1202_H_ +#pragma once #include #include #include - +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include "carma-clock/carma_clock.h" using namespace std; @@ -124,4 +141,3 @@ class Ntcip1202 -#endif /* SRC_NTCIP1202_H_ */ diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index e50b9d201..df0ce2dd6 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2024 LEIDOS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ #include "SignalControllerConnection.h" namespace SpatPlugin { @@ -5,14 +20,14 @@ namespace SpatPlugin { SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_unique(localIp, localPort)) ,scSNMPClient(std::make_unique(scIp, scSNMPPort ,"administrator", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; - bool SignalControllerConnection::initializeSignalControllerConnection() { + bool SignalControllerConnection::initializeSignalControllerConnection() const { tmx::utils::snmp_response_obj resp; resp.val_int = 2; resp.type = tmx::utils::snmp_response_obj::response_type::INTEGER; return scSNMPClient->process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, resp); }; - void SignalControllerConnection::receiveBinarySPAT(std::shared_ptr &spat, uint64_t timeMs ) { + void SignalControllerConnection::receiveBinarySPAT(const std::shared_ptr &spat, uint64_t timeMs ) const { FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; char buf[1000]; auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); @@ -33,7 +48,7 @@ namespace SpatPlugin { } } - void SignalControllerConnection::receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr) { + void SignalControllerConnection::receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr) const { FILE_LOG(tmx::utils::logDEBUG1) << "Receiving J2725 HEX SPAT ..." << std::endl; auto payload = spatPacketReceiver->stringTimedReceive( 1000 ); auto index = payload.find("Payload="); diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index a18a12f0a..f20dfc5cb 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2024 LEIDOS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ #pragma once #include @@ -13,27 +28,66 @@ namespace SpatPlugin { - + /** + * @brief Class to represent Traffic Signal Controller (TSC or SC) connection. Includes a UDP Server for listening for + * SPaT data broadcast from TSC. Also includes an SNMP Client for request or setting status of SNMP objects described + * in NTCIP 1202 (Object Definitions for Actuated Signal Controllers). + */ class SignalControllerConnection { private: - // UDP Server socket listening for SPAT + /** + * @brief UDP Server for receiving SPaT packets from TSC. + */ std::shared_ptr spatPacketReceiver; - + /** + * @brief SNMP Client for requesting or setting status of SNMP Objects on + * TSC. + */ std::shared_ptr scSNMPClient; - + /** + * @brief String that describes phase to signal group mapping configured on + * the TSC. TODO: Remove in place of SNMP requests on tables. + */ std::string signalGroupMapping; + /** + * @brief String name of intersection in SPaT messages. + */ std::string intersectionName; + /** + * @brief Numeric identifier for intersection in SPaT messages. + */ unsigned int intersectionId; + friend class TestSignalControllerConnection; public: + /** + * @brief Constructor for Signal Controller Connection. + * @param localIp IP address of device connecting to signal controller. This will be the IP of the UDP Server listening for SPaT data. + * @param localPort port on which to listen for SPaT data. + * @param signalGroupMapping JSON mapping of phases to signal groups + * @param scIp IP address of TSC + * @param scSNMPPort port of SNMP Server on TSC + * @param intersectionName Name of intersection + * @param intersectionID Intersection ID. + */ SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); - - bool initializeSignalControllerConnection(); - - void receiveBinarySPAT(std::shared_ptr &spat, uint64_t timeMs); - - void receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr); + /** + * @brief Method attempts to send SNMP SET request to SNMP object to enable SPAT broadcasting. + * @return true if successful and false if not. + */ + bool initializeSignalControllerConnection() const; + /** + * @brief Method to receive SPaT data in binary format from TSC. + * @param spat an empty SPaT pointer to which the SPAT data will be written. + * @param timeMs current time in ms from epoch to use for message timestamp. + */ + void receiveBinarySPAT(const std::shared_ptr &spat, uint64_t timeMs) const; + /** + * @brief Method to receive SPaT data in UPER Hex format from TSC. + * @param spatEncoded_ptr Empty SpatEncodedMessage to which the UPER encoded SPaT data will be written. + */ + void receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr) const; }; } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index e9c16538b..a25b8194b 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2024 LEIDOS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ #include "SpatPlugin.h" diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h index 744f88276..bb3bc9182 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.h +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.h @@ -1,12 +1,19 @@ -/* - * SpatPlugin.h +/** + * Copyright (C) 2024 LEIDOS. * - * Created on: April 20, 2017 - * Author: zinkg + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ - -#ifndef SPATPLUGIN_H_ -#define SPATPLUGIN_H_ +#pragma once #include #include @@ -20,43 +27,75 @@ #include "SignalControllerConnection.h" namespace SpatPlugin { - -class SpatPlugin: public tmx::utils::PluginClientClockAware { - -public: - - SpatPlugin(const std::string &name); - virtual ~SpatPlugin(); - - -protected: - - void UpdateConfigSettings(); - - // Virtual method overrides. - void OnConfigChanged(const char *key, const char *value); - void OnStateChange(IvpPluginState state); - -private: - - std::mutex data_lock; - - std::unique_ptr spatReceiverThread; - - std::unique_ptr scConnection; - - std::string spatMode = ""; - - const char* keyConnectionStatus = "Connection Status"; - - const char* keySkippedMessages = "Skipped Messages"; - - uint skippedMessages = 0; - - bool isConnected = false; - - void processSpat(); -}; + /** + * @brief The SPaT Plugin is responsible for receiving information from the Traffic Signal Controller (TSC or SC) necessary + * for broadcasting Signal Phase and Timing (SPaT) messages. This includes querying any SNMP objects to determine + * TSC state and listen for any broadcast SPaT information from the TSC. + */ + class SpatPlugin: public tmx::utils::PluginClientClockAware { + + public: + /** + * @brief Plugin Constructor. + * @param name Plugin Name. + */ + explicit SpatPlugin(const std::string &name); + /** + * @brief Plugin Destructor + */ + virtual ~SpatPlugin(); + + + protected: + /** + * @brief Method to update plugin after configuration settings have changed. + */ + void UpdateConfigSettings(); + + // Virtual method overrides. + void OnConfigChanged(const char *key, const char *value) override; + void OnStateChange(IvpPluginState state) override; + + private: + /** + * @brief Mutex for thread safety for configuration parameters. + */ + std::mutex data_lock; + /** + * @brief Thread timer used to periodically consume broadcast SPaT + * data from the TSC . + */ + std::unique_ptr spatReceiverThread; + /** + * @brief TSC Connection. + */ + std::unique_ptr scConnection; + /** + * @brief String describing the expected format of received SPaT data. + */ + std::string spatMode = ""; + /** + * @brief Key for state object describing TSC Connection Status. + */ + const char* keyConnectionStatus = "Connection Status"; + /** + * @brief Key for counting the number of received packets from TSC that + * have been skipped due to errors. + */ + const char* keySkippedMessages = "Skipped Messages"; + /** + * @brief Count of received packets from the TSC that have been skipped due to + * errors. + */ + uint skippedMessages = 0; + /** + * @brief Bool flag for TSC connection status. + */ + bool isConnected = false; + /** + * @brief Method to receive and process TSC broadcast SPaT data. + */ + void processSpat(); + }; } /* namespace SpatPlugin */ -#endif /* SPATPLUGIN_H_ */ diff --git a/src/v2i-hub/SpatPlugin/test/Main.cpp b/src/v2i-hub/SpatPlugin/test/Main.cpp index 75163d417..5c1d740bb 100644 --- a/src/v2i-hub/SpatPlugin/test/Main.cpp +++ b/src/v2i-hub/SpatPlugin/test/Main.cpp @@ -1,8 +1,17 @@ -/* - * Main.cpp +/** + * Copyright (C) 2024 LEIDOS. * - * Created on: May 10, 2016 - * Author: ivp + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ #include diff --git a/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp index 2f1b8cbd6..22d5f7486 100644 --- a/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp @@ -1,4 +1,18 @@ - +/** + * Copyright (C) 2024 LEIDOS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ #include #include #include diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp index 424a5430c..e78d50ee6 100644 --- a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2024 LEIDOS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + #include #include #include From e9ee34a921e4a0e1f66935435dd1fe4444bce0b6 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 18 Jul 2024 08:55:04 -0400 Subject: [PATCH 18/31] Updates --- src/v2i-hub/SpatPlugin/CMakeLists.txt | 22 ++------------ src/v2i-hub/SpatPlugin/README.md | 43 +++++++++++++++++++++++++++ src/v2i-hub/SpatPlugin/manifest.json | 13 +++----- 3 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 src/v2i-hub/SpatPlugin/README.md diff --git a/src/v2i-hub/SpatPlugin/CMakeLists.txt b/src/v2i-hub/SpatPlugin/CMakeLists.txt index b92eafb52..fa987d581 100644 --- a/src/v2i-hub/SpatPlugin/CMakeLists.txt +++ b/src/v2i-hub/SpatPlugin/CMakeLists.txt @@ -1,36 +1,20 @@ project( SpatPlugin VERSION 7.6.0 LANGUAGES CXX ) - set(TMX_PLUGIN_NAME "SPAT") set(CMAKE_CXX_STANDARD 17) - find_package(carma-clock REQUIRED) - BuildTmxPlugin() - target_link_libraries(${PROJECT_NAME} tmxutils ::carma-clock) - - - add_library(${PROJECT_NAME}_lib src/NTCIP1202.cpp src/SignalControllerConnection.cpp) -target_link_libraries(${PROJECT_NAME}_lib PUBLIC - tmxutils ::carma-clock ) +target_link_libraries(${PROJECT_NAME}_lib PUBLIC tmxutils ::carma-clock ) ############# ## Testing ## ############# enable_testing() - set(BINARY ${PROJECT_NAME}_test) - file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false test/*.h test/*.cpp) - -set(SOURCES ${TEST_SOURCES} - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/test) - +set(SOURCES ${TEST_SOURCES} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/test) add_executable(${BINARY} ${TEST_SOURCES}) - add_test(NAME ${BINARY} COMMAND ${BINARY}) target_include_directories(${BINARY} PUBLIC src/) - -target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME}_lib - gtest) \ No newline at end of file +target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME}_lib gtest) \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/README.md b/src/v2i-hub/SpatPlugin/README.md new file mode 100644 index 000000000..148c37e84 --- /dev/null +++ b/src/v2i-hub/SpatPlugin/README.md @@ -0,0 +1,43 @@ +# SPaT Plugin Documentation + +## Introduction +The SPaT Plugin is responsible for receiving information from the Traffic Signal Controller (TSC or SC) necessary for broadcasting Signal Phase and Timing (SPaT) messages. This includes querying any SNMP objects to determine TSC state and listen for any broadcast SPaT information from the TSC. + +## Related Plugins + +A list of plugins related to the SPaT Plugin. + +### Immediate Forward Plugin + +For RSU Immediate Message Forwarding (IMF) functionality forward SPaT (Signal Phase and Timing Messages). + +## Configuration/Deployment + +This plugin has several configuration parameters. Below these are listed out as together with descriptions on how to set them. + +**Intersection_ID**: The intersection id for SPAT generated by this plugin (Note Only used in SPAT MODE = BINARY). + +**Intersection_Name**: The intersection name for SPAT generated by this plugin (Note Only used in SPAT MODE = BINARY). + +**SignalGroupMapping**: JSON data defining a list of SignalGroups and phases (Note Only used in SPAT MODE = BINARY). + +**Local_IP**: The IPv4 address of the local computer for receiving Traffic Signal Controller Broadcast Messages. + +**Local_UDP_Port**: The local UDP port for reception of Traffic Signal Controller Broadcast Messages from the TSC. + +**TSC_IP**: The IPv4 address of the destination Traffic Signal Controller (TSC). + +**TSC_Remote_SNMP_Port**: The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP communication." + +**SPAT_Mode**: The format of received SPAT from Traffic Signal Controller (TSC). Acceptance values are BINARY and J2735_HEX. + +## Design + + +### Messages + + +## Functionality Testing + + + diff --git a/src/v2i-hub/SpatPlugin/manifest.json b/src/v2i-hub/SpatPlugin/manifest.json index 58dccbb51..2ca547efa 100644 --- a/src/v2i-hub/SpatPlugin/manifest.json +++ b/src/v2i-hub/SpatPlugin/manifest.json @@ -1,6 +1,6 @@ { "name":"SPAT", - "description":"Plugin that reads PTLM data from a configuration file, receives live data from the signal controller, and publishes a J2735 SPAT message.", + "description":"The SPaT plugin receives live Signal Phase and Timing data from the Traffic Signal Controller and publishes a J2735 SPAT message.", "version":"@PROJECT_VERSION@", "exeLocation":"/bin/SpatPlugin", "coreIpAddr":"127.0.0.1", @@ -10,28 +10,23 @@ "type":"J2735", "subtype":"SPAT-P", "description":"Signal Phase and Timing (SPAT) status for the signalized intersection." - }, - { - "type":"SIGCONT", - "subtype":"ACT", - "description":"Current signal controller action" } ], "configuration":[ { "key":"Intersection_Id", "default":"1", - "description":"The intersection id for SPAT generated by this plugin." + "description":"The intersection id for SPAT generated by this plugin (Note Only used in SPAT MODE = BINARY)." }, { "key":"Intersection_Name", "default":"Intersection", - "description":"The intersection name for SPAT generated by this plugin." + "description":"The intersection name for SPAT generated by this plugin (Note Only used in SPAT MODE = BINARY)." }, { "key":"SignalGroupMapping", "default":"{\"SignalGroups\":[{\"SignalGroupId\":1,\"Phase\":1,\"Type\":\"vehicle\"},{\"SignalGroupId\":2,\"Phase\":2,\"Type\":\"vehicle\"},{\"SignalGroupId\":3,\"Phase\":3,\"Type\":\"vehicle\"},{\"SignalGroupId\":4,\"Phase\":4,\"Type\":\"vehicle\"},{\"SignalGroupId\":5,\"Phase\":5,\"Type\":\"vehicle\"},{\"SignalGroupId\":6,\"Phase\":6,\"Type\":\"vehicle\"},{\"SignalGroupId\":7,\"Phase\":7,\"Type\":\"vehicle\"},{\"SignalGroupId\":8,\"Phase\":8,\"Type\":\"vehicle\"},{\"SignalGroupId\":22,\"Phase\":2,\"Type\":\"pedestrian\"},{\"SignalGroupId\":24,\"Phase\":4,\"Type\":\"pedestrian\"},{\"SignalGroupId\":26,\"Phase\":6,\"Type\":\"pedestrian\"},{\"SignalGroupId\":28,\"Phase\":8,\"Type\":\"pedestrian\"}]}", - "description":"JSON data defining a list of SignalGroups and phases." + "description":"JSON data defining a list of SignalGroups and phases (Note Only used in SPAT MODE = BINARY)." }, { "key":"Local_IP", From 487fdba205341233b032e3c45383e592465009ee Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 18 Jul 2024 11:26:40 -0400 Subject: [PATCH 19/31] Documentation --- src/v2i-hub/SpatPlugin/README.md | 38 ++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/README.md b/src/v2i-hub/SpatPlugin/README.md index 148c37e84..9a32cb844 100644 --- a/src/v2i-hub/SpatPlugin/README.md +++ b/src/v2i-hub/SpatPlugin/README.md @@ -1,6 +1,7 @@ # SPaT Plugin Documentation ## Introduction + The SPaT Plugin is responsible for receiving information from the Traffic Signal Controller (TSC or SC) necessary for broadcasting Signal Phase and Timing (SPaT) messages. This includes querying any SNMP objects to determine TSC state and listen for any broadcast SPaT information from the TSC. ## Related Plugins @@ -11,6 +12,11 @@ A list of plugins related to the SPaT Plugin. For RSU Immediate Message Forwarding (IMF) functionality forward SPaT (Signal Phase and Timing Messages). +### Map Plugin + +The SPaT and MAP messages are strongly related in that MAP messages are required to be able to understand SPaT messages. Map +messages offer data about which signal groups impact which lanes in a given intersection. + ## Configuration/Deployment This plugin has several configuration parameters. Below these are listed out as together with descriptions on how to set them. @@ -33,11 +39,39 @@ This plugin has several configuration parameters. Below these are listed out as ## Design +![Alt text](docs/spat_plugin_design.png) +The diagram above illustrates roughly how the SPaT Plugin functions. The SPaT Plugin is able to get and set Traffic Signal Controller (TSC or SC) configuration via SNMP requests. The defined objects and behaviour are standardize via NTCIP 1202, the National Transportation Communications for ITS Protocol Object Definitions for Actuated Signal Controllers (ASC) Interface. It also receives live Signal Phase and Timing Data from the TSC via UDP packets. Using this information, the SPaT plugin generates J2725 SPaT messags which are eventually forwarded to and Road Side Unit (RSU) radio for broadcast to actors at or near the intersection. ### Messages +**SPAT**: This message contains information from the traffic signal controller about Signal Phase and Timing (SPaT). To use this information for vehicle control the MAP message is also required for mapping signal phase to lanes in an intersection. ## Functionality Testing - - +Testing the functionality of a configured instance of the SPaT plugin requires access to a Traffic Signal Controller (TSC) or Virtual Traffic Signal Controller. + +1) Configure the TSC to broadcast SPAT to an open port your edge device. + 1) Take note of the format of this data. This can be done by consulting the TSC manual or by inspecting it using `tcpdump -i any port -X`.**BINARY** payloads will be complete human unreadable collection of bytes and **J2735_HEX** will include human readable header information like `Type=SPAT`, `PSID=0x8002`, and a `Payload` followed by string HEX of the UPER encoded J2735 SPaT message. +2) Inspect the configurations of your TSC to determine Signal Group to phase mapping and the SNMP port. This can be found either at the **Channel Table** or the **Load Switch Configuration** on the TSC. Using these configurations create your signal group mapping JSON (Example shown below) +```json + +{"SignalGroups": + [ + {"SignalGroupId":1,"Phase":1,"Type":"vehicle"}, + {"SignalGroupId":2,"Phase":2,"Type":"vehicle"}, + {"SignalGroupId":3,"Phase":3,"Type":"vehicle"}, + {"SignalGroupId":4,"Phase":4,"Type":"vehicle"}, + {"SignalGroupId":5,"Phase":5,"Type":"vehicle"}, + {"SignalGroupId":6,"Phase":6,"Type":"vehicle"}, + {"SignalGroupId":7,"Phase":7,"Type":"vehicle"}, + {"SignalGroupId":8,"Phase":8,"Type":"vehicle"}, + {"SignalGroupId":9,"Phase":2,"Type":"pedestrian"}, + {"SignalGroupId":10,"Phase":4,"Type":"pedestrian"}, + {"SignalGroupId":11,"Phase":6,"Type":"pedestrian"}, + {"SignalGroupId":12,"Phase":8,"Type":"pedestrian"} + ] +} +``` + +3) Configure your SPaT Plugin with the information gathers in steps 1 and 2. +4) Check the Status and Messages tabs on the plugin after enabling it. Once enabled the SPAT Plugin should be receive SPAT messages at 10Hz (visible in the Messages tab) and have a status of "CONNECTED" \ No newline at end of file From 4173d33fc5fa01219053692d1e77fa317df4510f Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 18 Jul 2024 12:00:08 -0400 Subject: [PATCH 20/31] Updates --- .../SpatPlugin/docs/spat_plugin_design.png | Bin 0 -> 21312 bytes src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h | 30 ++++++++++++++++++ .../src/SignalControllerConnection.cpp | 3 +- .../src/SignalControllerConnection.h | 4 ++- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/v2i-hub/SpatPlugin/docs/spat_plugin_design.png create mode 100644 src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h diff --git a/src/v2i-hub/SpatPlugin/docs/spat_plugin_design.png b/src/v2i-hub/SpatPlugin/docs/spat_plugin_design.png new file mode 100644 index 0000000000000000000000000000000000000000..de85b4ffecd51fbe38e11cf0d2bc00c17f1bfb0c GIT binary patch literal 21312 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 literal 0 HcmV?d00001 diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h new file mode 100644 index 000000000..585025a36 --- /dev/null +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2024 LEIDOS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +#pragma once + +#include + +namespace NTCIP1202V3{ + /** + * @brief OID for ENABLE_SPAT + */ + static const std::string ENABLE_SPAT_OID = "1.3.6.1.4.1.1206.3.5.2.9.44.1.0"; + /** + * @brief OID for Intersection ID. + */ + static const std::string INTERSECTION_ID = " 1.3.6.1.4.1.1206.4.2.1.17.1.2.1.2"; +} \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index df0ce2dd6..319dd0c55 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -21,10 +21,11 @@ namespace SpatPlugin { }; bool SignalControllerConnection::initializeSignalControllerConnection() const { + // TODO : Set Intersection ID and Intersection Name tmx::utils::snmp_response_obj resp; resp.val_int = 2; resp.type = tmx::utils::snmp_response_obj::response_type::INTEGER; - return scSNMPClient->process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, resp); + return scSNMPClient->process_snmp_request(NTCIP1202V3::ENABLE_SPAT_OID, tmx::utils::request_type::SET, resp); }; void SignalControllerConnection::receiveBinarySPAT(const std::shared_ptr &spat, uint64_t timeMs ) const { diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index f20dfc5cb..6212b61d9 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -18,13 +18,15 @@ #include #include #include -#include "NTCIP1202.h" #include #include #include #include #include +#include "NTCIP1202.h" +#include "NTCIP1202OIDs.h" + namespace SpatPlugin { From d63f552b251d5b7f6ef312387797740e47e3c6fa Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 18 Jul 2024 16:10:22 -0400 Subject: [PATCH 21/31] Updates --- src/tmx/TmxUtils/src/PluginClientClockAware.cpp | 3 +-- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 12 +++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/tmx/TmxUtils/src/PluginClientClockAware.cpp b/src/tmx/TmxUtils/src/PluginClientClockAware.cpp index a3dae0c2f..9c770f24d 100644 --- a/src/tmx/TmxUtils/src/PluginClientClockAware.cpp +++ b/src/tmx/TmxUtils/src/PluginClientClockAware.cpp @@ -14,14 +14,13 @@ namespace tmx::utils { clock = std::make_shared(_simulation_mode); if (_simulation_mode) { AddMessageFilter(this, &PluginClientClockAware::HandleTimeSyncMessage); - } } void PluginClientClockAware::HandleTimeSyncMessage(tmx::messages::TimeSyncMessage &msg, routeable_message &routeableMsg ) { - if (sim::is_simulation_mode() ) { + if (_simulation_mode ) { PLOG(logDEBUG) << "Message Received " << msg.to_string() << std::endl; this->getClock()->update( msg.get_timestep() ); SetStatus(Key_Simulation_Time_Step, Clock::ToUtcPreciseTimeString(msg.get_timestep())); diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index a25b8194b..2514b3923 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -24,7 +24,8 @@ namespace SpatPlugin { SpatPlugin::SpatPlugin(const std::string &name) :PluginClientClockAware(name) { spatReceiverThread = std::make_unique(std::chrono::milliseconds(5)); - if ( PluginClientClockAware::isSimulationMode() ) { + if (PluginClientClockAware::isSimulationMode()) { + PLOG(logDEBUG1) << "Subscribing to TimeSyncMessages ... " ; SubscribeToMessages(); } } @@ -64,6 +65,10 @@ namespace SpatPlugin { try { spatReceiverThread->AddPeriodicTick([this]() { + // Ensure Clock has received its first update + if (PluginClientClockAware::isSimulationMode()) { + PluginClientClockAware::getClock()->wait_for_initialization(); + } this->processSpat(); if (!this->isConnected) { SetStatus(keyConnectionStatus, "CONNECTED"); @@ -72,7 +77,7 @@ namespace SpatPlugin { }, // end of lambda expression std::chrono::milliseconds(5) ); - PluginClientClockAware::getClock()->wait_for_initialization(); + spatReceiverThread->Start(); } catch (const TmxException &e) { @@ -110,6 +115,7 @@ namespace SpatPlugin { PLOG(tmx::utils::logWARNING) << spatMode << " is an unsupport SPAT MODE. Defaulting to BINARY. Supported options are BINARY and J2735_HEX"; } auto spat_ptr = std::make_shared(); + PLOG(logDEBUG) << "Starting SPaT Receiver ..."; scConnection->receiveBinarySPAT(spat_ptr, PluginClientClockAware::getClock()->nowInMilliseconds()); tmx::messages::SpatMessage _spatMessage(spat_ptr); auto spatEncoded_ptr = std::make_shared(); @@ -133,7 +139,7 @@ namespace SpatPlugin { } } void SpatPlugin::OnConfigChanged(const char *key, const char *value) { - PluginClientClockAware::OnConfigChanged(key, value); + PluginClient::OnConfigChanged(key, value); UpdateConfigSettings(); } From 572f82ebaf1c1775121f70a03c21124461da704d Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 11:10:47 -0400 Subject: [PATCH 22/31] Updates --- .devcontainer/docker-compose-vscode.yml | 6 +++--- src/tmx/TmxUtils/src/PluginClientClockAware.cpp | 8 +++++++- src/tmx/TmxUtils/src/PluginClientClockAware.h | 11 +++++------ .../src/MUSTSensorDriverPlugin.cpp | 4 ---- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 8 -------- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/.devcontainer/docker-compose-vscode.yml b/.devcontainer/docker-compose-vscode.yml index 7900b9dea..ca174735d 100755 --- a/.devcontainer/docker-compose-vscode.yml +++ b/.devcontainer/docker-compose-vscode.yml @@ -13,7 +13,7 @@ services: command: /bin/sh -c "while sleep 1000; do :; done" environment: - MYSQL_PASSWORD=/run/secrets/mysql_password - - SIMULATION_MODE=false + - SIMULATION_MODE=true - SIMULATION_IP=127.0.0.1 - SIMULATION_REGISTRATION_PORT=6767 - LOCAL_IP=127.0.0.1 @@ -22,8 +22,8 @@ services: - SIM_V2X_PORT=5757 - SIM_INTERACTION_PORT=7576 - V2X_PORT=8686 - - INFRASTRUCTURE_ID=rsu_123 - - INFRASTRUCTURE_NAME=SOmething + - INFRASTRUCTURE_ID=rsu_ + - INFRASTRUCTURE_NAME= - SENSOR_JSON_FILE_PATH=/var/www/plugins/MAP/sensors.json secrets: - mysql_password diff --git a/src/tmx/TmxUtils/src/PluginClientClockAware.cpp b/src/tmx/TmxUtils/src/PluginClientClockAware.cpp index 9c770f24d..cf08e3b43 100644 --- a/src/tmx/TmxUtils/src/PluginClientClockAware.cpp +++ b/src/tmx/TmxUtils/src/PluginClientClockAware.cpp @@ -14,6 +14,7 @@ namespace tmx::utils { clock = std::make_shared(_simulation_mode); if (_simulation_mode) { AddMessageFilter(this, &PluginClientClockAware::HandleTimeSyncMessage); + SubscribeToMessages(); } } @@ -22,7 +23,7 @@ namespace tmx::utils { void PluginClientClockAware::HandleTimeSyncMessage(tmx::messages::TimeSyncMessage &msg, routeable_message &routeableMsg ) { if (_simulation_mode ) { PLOG(logDEBUG) << "Message Received " << msg.to_string() << std::endl; - this->getClock()->update( msg.get_timestep() ); + clock->update( msg.get_timestep() ); SetStatus(Key_Simulation_Time_Step, Clock::ToUtcPreciseTimeString(msg.get_timestep())); } } @@ -38,4 +39,9 @@ namespace tmx::utils { return _simulation_mode; } + std::shared_ptr PluginClientClockAware::getClock() const { + clock->wait_for_initialization(); // Blocks until first call to update when in sim mode. + return clock; + } + } \ No newline at end of file diff --git a/src/tmx/TmxUtils/src/PluginClientClockAware.h b/src/tmx/TmxUtils/src/PluginClientClockAware.h index 974f55060..41512809a 100644 --- a/src/tmx/TmxUtils/src/PluginClientClockAware.h +++ b/src/tmx/TmxUtils/src/PluginClientClockAware.h @@ -33,15 +33,14 @@ class PluginClientClockAware : public PluginClient { protected: /** - * @brief Method for child classes to use to retrieve the clock object and get the simulation or real time. - * @return + * @brief Method for child classes to use to retrieve the clock object and get the simulation or real time. + * In simulation mode method will block until first time update has been received. + * @return clock */ - inline std::shared_ptr getClock() const { - return clock; - } + std::shared_ptr getClock() const ; void OnStateChange(IvpPluginState state) override; - + bool isSimulationMode() const; diff --git a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp index 946d67869..024430b4c 100644 --- a/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp +++ b/src/v2i-hub/MUSTSensorDriverPlugin/src/MUSTSensorDriverPlugin.cpp @@ -23,10 +23,6 @@ namespace MUSTSensorDriverPlugin { MUSTSensorDriverPlugin::MUSTSensorDriverPlugin(const string &name): PluginClientClockAware(name) { mustSensorPacketReceiverThread = std::make_unique(std::chrono::milliseconds(5)); - if (PluginClientClockAware::isSimulationMode()) { - PLOG(tmx::utils::logINFO) << "Simulation mode on " << std::endl; - SubscribeToMessages(); - } } void MUSTSensorDriverPlugin::OnStateChange(IvpPluginState state) { diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 2514b3923..10661ab18 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -24,10 +24,6 @@ namespace SpatPlugin { SpatPlugin::SpatPlugin(const std::string &name) :PluginClientClockAware(name) { spatReceiverThread = std::make_unique(std::chrono::milliseconds(5)); - if (PluginClientClockAware::isSimulationMode()) { - PLOG(logDEBUG1) << "Subscribing to TimeSyncMessages ... " ; - SubscribeToMessages(); - } } SpatPlugin::~SpatPlugin() { @@ -65,10 +61,6 @@ namespace SpatPlugin { try { spatReceiverThread->AddPeriodicTick([this]() { - // Ensure Clock has received its first update - if (PluginClientClockAware::isSimulationMode()) { - PluginClientClockAware::getClock()->wait_for_initialization(); - } this->processSpat(); if (!this->isConnected) { SetStatus(keyConnectionStatus, "CONNECTED"); From 5ee6df695287c6f7d93a79f82b59d9a3d1773cab Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 11:12:45 -0400 Subject: [PATCH 23/31] Update --- src/v2i-hub/MapPlugin/src/MapPlugin.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/v2i-hub/MapPlugin/src/MapPlugin.cpp b/src/v2i-hub/MapPlugin/src/MapPlugin.cpp index 7ee8d4ae7..884a02217 100644 --- a/src/v2i-hub/MapPlugin/src/MapPlugin.cpp +++ b/src/v2i-hub/MapPlugin/src/MapPlugin.cpp @@ -132,8 +132,7 @@ namespace MapPlugin { std::unique_ptr msg; int activeAction = -1; - // wait for the clock to be initialized - getClock()->wait_for_initialization(); + while (_plugin->state != IvpPluginState_error) { if (_isMapFileNew) { From 63e0df0a31978b42dc9a3ea8f145311407f9ea85 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 15:24:49 -0400 Subject: [PATCH 24/31] Updates --- src/tmx/Messages/test/TestTimeSyncMessage.cpp | 2 +- .../CDASimAdapter/scripts/send_timestep_udp.py | 2 +- src/v2i-hub/SpatPlugin/README.md | 3 +++ src/v2i-hub/SpatPlugin/docs/hex_tcpdump.png | Bin 0 -> 141628 bytes .../src/SignalControllerConnection.cpp | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 src/v2i-hub/SpatPlugin/docs/hex_tcpdump.png diff --git a/src/tmx/Messages/test/TestTimeSyncMessage.cpp b/src/tmx/Messages/test/TestTimeSyncMessage.cpp index c05b46087..a853ed2b2 100644 --- a/src/tmx/Messages/test/TestTimeSyncMessage.cpp +++ b/src/tmx/Messages/test/TestTimeSyncMessage.cpp @@ -5,6 +5,6 @@ namespace tmx::messages { TEST(TestTimeSyncMessage, to_string) { TimeSyncMessage msg(20, 30); std::string json = "{ \"timestep\":20, \"seq\":30}"; - ASSERT_EQ( json, msg.to_string()); + EXPECT_EQ( json, msg.to_string()); } } \ No newline at end of file diff --git a/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py b/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py index a5041396f..ba031d114 100755 --- a/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py +++ b/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py @@ -35,7 +35,7 @@ def generate_time_sync(): count_num += 1 sock.sendto(encoded_msg,host) print( encoded_msg.decode(encoding= 'UTF-8'), 'was sent to ', host) - time.sleep(5) + time.sleep(.1) except socket.gaierror: print ('There an error resolving the host') diff --git a/src/v2i-hub/SpatPlugin/README.md b/src/v2i-hub/SpatPlugin/README.md index 9a32cb844..97c29000c 100644 --- a/src/v2i-hub/SpatPlugin/README.md +++ b/src/v2i-hub/SpatPlugin/README.md @@ -36,6 +36,9 @@ This plugin has several configuration parameters. Below these are listed out as **TSC_Remote_SNMP_Port**: The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP communication." **SPAT_Mode**: The format of received SPAT from Traffic Signal Controller (TSC). Acceptance values are BINARY and J2735_HEX. +> [!NOTE] +> **J2735_HEX** is a new added SPAT format. If your TSC is able to send UPER SPAT directly to an RSU, this is the format in which it is being sent. Below is a screen shot of J2735_HEX SPaT via TCP Dump +![Alt text](docs/hex_tcpdump.png) ## Design diff --git a/src/v2i-hub/SpatPlugin/docs/hex_tcpdump.png b/src/v2i-hub/SpatPlugin/docs/hex_tcpdump.png new file mode 100644 index 0000000000000000000000000000000000000000..3e3991b9b5d6bd4bcfe1749a067ff1bc698caf47 GIT binary patch literal 141628 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

9c6)m&z^Hk3OF_&y1&{Qh%jg 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 TQSwIs=ub-QgJ^}YzW@IL+x`-Q literal 0 HcmV?d00001 diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index 319dd0c55..bb2ad63bc 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -17,7 +17,7 @@ namespace SpatPlugin { - SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_unique(localIp, localPort)) ,scSNMPClient(std::make_unique(scIp, scSNMPPort ,"administrator", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { + SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_shared(localIp, localPort)) ,scSNMPClient(std::make_shared(scIp, scSNMPPort ,"administrator", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; bool SignalControllerConnection::initializeSignalControllerConnection() const { From bd2c2b8c63bee90efe9e3c66946232f632258d01 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 22 Jul 2024 15:38:36 -0400 Subject: [PATCH 25/31] Updates --- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 10661ab18..6fa1fcfcf 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -75,7 +75,7 @@ namespace SpatPlugin { catch (const TmxException &e) { PLOG(tmx::utils::logERROR) << "Encountered error " << e.what() << " during SPAT Processing." << std::endl << e.GetBacktrace(); - SetStatus(keyConnectionStatus, "ERROR"); + SetStatus(keyConnectionStatus, "DISCONNECTED"); this->isConnected = false; } From 92d6a185bbd3a2966161215a3d3a807ee9efa1b4 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 24 Jul 2024 18:11:11 -0400 Subject: [PATCH 26/31] Updates --- src/tmx/TmxUtils/src/SNMPClient.cpp | 69 +++++++++++++---------- src/tmx/TmxUtils/src/SNMPClient.h | 12 ++++ src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 26 ++++----- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/tmx/TmxUtils/src/SNMPClient.cpp b/src/tmx/TmxUtils/src/SNMPClient.cpp index 5c8a5ff52..17fd45ff7 100644 --- a/src/tmx/TmxUtils/src/SNMPClient.cpp +++ b/src/tmx/TmxUtils/src/SNMPClient.cpp @@ -168,39 +168,10 @@ namespace tmx::utils if (status == STAT_SUCCESS && response && response->errstat == SNMP_ERR_NOERROR ) { if ( request_type == request_type::GET ) { - for (auto vars = response->variables; vars; vars = vars->next_variable) - { - // Get value of variable depending on ASN.1 type - // Variable could be a integer, string, bitstring, ojbid, counter : defined here https://github.com/net-snmp/net-snmp/blob/master/include/net-snmp/types.h - // get Integer value - if (vars->type == ASN_INTEGER && vars->val.integer) - { - val.type = snmp_response_obj::response_type::INTEGER; - val.val_int = *vars->val.integer; - } - else if (vars->type == ASN_OCTET_STR && vars->val.string) - { - size_t str_len = vars->val_len; - for (size_t i = 0; i < str_len; ++i) - { - val.val_string.push_back(vars->val.string[i]); - } - val.type = snmp_response_obj::response_type::STRING; - } - } + process_snmp_get_response(val, *response); } else if( request_type == request_type::SET){ - - if(val.type == snmp_response_obj::response_type::INTEGER){ - FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value: " << val.val_int << std::endl; - } - - else if(val.type == snmp_response_obj::response_type::STRING){ - FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value:" << std::endl; - for(auto data : val.val_string){ - FILE_LOG(logDEBUG) << data ; - } - } + process_snmp_set_response(val, input_oid); } else { log_error(status, request_type, response); @@ -227,6 +198,42 @@ namespace tmx::utils return port_; } + void snmp_client::process_snmp_get_response( snmp_response_obj &val, const snmp_pdu &response) const { + for (auto vars = response.variables; vars; vars = vars->next_variable) + { + // Get value of variable depending on ASN.1 type + // Variable could be a integer, string, bitstring, ojbid, counter : defined here https://github.com/net-snmp/net-snmp/blob/master/include/net-snmp/types.h + // get Integer value + if (vars->type == ASN_INTEGER && vars->val.integer) + { + val.type = snmp_response_obj::response_type::INTEGER; + val.val_int = *vars->val.integer; + } + else if (vars->type == ASN_OCTET_STR && vars->val.string) + { + size_t str_len = vars->val_len; + for (size_t i = 0; i < str_len; ++i) + { + val.val_string.push_back(vars->val.string[i]); + } + val.type = snmp_response_obj::response_type::STRING; + } + } + } + + void snmp_client::process_snmp_set_response( snmp_response_obj &val, const std::string &input_oid) const { + if(val.type == snmp_response_obj::response_type::INTEGER){ + FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value: " << val.val_int << std::endl; + } + + else if(val.type == snmp_response_obj::response_type::STRING){ + FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value:" << std::endl; + for(auto data : val.val_string){ + FILE_LOG(logDEBUG) << data ; + } + } + } + void snmp_client::log_error(const int &status, const request_type &request_type, const snmp_pdu *response) const { diff --git a/src/tmx/TmxUtils/src/SNMPClient.h b/src/tmx/TmxUtils/src/SNMPClient.h index 75eaa9360..36debe609 100644 --- a/src/tmx/TmxUtils/src/SNMPClient.h +++ b/src/tmx/TmxUtils/src/SNMPClient.h @@ -68,6 +68,18 @@ namespace tmx::utils int snmp_version_ = 3; // default to 3 since previous versions not compatable currently /*Time after which the the snmp request times out*/ int timeout_ = 10000; + /** + * @brief Helper method for populating snmp_respons_obj with SNMP get response. + * @param val response object + * @param response pdu + */ + void process_snmp_get_response( snmp_response_obj &val, const snmp_pdu &response) const; + /** + * @brief Helper method for logging successful SNMP set responses + * @param val response object + * @param input_oid OID + */ + void process_snmp_set_response( snmp_response_obj &val, const std::string &input_oid) const; public: /** @brief Constructor for Traffic Signal Controller Service client. diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 6fa1fcfcf..967a3c186 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -33,27 +33,27 @@ namespace SpatPlugin { void SpatPlugin::UpdateConfigSettings() { if (this->IsPluginState(IvpPluginState_registered)) { - std::string signalGroupMappingJson; + std::string signal_group_mapping_json; std::string ip_address; unsigned int port; - std::string scIp; - unsigned int scSNMPPort; - std::string intersectionName; - unsigned int intersectionId; - GetConfigValue("SignalGroupMapping", signalGroupMappingJson, &data_lock); + std::string signal_controller_ip; + unsigned int signal_controller_snmp_port; + std::string intersection_name; + unsigned int intersection_id; + GetConfigValue("SignalGroupMapping", signal_group_mapping_json, &data_lock); GetConfigValue("Local_IP", ip_address, &data_lock); GetConfigValue("Local_UDP_Port", port, &data_lock); - GetConfigValue("TSC_IP", scIp, &data_lock); - GetConfigValue("TSC_Remote_SNMP_Port", scSNMPPort,&data_lock); - GetConfigValue("Intersection_Name", intersectionName,&data_lock); - GetConfigValue("Intersection_Id", intersectionId, &data_lock); + GetConfigValue("TSC_IP", signal_controller_ip, &data_lock); + GetConfigValue("TSC_Remote_SNMP_Port", signal_controller_snmp_port,&data_lock); + GetConfigValue("Intersection_Name", intersection_name,&data_lock); + GetConfigValue("Intersection_Id", intersection_id, &data_lock); GetConfigValue("SPAT_Mode", spatMode, &data_lock); if (scConnection) { - scConnection.reset(new SignalControllerConnection(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId)); + scConnection.reset(new SignalControllerConnection(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port, intersection_name, intersection_id)); } else { - scConnection = std::make_unique(ip_address, port, signalGroupMappingJson, scIp, scSNMPPort, intersectionName, intersectionId); + scConnection = std::make_unique(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port, intersection_name, intersection_id); } auto connected = scConnection->initializeSignalControllerConnection(); if ( connected ) { @@ -81,7 +81,7 @@ namespace SpatPlugin { } } else { - PLOG(tmx::utils::logERROR) << "Traffic Signal Controller at " << scIp << ":" << scSNMPPort << " failed!"; + PLOG(tmx::utils::logERROR) << "Traffic Signal Controller at " << signal_controller_ip << ":" << signal_controller_snmp_port << " failed!"; SetStatus(keyConnectionStatus, "DISCONNECTED"); this->isConnected = false; From 53a91c6d877765655acacf30623d8a768d56307d Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 24 Jul 2024 18:49:50 -0400 Subject: [PATCH 27/31] Updates --- src/tmx/TmxUtils/src/SNMPClient.cpp | 4 ++-- src/tmx/TmxUtils/src/SNMPClient.h | 4 ++-- src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tmx/TmxUtils/src/SNMPClient.cpp b/src/tmx/TmxUtils/src/SNMPClient.cpp index 17fd45ff7..1cbb53853 100644 --- a/src/tmx/TmxUtils/src/SNMPClient.cpp +++ b/src/tmx/TmxUtils/src/SNMPClient.cpp @@ -198,7 +198,7 @@ namespace tmx::utils return port_; } - void snmp_client::process_snmp_get_response( snmp_response_obj &val, const snmp_pdu &response) const { + void snmp_client::process_snmp_get_response(snmp_response_obj &val, const snmp_pdu &response) const { for (auto vars = response.variables; vars; vars = vars->next_variable) { // Get value of variable depending on ASN.1 type @@ -221,7 +221,7 @@ namespace tmx::utils } } - void snmp_client::process_snmp_set_response( snmp_response_obj &val, const std::string &input_oid) const { + void snmp_client::process_snmp_set_response( const snmp_response_obj &val, const std::string &input_oid) const { if(val.type == snmp_response_obj::response_type::INTEGER){ FILE_LOG(logDEBUG) << "Success in SET for OID: " << input_oid << " Value: " << val.val_int << std::endl; } diff --git a/src/tmx/TmxUtils/src/SNMPClient.h b/src/tmx/TmxUtils/src/SNMPClient.h index 36debe609..f36646a53 100644 --- a/src/tmx/TmxUtils/src/SNMPClient.h +++ b/src/tmx/TmxUtils/src/SNMPClient.h @@ -73,13 +73,13 @@ namespace tmx::utils * @param val response object * @param response pdu */ - void process_snmp_get_response( snmp_response_obj &val, const snmp_pdu &response) const; + void process_snmp_get_response(snmp_response_obj &val, const snmp_pdu &response) const; /** * @brief Helper method for logging successful SNMP set responses * @param val response object * @param input_oid OID */ - void process_snmp_set_response( snmp_response_obj &val, const std::string &input_oid) const; + void process_snmp_set_response( const snmp_response_obj &val, const std::string &input_oid) const; public: /** @brief Constructor for Traffic Signal Controller Service client. diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp index 2c4663746..0e8773629 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.cpp @@ -222,9 +222,9 @@ void Ntcip1202::ToJ2735SPAT(SPAT* spat, unsigned long msEpoch , const std::strin intersection->name = (DescriptiveName_t *) calloc(1, sizeof(DescriptiveName_t)); - intersection->name->size = strlen(intersectionName.c_str()); - intersection->name->buf = (uint8_t *) calloc(1, strlen(intersectionName.c_str())); - memcpy(intersection->name->buf, intersectionName.c_str(), strlen(intersectionName.c_str())); + intersection->name->size = intersectionName.length(); + intersection->name->buf = (uint8_t *) calloc(1, intersectionName.length()); + memcpy(intersection->name->buf, intersectionName.c_str(), intersectionName.length()); intersection->id.id = intersectionId; intersection->revision = (MsgCount_t) 1; From ad9383ca8b666a71c6fc992bff005dda02feacd1 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 29 Jul 2024 18:15:33 -0400 Subject: [PATCH 28/31] PR Updates --- src/tmx/Messages/test/Main.cpp | 17 ++++++++++---- .../scripts/send_timestep_udp.py | 2 +- src/v2i-hub/SpatPlugin/README.md | 2 +- src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h | 19 +++++++++++---- .../src/SignalControllerConnection.cpp | 23 ++++++++++++++----- .../src/SignalControllerConnection.h | 10 ++++++-- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 4 +++- .../test/TestSignalControllerConnection.cpp | 15 ++++++++---- 8 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/tmx/Messages/test/Main.cpp b/src/tmx/Messages/test/Main.cpp index 75163d417..5c1d740bb 100644 --- a/src/tmx/Messages/test/Main.cpp +++ b/src/tmx/Messages/test/Main.cpp @@ -1,8 +1,17 @@ -/* - * Main.cpp +/** + * Copyright (C) 2024 LEIDOS. * - * Created on: May 10, 2016 - * Author: ivp + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. */ #include diff --git a/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py b/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py index ba031d114..a5041396f 100755 --- a/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py +++ b/src/v2i-hub/CDASimAdapter/scripts/send_timestep_udp.py @@ -35,7 +35,7 @@ def generate_time_sync(): count_num += 1 sock.sendto(encoded_msg,host) print( encoded_msg.decode(encoding= 'UTF-8'), 'was sent to ', host) - time.sleep(.1) + time.sleep(5) except socket.gaierror: print ('There an error resolving the host') diff --git a/src/v2i-hub/SpatPlugin/README.md b/src/v2i-hub/SpatPlugin/README.md index 97c29000c..81a13c0f8 100644 --- a/src/v2i-hub/SpatPlugin/README.md +++ b/src/v2i-hub/SpatPlugin/README.md @@ -33,7 +33,7 @@ This plugin has several configuration parameters. Below these are listed out as **TSC_IP**: The IPv4 address of the destination Traffic Signal Controller (TSC). -**TSC_Remote_SNMP_Port**: The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP communication." +**TSC_Remote_SNMP_Port**: The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP 1202 communication." **SPAT_Mode**: The format of received SPAT from Traffic Signal Controller (TSC). Acceptance values are BINARY and J2735_HEX. > [!NOTE] diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h index 585025a36..898792c5a 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h @@ -17,14 +17,23 @@ #pragma once #include - +/** + * @brief Contains OIDs (Object Identifier) described in NTCIP 1202 V3 (National Transporation Communication for ITS Protocol Object + * Definitions for Actuated Signal Controllers (ASC) Interface) used for SPaT Plugin functionality. + */ namespace NTCIP1202V3{ - /** - * @brief OID for ENABLE_SPAT - */ - static const std::string ENABLE_SPAT_OID = "1.3.6.1.4.1.1206.3.5.2.9.44.1.0"; /** * @brief OID for Intersection ID. */ static const std::string INTERSECTION_ID = " 1.3.6.1.4.1.1206.4.2.1.17.1.2.1.2"; +} +/** + * @brief Contains OIDs (Object Identifier) described in NTCIP 1202 V3 (National Transporation Communication for ITS Protocol Object + * Definitions for Actuated Signal Controllers (ASC) Interface) used for SPaT Plugin functionality. + */ +namespace NTCIP1202V2{ + /** + * @brief OID for ENABLE_SPAT + */ + static const std::string ENABLE_SPAT_OID = "1.3.6.1.4.1.1206.3.5.2.9.44.1.0"; } \ No newline at end of file diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index bb2ad63bc..32c4dcacd 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -20,12 +20,23 @@ namespace SpatPlugin { SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_shared(localIp, localPort)) ,scSNMPClient(std::make_shared(scIp, scSNMPPort ,"administrator", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; - bool SignalControllerConnection::initializeSignalControllerConnection() const { - // TODO : Set Intersection ID and Intersection Name - tmx::utils::snmp_response_obj resp; - resp.val_int = 2; - resp.type = tmx::utils::snmp_response_obj::response_type::INTEGER; - return scSNMPClient->process_snmp_request(NTCIP1202V3::ENABLE_SPAT_OID, tmx::utils::request_type::SET, resp); + bool SignalControllerConnection::initializeSignalControllerConnection(bool enable_spat, bool set_intersection_id) const { + // TODO : Update to more generic TSC Initialization process that simply follows NTCIP 1202 version guidelines. + bool status = true; + if (enable_spat) + { + tmx::utils::snmp_response_obj enable_spat; + enable_spat.val_int = 2; + enable_spat.type = tmx::utils::snmp_response_obj::response_type::INTEGER; + status = status && scSNMPClient->process_snmp_request(NTCIP1202V2::ENABLE_SPAT_OID, tmx::utils::request_type::SET, enable_spat); + } + if ( set_intersection_id ) { + tmx::utils::snmp_response_obj intersection_id; + intersection_id.val_int = intersectionId; + intersection_id.type = tmx::utils::snmp_response_obj::response_type::INTEGER; + status = status && scSNMPClient->process_snmp_request(NTCIP1202V3::INTERSECTION_ID, tmx::utils::request_type::SET, intersection_id); + } + return status; }; void SignalControllerConnection::receiveBinarySPAT(const std::shared_ptr &spat, uint64_t timeMs ) const { diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 6212b61d9..2e70c6243 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -75,11 +75,17 @@ namespace SpatPlugin { * @param intersectionID Intersection ID. */ SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); + /** - * @brief Method attempts to send SNMP SET request to SNMP object to enable SPAT broadcasting. + * @brief Method attempts to send SNMP SET requests to initialize the TSC. NOTE: Some of the + * OIDs in called in this initialize method may not be exposed by a TSC depending on which + * version of the NTCIP 1202 standard they are complaint with. To avoid failures please use + * the bool flags to indicate which OIDs need to be set for initialization. + * @param enable_spat bool flag on whether to attempt to set enable spat to true (NOT available for NTCIP 1202 versions >= 3 ) + * @param set_intersection_id bool flag on whether to set intersection id (NOT available for NTCIP 1202 versions < 3) * @return true if successful and false if not. */ - bool initializeSignalControllerConnection() const; + bool initializeSignalControllerConnection(bool enable_spat, bool set_intersection_id) const; /** * @brief Method to receive SPaT data in binary format from TSC. * @param spat an empty SPaT pointer to which the SPAT data will be written. diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index 967a3c186..e8c308ac0 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -55,7 +55,9 @@ namespace SpatPlugin { else { scConnection = std::make_unique(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port, intersection_name, intersection_id); } - auto connected = scConnection->initializeSignalControllerConnection(); + // Only enable spat broadcast in simulation mode. TFHRC TSCs do not expose this OID so calls to it will fail in hardware deployment + // Only set intersection ID in J2735_HEX SPAT Mode. Is only available for TSCs that support J2735 UPER HEX SPaT broadcast, which are all NTCIP 1202V3 or later. + auto connected = scConnection->initializeSignalControllerConnection(PluginClientClockAware::isSimulationMode(), this->spatMode == "J2735_HEX"); if ( connected ) { SetStatus(keyConnectionStatus, "IDLE"); try { diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp index e78d50ee6..e5b5aae34 100644 --- a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using testing::_; using testing::Action; @@ -91,11 +92,15 @@ namespace SpatPlugin { }; TEST_F(TestSignalControllerConnection, initialize) { - tmx::utils::snmp_response_obj set_value; - set_value.type = tmx::utils::snmp_response_obj::response_type::INTEGER; - set_value.val_int = 2; - EXPECT_CALL(*mockSnmpClient, process_snmp_request("1.3.6.1.4.1.1206.3.5.2.9.44.1.0", tmx::utils::request_type::SET, set_value)).WillOnce(testing::DoAll(SetArgReferee<2>(set_value), Return(true))); - EXPECT_TRUE(signalControllerConnection->initializeSignalControllerConnection()); + tmx::utils::snmp_response_obj enable_spat; + enable_spat.type = tmx::utils::snmp_response_obj::response_type::INTEGER; + enable_spat.val_int = 2; + tmx::utils::snmp_response_obj intersection_id; + intersection_id.type = tmx::utils::snmp_response_obj::response_type::INTEGER; + intersection_id.val_int = 9001; + EXPECT_CALL(*mockSnmpClient, process_snmp_request(NTCIP1202V2::ENABLE_SPAT_OID, tmx::utils::request_type::SET, enable_spat)).WillOnce(testing::DoAll(SetArgReferee<2>(enable_spat), Return(true))); + EXPECT_CALL(*mockSnmpClient, process_snmp_request(NTCIP1202V3::INTERSECTION_ID, tmx::utils::request_type::SET, intersection_id)).WillOnce(testing::DoAll(SetArgReferee<2>(enable_spat), Return(true))); + EXPECT_TRUE(signalControllerConnection->initializeSignalControllerConnection(true, true)); } TEST_F(TestSignalControllerConnection, receiveBinarySPAT) { From db47284cdaa0bb4b01e6f76ba0c9304ee0d193c3 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 29 Jul 2024 19:02:23 -0400 Subject: [PATCH 29/31] Updates --- src/v2i-hub/SpatPlugin/README.md | 4 +++- src/v2i-hub/SpatPlugin/manifest.json | 17 +++++++++++------ src/v2i-hub/SpatPlugin/src/NTCIP1202.h | 1 - .../src/SignalControllerConnection.cpp | 6 ++++-- .../SpatPlugin/src/SignalControllerConnection.h | 2 +- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 9 ++++++--- src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp | 1 + .../test/TestSignalControllerConnection.cpp | 2 +- 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/README.md b/src/v2i-hub/SpatPlugin/README.md index 81a13c0f8..f106d1fb3 100644 --- a/src/v2i-hub/SpatPlugin/README.md +++ b/src/v2i-hub/SpatPlugin/README.md @@ -33,7 +33,9 @@ This plugin has several configuration parameters. Below these are listed out as **TSC_IP**: The IPv4 address of the destination Traffic Signal Controller (TSC). -**TSC_Remote_SNMP_Port**: The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP 1202 communication." +**TSC_SNMP_Port**: The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP 1202 communication." + +**TSC_SNMP_Community**: The SNMP Community used for sending SNMP NTCIP 1202 communication to Traffic Signal Controller (TSC). Please refer TSC vendor documentation for SNMP Community. **SPAT_Mode**: The format of received SPAT from Traffic Signal Controller (TSC). Acceptance values are BINARY and J2735_HEX. > [!NOTE] diff --git a/src/v2i-hub/SpatPlugin/manifest.json b/src/v2i-hub/SpatPlugin/manifest.json index 2ca547efa..1297658a9 100644 --- a/src/v2i-hub/SpatPlugin/manifest.json +++ b/src/v2i-hub/SpatPlugin/manifest.json @@ -30,23 +30,28 @@ }, { "key":"Local_IP", - "default":"", + "default":"127.0.0.1", "description":"The IPv4 address of the local computer for receiving Traffic Signal Controller Broadcast Messages." }, { "key":"Local_UDP_Port", - "default":"local port", + "default":"6053", "description":"The local UDP port for reception of Traffic Signal Controller Broadcast Messages from the TSC." }, { "key":"TSC_IP", - "default":"", + "default":"127.0.0.1", "description":"The IPv4 address of the destination Traffic Signal Controller (TSC)." }, { - "key":"TSC_Remote_SNMP_Port", - "default":"", - "description":"The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP communication." + "key":"TSC_SNMP_Port", + "default":"5050", + "description":"The destination port on the Traffic Signal Controller (TSC) for SNMP NTCIP 1202 communication." + }, + { + "key":"TSC_SNMP_Community", + "default":"public", + "description":"The SNMP Community used for sending SNMP NTCIP 1202 communication to Traffic Signal Controller (TSC). Please refer TSC vendor documentation for SNMP Community." }, { "key":"SPAT_Mode", diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h index b92a12a26..2ebb279a8 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202.h @@ -30,7 +30,6 @@ #include #include #include -#include "carma-clock/carma_clock.h" using namespace std; diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index 32c4dcacd..431f5980f 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -17,14 +17,16 @@ namespace SpatPlugin { - SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_shared(localIp, localPort)) ,scSNMPClient(std::make_shared(scIp, scSNMPPort ,"administrator", "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { + SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &scSNMPCommunity, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_shared(localIp, localPort)) ,scSNMPClient(std::make_shared(scIp, scSNMPPort ,scSNMPCommunity, "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; bool SignalControllerConnection::initializeSignalControllerConnection(bool enable_spat, bool set_intersection_id) const { // TODO : Update to more generic TSC Initialization process that simply follows NTCIP 1202 version guidelines. bool status = true; if (enable_spat) - { + { + // For binary SPAT a value of 2 enables original SPAT binary broadcast on the TSC and a value of 6 enables original SPAT plugin additional Pedestrian Information. + // NOTE: Pedestrian information is untested. tmx::utils::snmp_response_obj enable_spat; enable_spat.val_int = 2; enable_spat.type = tmx::utils::snmp_response_obj::response_type::INTEGER; diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index 2e70c6243..f207941a4 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -74,7 +74,7 @@ namespace SpatPlugin { * @param intersectionName Name of intersection * @param intersectionID Intersection ID. */ - SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &intersectionName, unsigned int intersectionID); + SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &scSNMPCommunity, const std::string &intersectionName, unsigned int intersectionID); /** * @brief Method attempts to send SNMP SET requests to initialize the TSC. NOTE: Some of the diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index e8c308ac0..c5067fc8b 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -38,22 +38,25 @@ namespace SpatPlugin { unsigned int port; std::string signal_controller_ip; unsigned int signal_controller_snmp_port; + std::string signal_controller_snmp_community; std::string intersection_name; unsigned int intersection_id; GetConfigValue("SignalGroupMapping", signal_group_mapping_json, &data_lock); GetConfigValue("Local_IP", ip_address, &data_lock); GetConfigValue("Local_UDP_Port", port, &data_lock); GetConfigValue("TSC_IP", signal_controller_ip, &data_lock); - GetConfigValue("TSC_Remote_SNMP_Port", signal_controller_snmp_port,&data_lock); + GetConfigValue("TSC_SNMP_Port", signal_controller_snmp_port,&data_lock); + GetConfigValue("TSC_SNMP_Community", signal_controller_snmp_community,&data_lock); + GetConfigValue("Intersection_Name", intersection_name,&data_lock); GetConfigValue("Intersection_Id", intersection_id, &data_lock); GetConfigValue("SPAT_Mode", spatMode, &data_lock); if (scConnection) { - scConnection.reset(new SignalControllerConnection(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port, intersection_name, intersection_id)); + scConnection.reset(new SignalControllerConnection(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port, signal_controller_snmp_community ,intersection_name, intersection_id)); } else { - scConnection = std::make_unique(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port, intersection_name, intersection_id); + scConnection = std::make_unique(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port,signal_controller_snmp_community, intersection_name, intersection_id); } // Only enable spat broadcast in simulation mode. TFHRC TSCs do not expose this OID so calls to it will fail in hardware deployment // Only set intersection ID in J2735_HEX SPAT Mode. Is only available for TSCs that support J2735 UPER HEX SPaT broadcast, which are all NTCIP 1202V3 or later. diff --git a/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp index 22d5f7486..ff30ba42a 100644 --- a/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestNTCIP1202.cpp @@ -16,6 +16,7 @@ #include #include #include +#include using namespace fwha_stol::lib::time; diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp index e5b5aae34..e98be9c82 100644 --- a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp @@ -56,7 +56,7 @@ namespace SpatPlugin { ] } )"; - signalControllerConnection = std::make_unique("127.0.0.1", 5000, signalGroupMapping, "", 5020, "someIntersection", 9001); + signalControllerConnection = std::make_unique("127.0.0.1", 5000, signalGroupMapping, "", 5020,"administrator", "someIntersection", 9001); mockSnmpClient = std::make_shared("127.0.0.1", 6045, "administrator", "", "", ""); mockUdpServer = std::make_shared(); signalControllerConnection->scSNMPClient = mockSnmpClient; From b10754863b0e62251770c7a240e41787cd778a98 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jul 2024 12:38:35 -0400 Subject: [PATCH 30/31] Updates --- src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h | 10 ---------- .../src/SignalControllerConnection.cpp | 18 +++++++----------- .../src/SignalControllerConnection.h | 9 ++++++--- src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp | 3 +-- .../test/TestSignalControllerConnection.cpp | 6 +----- 5 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h b/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h index 898792c5a..1d4789f1d 100644 --- a/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h +++ b/src/v2i-hub/SpatPlugin/src/NTCIP1202OIDs.h @@ -17,16 +17,6 @@ #pragma once #include -/** - * @brief Contains OIDs (Object Identifier) described in NTCIP 1202 V3 (National Transporation Communication for ITS Protocol Object - * Definitions for Actuated Signal Controllers (ASC) Interface) used for SPaT Plugin functionality. - */ -namespace NTCIP1202V3{ - /** - * @brief OID for Intersection ID. - */ - static const std::string INTERSECTION_ID = " 1.3.6.1.4.1.1206.4.2.1.17.1.2.1.2"; -} /** * @brief Contains OIDs (Object Identifier) described in NTCIP 1202 V3 (National Transporation Communication for ITS Protocol Object * Definitions for Actuated Signal Controllers (ASC) Interface) used for SPaT Plugin functionality. diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index 431f5980f..bd4166954 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -20,8 +20,9 @@ namespace SpatPlugin { SignalControllerConnection::SignalControllerConnection(const std::string &localIp, unsigned int localPort, const std::string &signalGroupMapping, const std::string &scIp, unsigned int scSNMPPort, const std::string &scSNMPCommunity, const std::string &intersectionName, unsigned int intersectionId) : spatPacketReceiver(std::make_shared(localIp, localPort)) ,scSNMPClient(std::make_shared(scIp, scSNMPPort ,scSNMPCommunity, "", "", "")), signalGroupMapping(signalGroupMapping), intersectionName(intersectionName), intersectionId(intersectionId) { }; - bool SignalControllerConnection::initializeSignalControllerConnection(bool enable_spat, bool set_intersection_id) const { - // TODO : Update to more generic TSC Initialization process that simply follows NTCIP 1202 version guidelines. + bool SignalControllerConnection::initializeSignalControllerConnection(bool enable_spat) const { + // TODO : Update to more generic TSC Initialization process that simply follows NTCIP 1202 version guidelines. Also + // set intersection ID in J2735_HEX SPAT Mode. The HEX payload should include a intersection ID. bool status = true; if (enable_spat) { @@ -32,19 +33,14 @@ namespace SpatPlugin { enable_spat.type = tmx::utils::snmp_response_obj::response_type::INTEGER; status = status && scSNMPClient->process_snmp_request(NTCIP1202V2::ENABLE_SPAT_OID, tmx::utils::request_type::SET, enable_spat); } - if ( set_intersection_id ) { - tmx::utils::snmp_response_obj intersection_id; - intersection_id.val_int = intersectionId; - intersection_id.type = tmx::utils::snmp_response_obj::response_type::INTEGER; - status = status && scSNMPClient->process_snmp_request(NTCIP1202V3::INTERSECTION_ID, tmx::utils::request_type::SET, intersection_id); - } + return status; }; void SignalControllerConnection::receiveBinarySPAT(const std::shared_ptr &spat, uint64_t timeMs ) const { FILE_LOG(tmx::utils::logDEBUG) << "Receiving binary SPAT ..." << std::endl; - char buf[1000]; - auto numBytes = spatPacketReceiver->TimedReceive(buf, 1000, 1000); + char buf[SPAT_BINARY_BUFFER_SIZE]; + auto numBytes = spatPacketReceiver->TimedReceive(buf, SPAT_BINARY_BUFFER_SIZE, UDP_SERVER_TIMEOUT_MS); if (numBytes > 0) { // Convert Binary buffer to SPAT pointer @@ -64,7 +60,7 @@ namespace SpatPlugin { void SignalControllerConnection::receiveUPERSPAT(std::shared_ptr &spatEncoded_ptr) const { FILE_LOG(tmx::utils::logDEBUG1) << "Receiving J2725 HEX SPAT ..." << std::endl; - auto payload = spatPacketReceiver->stringTimedReceive( 1000 ); + auto payload = spatPacketReceiver->stringTimedReceive( UDP_SERVER_TIMEOUT_MS ); auto index = payload.find("Payload="); if ( index != std::string::npos ) { // Retreive hex string payload diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h index f207941a4..8370d8f58 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.h @@ -60,7 +60,11 @@ namespace SpatPlugin { * @brief Numeric identifier for intersection in SPaT messages. */ unsigned int intersectionId; - + + const static unsigned int SPAT_BINARY_BUFFER_SIZE = 1000; + + const static unsigned int UDP_SERVER_TIMEOUT_MS = 1000; + friend class TestSignalControllerConnection; public: @@ -82,10 +86,9 @@ namespace SpatPlugin { * version of the NTCIP 1202 standard they are complaint with. To avoid failures please use * the bool flags to indicate which OIDs need to be set for initialization. * @param enable_spat bool flag on whether to attempt to set enable spat to true (NOT available for NTCIP 1202 versions >= 3 ) - * @param set_intersection_id bool flag on whether to set intersection id (NOT available for NTCIP 1202 versions < 3) * @return true if successful and false if not. */ - bool initializeSignalControllerConnection(bool enable_spat, bool set_intersection_id) const; + bool initializeSignalControllerConnection(bool enable_spat) const; /** * @brief Method to receive SPaT data in binary format from TSC. * @param spat an empty SPaT pointer to which the SPAT data will be written. diff --git a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp index c5067fc8b..1a7b9ca96 100644 --- a/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp +++ b/src/v2i-hub/SpatPlugin/src/SpatPlugin.cpp @@ -59,8 +59,7 @@ namespace SpatPlugin { scConnection = std::make_unique(ip_address, port, signal_group_mapping_json, signal_controller_ip, signal_controller_snmp_port,signal_controller_snmp_community, intersection_name, intersection_id); } // Only enable spat broadcast in simulation mode. TFHRC TSCs do not expose this OID so calls to it will fail in hardware deployment - // Only set intersection ID in J2735_HEX SPAT Mode. Is only available for TSCs that support J2735 UPER HEX SPaT broadcast, which are all NTCIP 1202V3 or later. - auto connected = scConnection->initializeSignalControllerConnection(PluginClientClockAware::isSimulationMode(), this->spatMode == "J2735_HEX"); + auto connected = scConnection->initializeSignalControllerConnection(PluginClientClockAware::isSimulationMode()); if ( connected ) { SetStatus(keyConnectionStatus, "IDLE"); try { diff --git a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp index e98be9c82..a95b53374 100644 --- a/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/test/TestSignalControllerConnection.cpp @@ -95,12 +95,8 @@ namespace SpatPlugin { tmx::utils::snmp_response_obj enable_spat; enable_spat.type = tmx::utils::snmp_response_obj::response_type::INTEGER; enable_spat.val_int = 2; - tmx::utils::snmp_response_obj intersection_id; - intersection_id.type = tmx::utils::snmp_response_obj::response_type::INTEGER; - intersection_id.val_int = 9001; EXPECT_CALL(*mockSnmpClient, process_snmp_request(NTCIP1202V2::ENABLE_SPAT_OID, tmx::utils::request_type::SET, enable_spat)).WillOnce(testing::DoAll(SetArgReferee<2>(enable_spat), Return(true))); - EXPECT_CALL(*mockSnmpClient, process_snmp_request(NTCIP1202V3::INTERSECTION_ID, tmx::utils::request_type::SET, intersection_id)).WillOnce(testing::DoAll(SetArgReferee<2>(enable_spat), Return(true))); - EXPECT_TRUE(signalControllerConnection->initializeSignalControllerConnection(true, true)); + EXPECT_TRUE(signalControllerConnection->initializeSignalControllerConnection(true)); } TEST_F(TestSignalControllerConnection, receiveBinarySPAT) { From b62fe28c2a43769b2959058c45e3ccb162b907a5 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jul 2024 18:01:58 -0400 Subject: [PATCH 31/31] Updates --- src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp index bd4166954..d5579886e 100644 --- a/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp +++ b/src/v2i-hub/SpatPlugin/src/SignalControllerConnection.cpp @@ -28,10 +28,10 @@ namespace SpatPlugin { { // For binary SPAT a value of 2 enables original SPAT binary broadcast on the TSC and a value of 6 enables original SPAT plugin additional Pedestrian Information. // NOTE: Pedestrian information is untested. - tmx::utils::snmp_response_obj enable_spat; - enable_spat.val_int = 2; - enable_spat.type = tmx::utils::snmp_response_obj::response_type::INTEGER; - status = status && scSNMPClient->process_snmp_request(NTCIP1202V2::ENABLE_SPAT_OID, tmx::utils::request_type::SET, enable_spat); + tmx::utils::snmp_response_obj enable_spat_resp; + enable_spat_resp.val_int = 2; + enable_spat_resp.type = tmx::utils::snmp_response_obj::response_type::INTEGER; + status = status && scSNMPClient->process_snmp_request(NTCIP1202V2::ENABLE_SPAT_OID, tmx::utils::request_type::SET, enable_spat_resp); } return status;