From 4c74c61a5001037f3979f6dbe74a72abd2ec4e76 Mon Sep 17 00:00:00 2001 From: Geoff Phillips Date: Thu, 21 Nov 2024 19:40:00 +0100 Subject: [PATCH] Release v1.2.0 --- .github/template/fwe-build/action.yml | 2 +- .github/workflows/ci.yml | 29 +- .pre-commit-config.yaml | 4 + CHANGELOG.md | 35 + CMakeLists.txt | 271 +- README.md | 62 +- THIRD-PARTY-LICENSES | 381 + cmake/AwsIotFweConfig.cmake.in | 4 + cmake/capicxx_gen.cmake | 41 + cmake/capicxx_gen_log4j_config.xml | 13 + configuration/static-config.json | 9 +- docs/custom-data-source.md | 190 - .../adding-custom-fidl-file-dev-guide.md | 193 + docs/dev-guide/can-actuators-dev-guide.md | 533 ++ docs/dev-guide/can-over-someip-demo.md | 418 + docs/dev-guide/custom-function-dev-guide.md | 580 ++ ...ent-dev-guide-device-shadow-over-someip.md | 239 + .../edge-agent-dev-guide-last-known-state.md | 396 + .../edge-agent-dev-guide-nxp-s32g.md | 2 + .../edge-agent-dev-guide-renesas-rcar-s4.md | 2 + docs/dev-guide/edge-agent-dev-guide-someip.md | 681 ++ docs/dev-guide/edge-agent-dev-guide.md | 162 +- .../dev-guide/edge-agent-uds-dtc-dev-guide.md | 572 ++ .../images/can-over-someip-demo-diagram.jpg | Bin 0 -> 108690 bytes .../images/collected_data_plot_someip.png | Bin 0 -> 60597 bytes .../images/last-known-state-diagram.jpg | Bin 0 -> 118879 bytes .../images/snf-forwarding-data-diagram.png | Bin 0 -> 55964 bytes .../images/snf-high-level-diagram.png | Bin 0 -> 37822 bytes .../images/snf-storing-data-diagram.png | Bin 0 -> 26872 bytes .../images/snf-stream-management-diagram.png | Bin 0 -> 26469 bytes .../images/someip-collection-diagram.jpg | Bin 0 -> 118770 bytes .../images/someip-commands-diagram.jpg | Bin 0 -> 109953 bytes .../images/uds-dtc-architecture-uml.png | Bin 0 -> 20282 bytes docs/dev-guide/network-agnostic-dev-guide.md | 301 + docs/dev-guide/store-and-forward-dev-guide.md | 367 + .../vision-system-data-demo.ipynb | 24 +- docs/iwave-g26-tutorial/IWAVE-GPS-CAN.dbc | 47 - docs/iwave-g26-tutorial/iwave-g26-tutorial.md | 77 +- docs/iwave-g26-tutorial/iwave-gps-setup.md | 49 - docs/metrics.md | 6 +- docs/rpi-tutorial/raspberry-pi-tutorial.md | 25 +- .../last_known_state_message.proto | 76 + .../cloudToEdge/collection_schemes.proto | 136 + .../schemas/cloudToEdge/command_request.proto | 149 + .../schemas/cloudToEdge/common_types.proto | 36 +- .../cloudToEdge/decoder_manifest.proto | 32 + .../schemas/cloudToEdge/state_templates.proto | 90 + .../staticConfiguration.json | 302 +- .../edgeToCloud/command_response.proto | 41 + .../edgeToCloud/last_known_state_data.proto | 87 + .../schemas/edgeToCloud/vehicle_data.proto | 5 + .../DeviceShadowOverSomeipInterface.fdepl | 173 + .../fidl/DeviceShadowOverSomeipInterface.fidl | 62 + .../someip/fidl/ExampleSomeipInterface.fdepl | 209 + .../someip/fidl/ExampleSomeipInterface.fidl | 120 + interfaces/uds-dtc/udsDtcSchema.json | 56 + pyproject.toml | 3 + src/AaosVhalSource.cpp | 101 +- src/AaosVhalSource.h | 43 +- src/ActuatorCommandManager.cpp | 295 + src/ActuatorCommandManager.h | 162 + src/AwsGreengrassV2ConnectivityModule.cpp | 19 +- src/AwsGreengrassV2ConnectivityModule.h | 6 +- src/AwsGreengrassV2Sender.cpp | 33 +- src/AwsGreengrassV2Sender.h | 30 +- src/AwsIotConnectivityModule.cpp | 17 +- src/AwsIotConnectivityModule.h | 6 +- src/AwsIotReceiver.cpp | 14 +- src/AwsIotSender.cpp | 52 +- src/AwsIotSender.h | 35 +- src/CANDecoder.cpp | 2 +- src/CacheAndPersist.cpp | 13 + src/CacheAndPersist.h | 12 + src/CanCommandDispatcher.cpp | 452 + src/CanCommandDispatcher.h | 213 + src/CollectionInspectionAPITypes.h | 144 +- src/CollectionInspectionEngine.cpp | 796 +- src/CollectionInspectionEngine.h | 112 +- src/CollectionInspectionWorkerThread.cpp | 92 +- src/CollectionInspectionWorkerThread.h | 15 + src/CollectionSchemeIngestion.cpp | 359 +- src/CollectionSchemeIngestion.h | 44 + src/CollectionSchemeManager.cpp | 162 +- src/CollectionSchemeManager.h | 110 +- src/CommandResponseDataSender.cpp | 201 + src/CommandResponseDataSender.h | 47 + src/CommandSchema.cpp | 290 + src/CommandSchema.h | 99 + src/CommandTypes.h | 121 + src/CustomDataSource.cpp | 219 - src/CustomDataSource.h | 119 - src/CustomFunctionMath.cpp | 194 + src/CustomFunctionMath.h | 34 + src/CustomFunctionMultiRisingEdgeTrigger.cpp | 136 + src/CustomFunctionMultiRisingEdgeTrigger.h | 70 + src/DataFetchManager.cpp | 232 + src/DataFetchManager.h | 89 + src/DataFetchManagerAPITypes.h | 60 + src/DataSenderManagerWorkerThread.h | 4 + src/DataSenderProtoReader.cpp | 114 + src/DataSenderProtoReader.h | 36 + src/DataSenderProtoWriter.cpp | 32 +- src/DataSenderTypes.h | 28 + src/DecoderDictionaryExtractor.cpp | 50 + src/DecoderManifestIngestion.cpp | 59 +- src/DecoderManifestIngestion.h | 9 + src/DeviceShadowOverSomeip.cpp | 213 + src/DeviceShadowOverSomeip.h | 63 + src/ExampleSomeipInterfaceWrapper.h | 375 + src/ExampleUDSInterface.cpp | 366 + src/ExampleUDSInterface.h | 139 + src/ExternalGpsSource.cpp | 62 +- src/ExternalGpsSource.h | 56 +- src/ICollectionScheme.h | 157 + src/ICommandDispatcher.h | 136 + src/IConnectivityModule.h | 12 +- src/IDecoderDictionary.h | 8 + src/IDecoderManifest.h | 56 + src/IRemoteDiagnostics.h | 122 + src/ISender.h | 24 +- src/ISomeipInterfaceWrapper.h | 103 + src/IWaveGpsSource.cpp | 161 +- src/IWaveGpsSource.h | 76 +- src/InspectionMatrixExtractor.cpp | 343 +- src/IoTFleetWiseEngine.cpp | 1001 +- src/IoTFleetWiseEngine.h | 100 +- src/IoTJobsDataRequestHandler.cpp | 878 ++ src/IoTJobsDataRequestHandler.h | 103 + src/LastKnownStateDataSender.cpp | 204 + src/LastKnownStateDataSender.h | 55 + src/LastKnownStateIngestion.cpp | 154 + src/LastKnownStateIngestion.h | 72 + src/LastKnownStateInspector.cpp | 704 ++ src/LastKnownStateInspector.h | 382 + src/LastKnownStateSchema.cpp | 37 + src/LastKnownStateSchema.h | 62 + src/LastKnownStateTypes.h | 79 + src/LastKnownStateWorkerThread.cpp | 293 + src/LastKnownStateWorkerThread.h | 112 + src/NamedSignalDataSource.cpp | 127 + src/NamedSignalDataSource.h | 81 + src/Persistency.cpp | 65 + src/RateLimiter.cpp | 54 + src/RateLimiter.h | 38 + src/RemoteDiagnosticDataSource.cpp | 776 ++ src/RemoteDiagnosticDataSource.h | 285 + src/RemoteProfiler.cpp | 44 +- src/RemoteProfiler.h | 9 +- src/Schema.cpp | 47 +- src/Schema.h | 2 +- src/SignalTypes.h | 20 + src/SomeipCommandDispatcher.cpp | 147 + src/SomeipCommandDispatcher.h | 98 + src/SomeipDataSource.cpp | 167 + src/SomeipDataSource.h | 67 + src/SomeipToCanBridge.cpp | 154 + src/SomeipToCanBridge.h | 71 + src/StoreFileSystem.cpp | 253 + src/StoreFileSystem.h | 77 + src/StoreLogger.cpp | 64 + src/StoreLogger.h | 24 + src/StreamForwarder.cpp | 379 + src/StreamForwarder.h | 103 + src/StreamManager.cpp | 552 ++ src/StreamManager.h | 143 + src/TelemetryDataSender.cpp | 45 +- src/TelemetryDataSender.h | 4 + src/TopicConfig.h | 147 + src/TraceModule.cpp | 54 + src/TraceModule.h | 27 + src/VehicleDataSourceTypes.h | 4 +- src/android_shared_library.cpp | 117 +- test/unit/AaosVhalSourceTest.cpp | 49 +- test/unit/ActuatorCommandManagerTest.cpp | 528 + test/unit/AwsIotConnectivityModuleTest.cpp | 64 +- test/unit/CacheAndPersistTest.cpp | 23 + test/unit/CanCommandDispatcherTest.cpp | 691 ++ test/unit/CollectionInspectionEngineTest.cpp | 3173 ++++-- .../CollectionInspectionWorkerThreadTest.cpp | 106 +- test/unit/CollectionSchemeManagerTest.cpp | 346 + test/unit/CommandSchemaTest.cpp | 593 ++ test/unit/CustomDataSourceTest.cpp | 122 - test/unit/CustomFunctionMathTest.cpp | 157 + ...stomFunctionMultiRisingEdgeTriggerTest.cpp | 250 + test/unit/DataFetchManagerTest.cpp | 191 + test/unit/DataSenderManagerTest.cpp | 650 +- .../DataSenderManagerWorkerThreadTest.cpp | 62 + test/unit/DataSenderProtoReaderTest.cpp | 140 + test/unit/DataSenderProtoWriterTest.cpp | 115 + test/unit/DecoderDictionaryExtractorTest.cpp | 114 +- test/unit/DeviceShadowOverSomeipTest.cpp | 349 + test/unit/ExampleUDSInterfaceTest.cpp | 637 ++ test/unit/ExternalGpsSourceTest.cpp | 81 +- test/unit/IWaveGpsSourceTest.cpp | 107 +- test/unit/InspectionMatrixExtractorTest.cpp | 577 +- test/unit/IoTJobsDataRequestHandlerTest.cpp | 1059 ++ test/unit/LastKnownStateInspectorTest.cpp | 1042 ++ test/unit/LastKnownStateSchemaTest.cpp | 232 + test/unit/LastKnownStateWorkerThreadTest.cpp | 361 + test/unit/NamedSignalDataSourceTest.cpp | 125 + test/unit/PersistencyTest.cpp | 121 + test/unit/RateLimiterTest.cpp | 46 + test/unit/RemoteProfilerTest.cpp | 17 +- test/unit/SchemaTest.cpp | 400 +- test/unit/SomeipCommandDispatcherTest.cpp | 447 + test/unit/SomeipDataSourceTest.cpp | 226 + test/unit/SomeipToCanBridgeTest.cpp | 313 + test/unit/StoreFileSystemTest.cpp | 124 + test/unit/StoreLoggerTest.cpp | 56 + test/unit/StreamForwarderTest.cpp | 513 + test/unit/StreamManagerTest.cpp | 1109 +++ test/unit/TraceModuleTest.cpp | 8 + .../support/CollectionSchemeManagerMock.h | 93 +- .../support/CollectionSchemeManagerTest.h | 137 +- test/unit/support/CommandDispatcherMock.h | 42 + test/unit/support/CommonAPIProxyMock.h | 27 + test/unit/support/ConnectivityModuleMock.h | 6 +- .../support/ExampleSomeipInterfaceProxyMock.h | 111 + test/unit/support/SenderMock.h | 55 +- test/unit/support/SomeipMock.h | 310 + test/unit/support/StreamForwarderMock.h | 30 + test/unit/support/StreamManagerMock.h | 30 + .../support/static-config-inline-creds.json | 11 - test/unit/support/static-config-ok.json | 84 +- tools/android-app/README.md | 66 +- .../app/src/main/assets/config-0.json | 16 +- .../main/java/com/aws/iotfleetwise/Fwe.java | 18 + .../com/aws/iotfleetwise/FweApplication.java | 6 +- tools/android-app/cloud/aaosVhalDecoders.json | 8514 ----------------- .../cloud/campaign-android-aaos-vhal.json | 15 + .../cloud/custom-decoders-aaos-vhal.json | 4866 ++++++++++ ...Nodes.json => custom-nodes-aaos-vhal.json} | 10 +- .../cloud/externalGpsDecoders.json | 30 - tools/android-app/cloud/gen-aaos-vhal-info.py | 25 +- .../network-interface-can-aaos-vhal.json | 11 - .../network-interface-can-external-gps.json | 11 - .../network-interface-custom-aaos-vhal.json | 9 + tools/android-app/cloud/provision.sh | 22 +- tools/build-fwe-cross-android.sh | 41 +- tools/build-fwe-cross-arm64.sh | 58 +- tools/build-fwe-cross-armhf.sh | 60 +- tools/build-fwe-native.sh | 58 +- tools/can-to-someip/.gitignore | 1 + tools/can-to-someip/README.md | 8 + tools/can-to-someip/can-to-someip.cmake | 21 + tools/can-to-someip/can-to-someip.py | 225 + tools/can-to-someip/main.cpp | 247 + tools/cansim/can_command_server.py | 147 + tools/cansim/canigen.py | 141 +- tools/cansim/cansim.py | 20 +- tools/cansim/obd_config.json | 12 +- tools/cfn-templates/fwdemo.yml | 40 +- tools/cfn-templates/fwdev.yml | 37 +- .../vision-system-data-jupyter.yml | 32 +- tools/cloud/.gitignore | 1 + tools/cloud/campaign-math.json | 18 + .../campaign-multi-rising-edge-trigger.json | 15 + .../campaign-obd-and-location-heartbeat.json | 36 + tools/cloud/campaign-someip-heartbeat.json | 23 + .../cloud/campaign-store-only-no-upload.json | 33 + ...ampaign-uds-dtc-condition-based-fetch.json | 29 + .../campaign-uds-dtc-time-based-fetch.json | 31 + ...n-upload-critical-during-hard-braking.json | 34 + tools/cloud/campaign-upload-during-wifi.json | 34 + tools/cloud/clean-up.sh | 66 +- .../cloud/custom-decoders-can-actuators.json | 18 + tools/cloud/custom-decoders-location.json | 18 + ...om-decoders-multi-rising-edge-trigger.json | 10 + tools/cloud/custom-decoders-someip.json | 98 + tools/cloud/custom-decoders-uds-dtc.json | 10 + tools/cloud/custom-nodes-can-actuators.json | 16 + .../custom-nodes-location.json} | 0 ...ustom-nodes-multi-rising-edge-trigger.json | 15 + tools/cloud/custom-nodes-someip.json | 104 + tools/cloud/custom-nodes-uds-dtc.json | 21 + tools/cloud/dbc-to-decoders.py | 2 +- tools/cloud/demo.sh | 175 +- tools/cloud/install-deps.sh | 10 +- tools/cloud/iot-topic-subscribe.py | 97 + tools/cloud/iot-topic-to-html.py | 93 + tools/cloud/lks-subscribe.py | 126 + tools/cloud/manage-service-role.sh | 158 + ...etwork-interface-custom-can-actuators.json | 9 + .../network-interface-custom-location.json | 9 + ...network-interface-custom-named-signal.json | 9 + .../network-interface-custom-someip.json | 9 + .../network-interface-custom-uds-dtc.json | 9 + tools/cloud/nuke-fw.sh | 11 + tools/cloud/request-forward.sh | 333 + tools/cloud/timestream-to-html.py | 2 + tools/configure-fwe.sh | 125 +- tools/install-cansim.sh | 4 +- tools/install-deps-cross-android.sh | 4 +- tools/install-deps-cross-arm64.sh | 120 +- tools/install-deps-cross-armhf.sh | 120 +- tools/install-deps-native.sh | 119 +- tools/install-deps-versions.sh | 6 + tools/install-deps-yocto.sh | 6 +- tools/install-socketcan.sh | 12 +- ...picxx_core_runtime_allow_static_libs.patch | 26 + ...cxx_someip_runtime_allow_static_libs.patch | 68 + ...w_static_libs_fix_shutdown_segfaults.patch | 428 + tools/renesas-rcar-s4/make-rootfs.sh | 16 +- tools/someip_device_shadow_editor/.gitignore | 5 + tools/someip_device_shadow_editor/README.md | 34 + .../commonapi-config.ini | 8 + tools/someip_device_shadow_editor/linker.lds | 4 + .../someip_device_shadow_editor.cmake | 48 + .../someip_device_shadow_editor_repl.py | 71 + .../someip_device_shadow_editor_sim.py | 82 + ...viceShadowOverSomeipExampleApplication.cpp | 199 + ...viceShadowOverSomeipExampleApplication.hpp | 74 + .../src/bindings.cpp | 23 + .../vsomeip-config.json | 14 + tools/someipigen/.gitignore | 6 + tools/someipigen/README.md | 122 + tools/someipigen/commonapi-config.ini | 8 + tools/someipigen/linker.lds | 4 + tools/someipigen/signals.json | 12 + tools/someipigen/someipigen.cmake | 48 + tools/someipigen/someipigen_repl.py | 95 + tools/someipigen/someipsim.py | 39 + .../src/ExampleSomeipInterfaceStubImpl.cpp | 161 + .../src/ExampleSomeipInterfaceStubImpl.hpp | 369 + tools/someipigen/src/SignalManager.cpp | 314 + tools/someipigen/src/SignalManager.hpp | 102 + tools/someipigen/src/bindings.cpp | 129 + tools/someipigen/vsomeip-config.json | 14 + 328 files changed, 47745 insertions(+), 11830 deletions(-) create mode 100644 cmake/capicxx_gen.cmake create mode 100644 cmake/capicxx_gen_log4j_config.xml delete mode 100644 docs/custom-data-source.md create mode 100644 docs/dev-guide/adding-custom-fidl-file-dev-guide.md create mode 100644 docs/dev-guide/can-actuators-dev-guide.md create mode 100644 docs/dev-guide/can-over-someip-demo.md create mode 100644 docs/dev-guide/custom-function-dev-guide.md create mode 100644 docs/dev-guide/edge-agent-dev-guide-device-shadow-over-someip.md create mode 100644 docs/dev-guide/edge-agent-dev-guide-last-known-state.md create mode 100644 docs/dev-guide/edge-agent-dev-guide-someip.md create mode 100644 docs/dev-guide/edge-agent-uds-dtc-dev-guide.md create mode 100644 docs/dev-guide/images/can-over-someip-demo-diagram.jpg create mode 100644 docs/dev-guide/images/collected_data_plot_someip.png create mode 100644 docs/dev-guide/images/last-known-state-diagram.jpg create mode 100644 docs/dev-guide/images/snf-forwarding-data-diagram.png create mode 100644 docs/dev-guide/images/snf-high-level-diagram.png create mode 100644 docs/dev-guide/images/snf-storing-data-diagram.png create mode 100644 docs/dev-guide/images/snf-stream-management-diagram.png create mode 100644 docs/dev-guide/images/someip-collection-diagram.jpg create mode 100644 docs/dev-guide/images/someip-commands-diagram.jpg create mode 100644 docs/dev-guide/images/uds-dtc-architecture-uml.png create mode 100644 docs/dev-guide/network-agnostic-dev-guide.md create mode 100644 docs/dev-guide/store-and-forward-dev-guide.md delete mode 100644 docs/iwave-g26-tutorial/IWAVE-GPS-CAN.dbc delete mode 100644 docs/iwave-g26-tutorial/iwave-gps-setup.md create mode 100644 interfaces/protobuf/schemas/cloudToCustomer/last_known_state_message.proto create mode 100644 interfaces/protobuf/schemas/cloudToEdge/command_request.proto create mode 100644 interfaces/protobuf/schemas/cloudToEdge/state_templates.proto create mode 100644 interfaces/protobuf/schemas/edgeToCloud/command_response.proto create mode 100644 interfaces/protobuf/schemas/edgeToCloud/last_known_state_data.proto create mode 100644 interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fdepl create mode 100644 interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fidl create mode 100644 interfaces/someip/fidl/ExampleSomeipInterface.fdepl create mode 100644 interfaces/someip/fidl/ExampleSomeipInterface.fidl create mode 100644 interfaces/uds-dtc/udsDtcSchema.json create mode 100644 src/ActuatorCommandManager.cpp create mode 100644 src/ActuatorCommandManager.h create mode 100644 src/CanCommandDispatcher.cpp create mode 100644 src/CanCommandDispatcher.h create mode 100644 src/CommandResponseDataSender.cpp create mode 100644 src/CommandResponseDataSender.h create mode 100644 src/CommandSchema.cpp create mode 100644 src/CommandSchema.h create mode 100644 src/CommandTypes.h delete mode 100644 src/CustomDataSource.cpp delete mode 100644 src/CustomDataSource.h create mode 100644 src/CustomFunctionMath.cpp create mode 100644 src/CustomFunctionMath.h create mode 100644 src/CustomFunctionMultiRisingEdgeTrigger.cpp create mode 100644 src/CustomFunctionMultiRisingEdgeTrigger.h create mode 100644 src/DataFetchManager.cpp create mode 100644 src/DataFetchManager.h create mode 100644 src/DataFetchManagerAPITypes.h create mode 100644 src/DataSenderProtoReader.cpp create mode 100644 src/DataSenderProtoReader.h create mode 100644 src/DeviceShadowOverSomeip.cpp create mode 100644 src/DeviceShadowOverSomeip.h create mode 100644 src/ExampleSomeipInterfaceWrapper.h create mode 100644 src/ExampleUDSInterface.cpp create mode 100644 src/ExampleUDSInterface.h create mode 100644 src/ICommandDispatcher.h create mode 100644 src/IRemoteDiagnostics.h create mode 100644 src/ISomeipInterfaceWrapper.h create mode 100644 src/IoTJobsDataRequestHandler.cpp create mode 100644 src/IoTJobsDataRequestHandler.h create mode 100644 src/LastKnownStateDataSender.cpp create mode 100644 src/LastKnownStateDataSender.h create mode 100644 src/LastKnownStateIngestion.cpp create mode 100644 src/LastKnownStateIngestion.h create mode 100644 src/LastKnownStateInspector.cpp create mode 100644 src/LastKnownStateInspector.h create mode 100644 src/LastKnownStateSchema.cpp create mode 100644 src/LastKnownStateSchema.h create mode 100644 src/LastKnownStateTypes.h create mode 100644 src/LastKnownStateWorkerThread.cpp create mode 100644 src/LastKnownStateWorkerThread.h create mode 100644 src/NamedSignalDataSource.cpp create mode 100644 src/NamedSignalDataSource.h create mode 100644 src/RateLimiter.cpp create mode 100644 src/RateLimiter.h create mode 100644 src/RemoteDiagnosticDataSource.cpp create mode 100644 src/RemoteDiagnosticDataSource.h create mode 100644 src/SomeipCommandDispatcher.cpp create mode 100644 src/SomeipCommandDispatcher.h create mode 100644 src/SomeipDataSource.cpp create mode 100644 src/SomeipDataSource.h create mode 100644 src/SomeipToCanBridge.cpp create mode 100644 src/SomeipToCanBridge.h create mode 100644 src/StoreFileSystem.cpp create mode 100644 src/StoreFileSystem.h create mode 100644 src/StoreLogger.cpp create mode 100644 src/StoreLogger.h create mode 100644 src/StreamForwarder.cpp create mode 100644 src/StreamForwarder.h create mode 100644 src/StreamManager.cpp create mode 100644 src/StreamManager.h create mode 100644 src/TopicConfig.h create mode 100644 test/unit/ActuatorCommandManagerTest.cpp create mode 100644 test/unit/CanCommandDispatcherTest.cpp create mode 100644 test/unit/CommandSchemaTest.cpp delete mode 100644 test/unit/CustomDataSourceTest.cpp create mode 100644 test/unit/CustomFunctionMathTest.cpp create mode 100644 test/unit/CustomFunctionMultiRisingEdgeTriggerTest.cpp create mode 100644 test/unit/DataFetchManagerTest.cpp create mode 100644 test/unit/DataSenderProtoReaderTest.cpp create mode 100644 test/unit/DeviceShadowOverSomeipTest.cpp create mode 100644 test/unit/ExampleUDSInterfaceTest.cpp create mode 100644 test/unit/IoTJobsDataRequestHandlerTest.cpp create mode 100644 test/unit/LastKnownStateInspectorTest.cpp create mode 100644 test/unit/LastKnownStateSchemaTest.cpp create mode 100644 test/unit/LastKnownStateWorkerThreadTest.cpp create mode 100644 test/unit/NamedSignalDataSourceTest.cpp create mode 100644 test/unit/RateLimiterTest.cpp create mode 100644 test/unit/SomeipCommandDispatcherTest.cpp create mode 100644 test/unit/SomeipDataSourceTest.cpp create mode 100644 test/unit/SomeipToCanBridgeTest.cpp create mode 100644 test/unit/StoreFileSystemTest.cpp create mode 100644 test/unit/StoreLoggerTest.cpp create mode 100644 test/unit/StreamForwarderTest.cpp create mode 100644 test/unit/StreamManagerTest.cpp create mode 100644 test/unit/support/CommandDispatcherMock.h create mode 100644 test/unit/support/CommonAPIProxyMock.h create mode 100644 test/unit/support/ExampleSomeipInterfaceProxyMock.h create mode 100644 test/unit/support/SomeipMock.h create mode 100644 test/unit/support/StreamForwarderMock.h create mode 100644 test/unit/support/StreamManagerMock.h delete mode 100644 tools/android-app/cloud/aaosVhalDecoders.json create mode 100644 tools/android-app/cloud/custom-decoders-aaos-vhal.json rename tools/android-app/cloud/{aaosVhalNodes.json => custom-nodes-aaos-vhal.json} (99%) delete mode 100644 tools/android-app/cloud/externalGpsDecoders.json delete mode 100644 tools/android-app/cloud/network-interface-can-aaos-vhal.json delete mode 100644 tools/android-app/cloud/network-interface-can-external-gps.json create mode 100644 tools/android-app/cloud/network-interface-custom-aaos-vhal.json create mode 100644 tools/can-to-someip/.gitignore create mode 100644 tools/can-to-someip/README.md create mode 100644 tools/can-to-someip/can-to-someip.cmake create mode 100755 tools/can-to-someip/can-to-someip.py create mode 100644 tools/can-to-someip/main.cpp create mode 100755 tools/cansim/can_command_server.py create mode 100644 tools/cloud/campaign-math.json create mode 100644 tools/cloud/campaign-multi-rising-edge-trigger.json create mode 100644 tools/cloud/campaign-obd-and-location-heartbeat.json create mode 100644 tools/cloud/campaign-someip-heartbeat.json create mode 100644 tools/cloud/campaign-store-only-no-upload.json create mode 100644 tools/cloud/campaign-uds-dtc-condition-based-fetch.json create mode 100644 tools/cloud/campaign-uds-dtc-time-based-fetch.json create mode 100644 tools/cloud/campaign-upload-critical-during-hard-braking.json create mode 100644 tools/cloud/campaign-upload-during-wifi.json create mode 100644 tools/cloud/custom-decoders-can-actuators.json create mode 100644 tools/cloud/custom-decoders-location.json create mode 100644 tools/cloud/custom-decoders-multi-rising-edge-trigger.json create mode 100644 tools/cloud/custom-decoders-someip.json create mode 100644 tools/cloud/custom-decoders-uds-dtc.json create mode 100644 tools/cloud/custom-nodes-can-actuators.json rename tools/{android-app/cloud/externalGpsNodes.json => cloud/custom-nodes-location.json} (100%) create mode 100644 tools/cloud/custom-nodes-multi-rising-edge-trigger.json create mode 100644 tools/cloud/custom-nodes-someip.json create mode 100644 tools/cloud/custom-nodes-uds-dtc.json create mode 100755 tools/cloud/iot-topic-subscribe.py create mode 100755 tools/cloud/iot-topic-to-html.py create mode 100755 tools/cloud/lks-subscribe.py create mode 100755 tools/cloud/manage-service-role.sh create mode 100644 tools/cloud/network-interface-custom-can-actuators.json create mode 100644 tools/cloud/network-interface-custom-location.json create mode 100644 tools/cloud/network-interface-custom-named-signal.json create mode 100644 tools/cloud/network-interface-custom-someip.json create mode 100644 tools/cloud/network-interface-custom-uds-dtc.json create mode 100755 tools/cloud/request-forward.sh create mode 100644 tools/patches/capicxx_core_runtime_allow_static_libs.patch create mode 100644 tools/patches/capicxx_someip_runtime_allow_static_libs.patch create mode 100644 tools/patches/vsomeip_allow_static_libs_fix_shutdown_segfaults.patch create mode 100644 tools/someip_device_shadow_editor/.gitignore create mode 100644 tools/someip_device_shadow_editor/README.md create mode 100644 tools/someip_device_shadow_editor/commonapi-config.ini create mode 100644 tools/someip_device_shadow_editor/linker.lds create mode 100644 tools/someip_device_shadow_editor/someip_device_shadow_editor.cmake create mode 100755 tools/someip_device_shadow_editor/someip_device_shadow_editor_repl.py create mode 100755 tools/someip_device_shadow_editor/someip_device_shadow_editor_sim.py create mode 100644 tools/someip_device_shadow_editor/src/DeviceShadowOverSomeipExampleApplication.cpp create mode 100644 tools/someip_device_shadow_editor/src/DeviceShadowOverSomeipExampleApplication.hpp create mode 100644 tools/someip_device_shadow_editor/src/bindings.cpp create mode 100644 tools/someip_device_shadow_editor/vsomeip-config.json create mode 100644 tools/someipigen/.gitignore create mode 100644 tools/someipigen/README.md create mode 100644 tools/someipigen/commonapi-config.ini create mode 100644 tools/someipigen/linker.lds create mode 100644 tools/someipigen/signals.json create mode 100644 tools/someipigen/someipigen.cmake create mode 100755 tools/someipigen/someipigen_repl.py create mode 100755 tools/someipigen/someipsim.py create mode 100644 tools/someipigen/src/ExampleSomeipInterfaceStubImpl.cpp create mode 100644 tools/someipigen/src/ExampleSomeipInterfaceStubImpl.hpp create mode 100644 tools/someipigen/src/SignalManager.cpp create mode 100644 tools/someipigen/src/SignalManager.hpp create mode 100644 tools/someipigen/src/bindings.cpp create mode 100644 tools/someipigen/vsomeip-config.json diff --git a/.github/template/fwe-build/action.yml b/.github/template/fwe-build/action.yml index fc178740..fc8cd744 100644 --- a/.github/template/fwe-build/action.yml +++ b/.github/template/fwe-build/action.yml @@ -62,7 +62,7 @@ runs: shell: bash if: inputs.build-arch == 'native' run: | - ./tools/test-fwe.sh ${{ inputs.extra-options }} --extra-gtest-filter "-*CANDataSourceTest*:*ISOTPOverCANProtocolTest*:*IoTFleetWiseEngineTest*:*OBDOverCANModuleTest*" + ./tools/test-fwe.sh ${{ inputs.extra-options }} --extra-gtest-filter "-*CANDataSourceTest*:*ISOTPOverCANProtocolTest*:*IoTFleetWiseEngineTest*:*OBDOverCANModuleTest*:*CanCommandDispatcherTest*:*UDSTemplateInterfaceTest*" - name: upload-artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59a7949d..51c18cc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,9 @@ jobs: with: build-arch: "native" upload-arch: "amd64" - extra-options: "--with-greengrassv2-support" + extra-options: + "--with-someip-support --with-lks-support --with-greengrassv2-support + --with-custom-function-examples --with-store-and-forward-support --with-uds-dtc-example" dist-name: "aws-iot-fleetwise-edge" cache-paths: /usr/local/x86_64-linux-gnu dist-files: build/aws-iot-fleetwise-edge:. @@ -50,7 +52,9 @@ jobs: with: build-arch: "cross-arm64" upload-arch: "arm64" - extra-options: "--with-greengrassv2-support" + extra-options: + "--with-greengrassv2-support --with-someip-support --with-lks-support + --with-custom-function-examples --with-store-and-forward-support --with-uds-dtc-example" dist-name: "aws-iot-fleetwise-edge" cache-paths: /usr/local/aarch64-linux-gnu:/usr/local/x86_64-linux-gnu dist-files: build/aws-iot-fleetwise-edge:. @@ -63,7 +67,10 @@ jobs: with: build-arch: "cross-armhf" upload-arch: "armhf" - extra-options: "--with-greengrassv2-support --with-iwave-gps-support" + extra-options: + "--with-greengrassv2-support --with-someip-support --with-lks-support + --with-iwave-gps-support --with-custom-function-examples + --with-store-and-forward-support --with-uds-dtc-example" dist-name: "aws-iot-fleetwise-edge" cache-paths: /usr/local/arm-linux-gnueabihf:/usr/local/x86_64-linux-gnu dist-files: build/aws-iot-fleetwise-edge:. @@ -76,7 +83,10 @@ jobs: with: build-arch: "native" upload-arch: "amd64" - extra-options: "--with-greengrassv2-support --with-ros2-support" + extra-options: + "--with-greengrassv2-support --with-ros2-support --with-someip-support + --with-lks-support --with-custom-function-examples --with-store-and-forward-support + --with-uds-dtc-example" dist-name: "aws-iot-fleetwise-edge-ros2" cache-paths: /usr/local/x86_64-linux-gnu:/opt/ros dist-files: build/iotfleetwise/aws-iot-fleetwise-edge:. @@ -89,7 +99,10 @@ jobs: with: build-arch: "cross-arm64" upload-arch: "arm64" - extra-options: "--with-greengrassv2-support --with-ros2-support" + extra-options: + "--with-greengrassv2-support --with-ros2-support --with-someip-support + --with-lks-support --with-custom-function-examples --with-store-and-forward-support + --with-uds-dtc-example" dist-name: "aws-iot-fleetwise-edge-ros2" cache-paths: /usr/local/aarch64-linux-gnu:/usr/local/x86_64-linux-gnu:/opt/ros dist-files: build/iotfleetwise/aws-iot-fleetwise-edge:. @@ -102,7 +115,10 @@ jobs: with: build-arch: "cross-armhf" upload-arch: "armhf" - extra-options: "--with-greengrassv2-support --with-ros2-support" + extra-options: + "--with-greengrassv2-support --with-ros2-support --with-someip-support + --with-lks-support --with-custom-function-examples --with-store-and-forward-support + --with-uds-dtc-example" dist-name: "aws-iot-fleetwise-edge-ros2" cache-paths: /usr/local/arm-linux-gnueabihf:/usr/local/x86_64-linux-gnu:/opt/ros dist-files: build/iotfleetwise/aws-iot-fleetwise-edge:. @@ -115,6 +131,7 @@ jobs: with: build-arch: "cross-android" upload-arch: "android" + extra-options: "--with-lks-support --with-custom-function-examples" dist-name: "aws-iot-fleetwise-edge" cache-paths: /usr/local/x86_64-linux-android:/usr/local/aarch64-linux-android:/usr/local/armv7a-linux-androideabi:/usr/local/x86_64-linux-gnu dist-files: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2af869d9..d0011186 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -167,3 +167,7 @@ repos: types_or: [c++, python, yaml, cmake, proto, shell] exclude: "gradlew|.prettierrc.yaml|.clang-format|.clang-tidy|create_avd_config.sh" verbose: true + - repo: https://github.com/AlexanderDokuchaev/md-dead-link-check + rev: "v0.9" + hooks: + - id: md-dead-link-check diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2b977d..0bba1e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Change Log +## v1.2.0 (2024-11-21) + +New features: + +- Remote commands for actuators, see the + [CAN actuators guide](./docs/dev-guide/can-actuators-dev-guide.md) and the + [SOME/IP guide](./docs/dev-guide/edge-agent-dev-guide-someip.md). +- [Network agnostic data collection and actuator commands](./docs/dev-guide/network-agnostic-dev-guide.md). +- SOME/IP support, see the + [SOME/IP guide for data collection and commands](./docs/dev-guide/edge-agent-dev-guide-someip.md), + the + [Device shadow proxy for SOME/IP guide](./docs/dev-guide/edge-agent-dev-guide-device-shadow-over-someip.md), + and the [CAN over SOME/IP guide](./docs/dev-guide/can-over-someip-demo.md). +- String datatype support for both sensor data collection and actuator commands. +- [Last Known State (LKS)](./docs/dev-guide/edge-agent-dev-guide-last-known-state.md), a lighter + method of data collection. +- [Custom functions in expressions](./docs/dev-guide/custom-function-dev-guide.md). +- [Store and forward](./docs/dev-guide/store-and-forward-dev-guide.md), for conditional upload of + collected data. +- [UDS DTC data collection](./docs/dev-guide/edge-agent-uds-dtc-dev-guide.md). + +Breaking changes: + +- The `collectionSchemeListTopic`, `decoderManifestTopic`, `canDataTopic` and `checkinTopic` config + fields are deprecated. If a config file has any of them, they will be ignored. Now FWE defaults to + AWS reserved topics without needing any additional config. If you still need to customize the + topics you can set the prefix using the new `iotFleetWiseTopicPrefix` field. +- **Only affects existing iWave and Android instances**: Replaced the CAN-based `CustomDataSource` + implementation with the new + [Network agnostic data collection](./docs/dev-guide/network-agnostic-dev-guide.md) approach for + custom data sources. Existing iWave and Android instances will need to be re-configured and have + changes made to their decoder manifest to use this new version. + - For iWave devices, see the [iWave guide](./docs/iwave-g26-tutorial/iwave-g26-tutorial.md). + - For Android (including AAOS), see the [Android guide](./tools/android-app/README.md). + ## v1.1.2 (2024-10-29) Bug fixes: diff --git a/CMakeLists.txt b/CMakeLists.txt index aaffc684..100989c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10.2) -project(iotfleetwise VERSION 1.1.2) +project(iotfleetwise VERSION 1.2.0) # FWE uses C++14 for compatibility reasons with Automotive middlewares (Adaptive AUTOSAR, ROS2) # Note: When built with FWE_FEATURE_ROS2, colcon will override these settings @@ -25,25 +25,30 @@ option(FWE_SECURITY_COMPILE_FLAGS "Add security related compile options" OFF) option(FWE_AWS_SDK_SHARED_LIBS "Use AWS SDK shared libs. Needs to be set to the same value of BUILD_SHARED_LIBS that the SDK was compiled with." OFF) option(FWE_AWS_SDK_EXTRA_LIBS "Extra libs required to link with the AWS SDK. When FWE_STATIC_LINK is ON, setting this to ON will automatically find the standard libs. Can be a space-separated list of libs." ON) option(FWE_FEATURE_GREENGRASSV2 "Enable Greengrass connection module" OFF) -option(FWE_FEATURE_CUSTOM_DATA_SOURCE "Include the custom data source interface, which uses CAN signals to model arbitary signal sources" OFF) -option(FWE_FEATURE_IWAVE_GPS "Include the IWave GPS example for a custom data source (implies FWE_FEATURE_CUSTOM_DATA_SOURCE)" OFF) -option(FWE_FEATURE_EXTERNAL_GPS "Include the external GPS example for a custom data source (implies FWE_FEATURE_CUSTOM_DATA_SOURCE)" OFF) -option(FWE_FEATURE_AAOS_VHAL "Include the Android Automotive VHAL example for a custom data source (implies FWE_FEATURE_CUSTOM_DATA_SOURCE)" OFF) +option(FWE_FEATURE_IWAVE_GPS "Include the IWave GPS example" OFF) +option(FWE_FEATURE_EXTERNAL_GPS "Include the external GPS example" OFF) +option(FWE_FEATURE_AAOS_VHAL "Include the Android Automotive VHAL example" OFF) option(FWE_FEATURE_S3 "Internal option, not useful on its own. Enables usage of AWS credential provider, and uploading and downloading from S3." OFF) option(FWE_FEATURE_VISION_SYSTEM_DATA "Include support for vision-system-data sources. Implies FWE_FEATURE_S3." OFF) option(FWE_FEATURE_ROS2 "Include support for ROS2 as a vision-system-data source. Implies FWE_FEATURE_VISION_SYSTEM_DATA." OFF) +option(FWE_FEATURE_SOMEIP "Include the example SOME/IP data source, the example SOME/IP to CAN bridge, the example SOME/IP command dispatcher, and the example SOME/IP to device shadow proxy. Implies FWE_FEATURE_REMOTE_COMMANDS." OFF) +option(FWE_FEATURE_REMOTE_COMMANDS "Include support for remote commands" OFF) +option(FWE_FEATURE_LAST_KNOWN_STATE "Include support for last known state. Implies FWE_FEATURE_REMOTE_COMMANDS." OFF) +option(FWE_FEATURE_STORE_AND_FORWARD "Include support for store and forward" OFF) +option(FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES "Include the custom function examples" OFF) option(FWE_BUILD_EXECUTABLE "Build the executable, otherwise build a library" ON) option(FWE_BUILD_ANDROID_SHARED_LIBRARY "Build the android shared library" OFF) +option(FWE_BUILD_TOOLS "Build helper tools" ON) +option(FWE_FEATURE_UDS_DTC "Enable UDS DTC information collection" OFF) +option(FWE_FEATURE_UDS_DTC_EXAMPLE "Include example UDS DTC interface for collecting UDS DTC information. Implies FWE_FEATURE_UDS_DTC." OFF) + if(FWE_FEATURE_IWAVE_GPS) - set(FWE_FEATURE_CUSTOM_DATA_SOURCE ON FORCE) add_compile_options("-DFWE_FEATURE_IWAVE_GPS") endif() if(FWE_FEATURE_EXTERNAL_GPS) - set(FWE_FEATURE_CUSTOM_DATA_SOURCE ON FORCE) add_compile_options("-DFWE_FEATURE_EXTERNAL_GPS") endif() if(FWE_FEATURE_AAOS_VHAL) - set(FWE_FEATURE_CUSTOM_DATA_SOURCE ON FORCE) add_compile_options("-DFWE_FEATURE_AAOS_VHAL") endif() if(FWE_FEATURE_GREENGRASSV2) @@ -61,6 +66,33 @@ if(FWE_FEATURE_VISION_SYSTEM_DATA) set(FWE_FEATURE_S3 ON FORCE) add_compile_options("-DFWE_FEATURE_VISION_SYSTEM_DATA;-DDECNUMDIGITS=34") endif() +if(FWE_FEATURE_SOMEIP) + set(FWE_FEATURE_REMOTE_COMMANDS ON FORCE) + add_compile_options("-DFWE_FEATURE_SOMEIP") +endif() +if(FWE_FEATURE_LAST_KNOWN_STATE) + set(FWE_FEATURE_REMOTE_COMMANDS ON FORCE) + add_compile_options("-DFWE_FEATURE_LAST_KNOWN_STATE") +endif() +if(FWE_FEATURE_REMOTE_COMMANDS) + add_compile_options("-DFWE_FEATURE_REMOTE_COMMANDS") +endif() +if(FWE_FEATURE_STORE_AND_FORWARD) + find_path(STORE_LIBRARY_CPP_INCLUDE_DIR "stream/stream.hpp" PATH_SUFFIXES "aws/store") + find_library(STORE_LIBRARY_CPP_STREAM NAMES stream) + find_library(STORE_LIBRARY_CPP_KV NAMES kv) + add_compile_options("-DFWE_FEATURE_STORE_AND_FORWARD") +endif() +if(FWE_FEATURE_UDS_DTC_EXAMPLE) + set(FWE_FEATURE_UDS_DTC ON FORCE) + add_compile_options("-DFWE_FEATURE_UDS_DTC_EXAMPLE") +endif() +if(FWE_FEATURE_UDS_DTC) + add_compile_options("-DFWE_FEATURE_UDS_DTC") +endif() +if(FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES) + add_compile_options("-DFWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES") +endif() if(FWE_FEATURE_S3) add_compile_options("-DFWE_FEATURE_S3") endif() @@ -82,6 +114,9 @@ if(BUILD_TESTING) include(cmake/valgrind.cmake) include(cmake/clang_tidy.cmake) endif() +if(FWE_FEATURE_SOMEIP) + include(cmake/capicxx_gen.cmake) +endif() # Disallow cycles set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_NO_CYCLES ON) @@ -107,6 +142,19 @@ set(PROTO_FILES interfaces/protobuf/schemas/edgeToCloud/checkin.proto interfaces/protobuf/schemas/edgeToCloud/vehicle_data.proto ) +if(FWE_FEATURE_REMOTE_COMMANDS) + set(PROTO_FILES ${PROTO_FILES} + interfaces/protobuf/schemas/cloudToEdge/command_request.proto + interfaces/protobuf/schemas/edgeToCloud/command_response.proto + ) +endif() +if(FWE_FEATURE_LAST_KNOWN_STATE) + set(PROTO_FILES ${PROTO_FILES} + interfaces/protobuf/schemas/cloudToEdge/state_templates.proto + interfaces/protobuf/schemas/edgeToCloud/last_known_state_data.proto + ) +endif() + protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES}) add_library(fwe-proto OBJECT ${PROTO_SRCS} @@ -118,6 +166,49 @@ target_include_directories(fwe-proto PUBLIC # Protobuf does not support -Wall https://github.com/protocolbuffers/protobuf/issues/6781 set_source_files_properties(${PROTO_SRCS} PROPERTIES COMPILE_FLAGS "-Wno-conversion -Wno-pedantic") +if(FWE_FEATURE_SOMEIP) + # CommonAPI code generation + set(CAPICXX_EXAMPLE_GENERATED_FILES + v1/commonapi/CommonTypes.hpp + v1/commonapi/CommonTypesSomeIPDeployment.cpp + v1/commonapi/CommonTypesSomeIPDeployment.hpp + v1/commonapi/ExampleSomeipInterface.hpp + v1/commonapi/ExampleSomeipInterfaceProxy.hpp + v1/commonapi/ExampleSomeipInterfaceProxyBase.hpp + v1/commonapi/ExampleSomeipInterfaceSomeIPDeployment.cpp + v1/commonapi/ExampleSomeipInterfaceSomeIPDeployment.hpp + v1/commonapi/ExampleSomeipInterfaceSomeIPProxy.cpp + v1/commonapi/ExampleSomeipInterfaceSomeIPProxy.hpp + v1/commonapi/ExampleSomeipInterfaceSomeIPStubAdapter.cpp + v1/commonapi/ExampleSomeipInterfaceSomeIPStubAdapter.hpp + v1/commonapi/ExampleSomeipInterfaceStub.hpp + v1/commonapi/ExampleSomeipInterfaceStubDefault.hpp + ) + capicxx_generate_someip( + interfaces/someip/fidl/ExampleSomeipInterface.fidl + interfaces/someip/fidl/ExampleSomeipInterface.fdepl + "${CAPICXX_EXAMPLE_GENERATED_FILES}" + ) + set(CAPICXX_DEVICE_SHADOW_GENERATED_FILES + v1/commonapi/DeviceShadowOverSomeipInterface.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceProxy.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceProxyBase.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceSomeIPDeployment.cpp + v1/commonapi/DeviceShadowOverSomeipInterfaceSomeIPDeployment.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceSomeIPProxy.cpp + v1/commonapi/DeviceShadowOverSomeipInterfaceSomeIPProxy.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceSomeIPStubAdapter.cpp + v1/commonapi/DeviceShadowOverSomeipInterfaceSomeIPStubAdapter.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceStub.hpp + v1/commonapi/DeviceShadowOverSomeipInterfaceStubDefault.hpp + ) + capicxx_generate_someip( + interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fidl + interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fdepl + "${CAPICXX_DEVICE_SHADOW_GENERATED_FILES}" + ) +endif() + set(HEADER_FILES src/Assert.h src/AwsBootstrap.h @@ -142,6 +233,8 @@ set(HEADER_FILES src/CollectionSchemeManager.h src/ConsoleLogger.h src/CPUUsageInfo.h + src/DataFetchManagerAPITypes.h + src/DataFetchManager.h src/DataSenderManager.h src/DataSenderManagerWorkerThread.h src/DataSenderProtoWriter.h @@ -173,6 +266,7 @@ set(HEADER_FILES src/MemoryUsageInfo.h src/MessageTypes.h src/MqttClientWrapper.h + src/NamedSignalDataSource.h src/OBDDataDecoder.h src/OBDDataTypes.h src/OBDOverCANECU.h @@ -192,6 +286,7 @@ set(HEADER_FILES src/Timer.h src/TimeTypes.h src/TraceModule.h + src/TopicConfig.h src/VehicleDataSourceTypes.h ) @@ -229,6 +324,7 @@ set(SRC_FILES src/ISOTPOverCANSenderReceiver.cpp src/LoggingModule.cpp src/MemoryUsageInfo.cpp + src/NamedSignalDataSource.cpp src/OBDDataDecoder.cpp src/OBDOverCANECU.cpp src/OBDOverCANModule.cpp @@ -239,6 +335,7 @@ set(SRC_FILES src/RetryThread.cpp src/Schema.cpp src/TelemetryDataSender.cpp + src/DataFetchManager.cpp src/Thread.cpp src/TraceModule.cpp ${CMAKE_CURRENT_BINARY_DIR}/IoTFleetWiseVersion.cpp @@ -259,6 +356,7 @@ set(TEST_FILES test/unit/CPUUsageInfoTest.cpp test/unit/DataSenderManagerTest.cpp test/unit/DataSenderManagerWorkerThreadTest.cpp + test/unit/DataFetchManagerTest.cpp test/unit/DataSenderProtoWriterTest.cpp test/unit/DecoderDictionaryExtractorTest.cpp test/unit/ExternalCANDataSourceTest.cpp @@ -268,6 +366,7 @@ set(TEST_FILES test/unit/ISOTPOverCANProtocolTest.cpp test/unit/LoggingModuleTest.cpp test/unit/MemoryUsageInfoTest.cpp + test/unit/NamedSignalDataSourceTest.cpp test/unit/OBDDataDecoderTest.cpp test/unit/OBDOverCANModuleTest.cpp test/unit/PayloadManagerTest.cpp @@ -301,11 +400,6 @@ if(FWE_FEATURE_AAOS_VHAL) set(TEST_FILES ${TEST_FILES} test/unit/AaosVhalSourceTest.cpp) set(HEADER_FILES ${HEADER_FILES} src/AaosVhalSource.h) endif() -if(FWE_FEATURE_CUSTOM_DATA_SOURCE) - set(SRC_FILES ${SRC_FILES} src/CustomDataSource.cpp) - set(TEST_FILES ${TEST_FILES} test/unit/CustomDataSourceTest.cpp) - set(HEADER_FILES ${HEADER_FILES} src/CustomDataSource.h) -endif() if(FWE_FEATURE_GREENGRASSV2) set(SRC_FILES ${SRC_FILES} src/AwsGreengrassV2ConnectivityModule.cpp @@ -346,12 +440,135 @@ if(FWE_FEATURE_ROS2) set(SRC_FILES ${SRC_FILES} src/ROS2DataSource.cpp) set(HEADER_FILES ${HEADER_FILES} src/ROS2DataSource.h) endif() +if(FWE_FEATURE_SOMEIP) + set(SRC_FILES ${SRC_FILES} + src/DeviceShadowOverSomeip.cpp + src/SomeipCommandDispatcher.cpp + src/SomeipDataSource.cpp + src/SomeipToCanBridge.cpp + ${CAPICXX_EXAMPLE_GENERATED_FILES} + ${CAPICXX_DEVICE_SHADOW_GENERATED_FILES} + ) + set(HEADER_FILES ${HEADER_FILES} + src/DeviceShadowOverSomeip.h + src/ExampleSomeipInterfaceWrapper.h + src/ISomeipInterfaceWrapper.h + src/SomeipCommandDispatcher.h + src/SomeipDataSource.h + src/SomeipToCanBridge.h + ) + set(TEST_FILES ${TEST_FILES} + test/unit/DeviceShadowOverSomeipTest.cpp + test/unit/SomeipCommandDispatcherTest.cpp + test/unit/SomeipDataSourceTest.cpp + test/unit/SomeipToCanBridgeTest.cpp + ) +endif() +if(FWE_FEATURE_REMOTE_COMMANDS) + set(SRC_FILES ${SRC_FILES} + src/ActuatorCommandManager.cpp + src/CanCommandDispatcher.cpp + src/CommandResponseDataSender.cpp + src/CommandSchema.cpp + ) + set(HEADER_FILES ${HEADER_FILES} + src/ActuatorCommandManager.h + src/CanCommandDispatcher.h + src/CommandResponseDataSender.h + src/CommandSchema.h + src/CommandTypes.h + src/ICommandDispatcher.h + ) + set(TEST_FILES ${TEST_FILES} + test/unit/ActuatorCommandManagerTest.cpp + test/unit/CanCommandDispatcherTest.cpp + test/unit/CommandSchemaTest.cpp + ) +endif() +if(FWE_FEATURE_LAST_KNOWN_STATE) + set(SRC_FILES ${SRC_FILES} + src/LastKnownStateDataSender.cpp + src/LastKnownStateIngestion.cpp + src/LastKnownStateInspector.cpp + src/LastKnownStateSchema.cpp + src/LastKnownStateWorkerThread.cpp + ) + set(HEADER_FILES ${HEADER_FILES} + src/LastKnownStateDataSender.h + src/LastKnownStateIngestion.h + src/LastKnownStateInspector.h + src/LastKnownStateSchema.h + src/LastKnownStateTypes.h + src/LastKnownStateWorkerThread.h + ) + set(TEST_FILES ${TEST_FILES} + test/unit/LastKnownStateSchemaTest.cpp + test/unit/LastKnownStateWorkerThreadTest.cpp + test/unit/LastKnownStateInspectorTest.cpp + ) +endif() if(FWE_TEST_FAKETIME) set(FAKETIME_TEST_FILES test/unit/FakeSystemTimeTest.cpp) endif() +if(FWE_FEATURE_STORE_AND_FORWARD) + set(HEADER_FILES ${HEADER_FILES} + src/RateLimiter.h + src/StoreFileSystem.h + src/StoreLogger.h + src/StreamManager.h + src/StreamForwarder.h + src/DataSenderProtoReader.h + src/IoTJobsDataRequestHandler.h) + set(SRC_FILES ${SRC_FILES} + src/RateLimiter.cpp + src/StoreFileSystem.cpp + src/StoreLogger.cpp + src/StreamManager.cpp + src/StreamForwarder.cpp + src/DataSenderProtoReader.cpp + src/IoTJobsDataRequestHandler.cpp) + set(TEST_FILES ${TEST_FILES} + test/unit/RateLimiterTest.cpp + test/unit/StoreFileSystemTest.cpp + test/unit/StoreLoggerTest.cpp + test/unit/StreamManagerTest.cpp + test/unit/StreamForwarderTest.cpp + test/unit/DataSenderProtoReaderTest.cpp + test/unit/IoTJobsDataRequestHandlerTest.cpp) +endif() +if (FWE_FEATURE_UDS_DTC_EXAMPLE) + set(SRC_FILES ${SRC_FILES} + src/ExampleUDSInterface.cpp) + set(HEADER_FILES ${HEADER_FILES} + src/ExampleUDSInterface.h) + set(TEST_FILES ${TEST_FILES} + test/unit/ExampleUDSInterfaceTest.cpp) +endif() +if(FWE_FEATURE_UDS_DTC) + set(SRC_FILES ${SRC_FILES} + src/RemoteDiagnosticDataSource.cpp) + set(HEADER_FILES ${HEADER_FILES} + src/RemoteDiagnosticDataSource.h + src/IRemoteDiagnostics.h) +endif() +if(FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES) + set(SRC_FILES ${SRC_FILES} + src/CustomFunctionMath.cpp + src/CustomFunctionMultiRisingEdgeTrigger.cpp) + set(HEADER_FILES ${HEADER_FILES} + src/CustomFunctionMath.h + src/CustomFunctionMultiRisingEdgeTrigger.h) + set(TEST_FILES ${TEST_FILES} + test/unit/CustomFunctionMathTest.cpp + test/unit/CustomFunctionMultiRisingEdgeTriggerTest.cpp) +endif() # Dependencies set(REQUIRED_BOOST_COMPONENTS "thread;filesystem") +if(FWE_FEATURE_SOMEIP) + add_compile_options("-DBOOST_UUID_NO_SIMD") # For clang compatibility + set(REQUIRED_BOOST_COMPONENTS "${REQUIRED_BOOST_COMPONENTS};system;program_options") +endif() find_package(Boost 1.71.0 REQUIRED COMPONENTS ${REQUIRED_BOOST_COMPONENTS}) find_path(JSONCPP_INCLUDE_DIR "json/json.h" PATH_SUFFIXES "jsoncpp") @@ -396,6 +613,15 @@ if(FWE_FEATURE_GREENGRASSV2) find_package(GreengrassIpc-cpp REQUIRED) endif() +if(FWE_FEATURE_SOMEIP) + find_package(vsomeip3 REQUIRED) + find_package(CommonAPI REQUIRED) + find_package(CommonAPI-SomeIP REQUIRED) + # For someipigen: + find_package(Python3 REQUIRED COMPONENTS Interpreter Development) + find_package(pybind11 CONFIG REQUIRED) +endif() + # Object lib used in output binary and unit tests add_library(fwe OBJECT ${SRC_FILES} @@ -406,13 +632,17 @@ target_include_directories(fwe PUBLIC ${JSONCPP_INCLUDE_DIR} ${SNAPPY_INCLUDE_DIR} ${Protobuf_INCLUDE_DIRS} + $<$:${VSOMEIP_INCLUDE_DIRS} ${COMMONAPI_INCLUDE_DIRS} ${COMMONAPI_SOMEIP_INCLUDE_DIRS}> $ $ + $<$:${STORE_LIBRARY_CPP_INCLUDE_DIR}> ) # Link libraries target_link_libraries(fwe $<$:AWS::GreengrassIpc-cpp> + $<$:${STORE_LIBRARY_CPP_STREAM}> + $<$:${STORE_LIBRARY_CPP_KV}> ${AWSSDK_LINK_LIBRARIES} ${FWE_AWS_SDK_EXTRA_LIBS} ${SNAPPY_LIBRARY} @@ -421,6 +651,10 @@ target_link_libraries(fwe Boost::thread Boost::filesystem $<$:fastcdr> + $<$:CommonAPI-SomeIP> + $<$:CommonAPI> + $<$:${VSOMEIP_LIBRARIES}> + $<$:Boost::system> ) if(FWE_FEATURE_VISION_SYSTEM_DATA) @@ -504,7 +738,6 @@ if(${BUILD_TESTING}) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test/unit/support/static-config-ok.json DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test/unit/support/static-config-corrupt.json DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test/unit/support/static-config-inline-creds.json DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) - find_package(GTest REQUIRED) find_library(GMOCK_LIB NAMES gmock) find_package(benchmark REQUIRED) @@ -514,6 +747,7 @@ if(${BUILD_TESTING}) ${TEST_FILES} test/unit/support/main.cpp ) + target_include_directories(fwe-gtest PUBLIC src test/unit/support @@ -596,3 +830,12 @@ if(${BUILD_TESTING}) add_valgrind_test(ROS2DataSourceTest) endif() endif() + +# Tools +if(FWE_BUILD_TOOLS) + if(FWE_FEATURE_SOMEIP) + include(tools/can-to-someip/can-to-someip.cmake) + include(tools/someipigen/someipigen.cmake) + include(tools/someip_device_shadow_editor/someip_device_shadow_editor.cmake) + endif() +endif() diff --git a/README.md b/README.md index 916b592b..787e3357 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,35 @@ # Reference Implementation for AWS IoT FleetWise -:robot: AWS IoT FleetWise now supports [ROS2](https://docs.ros.org) for collecting vision system -data.
:information_source: To quickly get started, jump to the -**[Jupyter Notebook demo](./docs/dev-guide/vision-system-data/vision-system-data-demo.ipynb)** and -collect camera data from a [CARLA](https://carla.org) vehicle simulation. - -:information_source: To quickly get started with telematics data collection, jump to the -[Edge Agent Developer Guide](./docs/dev-guide/edge-agent-dev-guide.md), the -[Android Guide](./tools/android-app/README.md), or the -[Raspberry Pi Tutorial](./docs/rpi-tutorial/raspberry-pi-tutorial.md). +**AWS IoT FleetWise now supports:** + +- :mechanical_arm: Remote commands for actuators, see the + [CAN actuators guide](./docs/dev-guide/can-actuators-dev-guide.md) and the + [SOME/IP guide](./docs/dev-guide/edge-agent-dev-guide-someip.md). +- :electric_plug: + [Network agnostic data collection and actuator commands](./docs/dev-guide/network-agnostic-dev-guide.md). +- :red_car: SOME/IP support, see: + - the + [SOME/IP guide for data collection and commands](./docs/dev-guide/edge-agent-dev-guide-someip.md), + - the + [Device shadow proxy for SOME/IP guide](./docs/dev-guide/edge-agent-dev-guide-device-shadow-over-someip.md), + - and the [CAN over SOME/IP guide](./docs/dev-guide/can-over-someip-demo.md). +- :abc: String datatype support for both sensor data collection and actuator commands. +- :feather: [Last Known State (LKS)](./docs/dev-guide/edge-agent-dev-guide-last-known-state.md), a + lighter method of data collection. +- :cloud: + [IoT topics as a data destination](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/create-campaign.html). +- :jigsaw: [Custom functions in expressions](./docs/dev-guide/custom-function-dev-guide.md). +- :fast_forward: [Store and forward](./docs/dev-guide/store-and-forward-dev-guide.md), for + conditional upload of collected data. +- :wrench: [UDS DTC data collection](./docs/dev-guide/edge-agent-uds-dtc-dev-guide.md). + + +> [!NOTE] +> To quickly get started, jump to the +> [Edge Agent Developer Guide](./docs/dev-guide/edge-agent-dev-guide.md), the +> [ROS2 developer guide](./docs/dev-guide/vision-system-data/vision-system-data-demo.ipynb), the +> [Android Guide](./tools/android-app/README.md), or the +> [Raspberry Pi Tutorial](./docs/rpi-tutorial/raspberry-pi-tutorial.md). AWS IoT FleetWise is a service that makes it easy for Automotive OEMs, Fleet operators, Independent Software vendors (ISVs) to collect, store, organize, and monitor data from vehicles at scale. The @@ -28,10 +49,11 @@ Predictive Diagnostics, electric vehicle's battery cells outlier detection, etc. included sample C++ application to learn more about the FWE, develop an Edge Agent for your use case and test interactions before integration. -> _**Important**_ As provided in the AWS IoT FleetWise -> [Service Terms](https://aws.amazon.com/service-terms/), you are solely responsible for your Edge -> Agent, including ensuring that your Edge Agent and any updates and modifications to it are -> deployed and maintained safely and securely in any vehicles. + +> [!IMPORTANT] +> As provided in the AWS IoT FleetWise [Service Terms](https://aws.amazon.com/service-terms/), you +> are solely responsible for your Edge Agent, including ensuring that your Edge Agent and any +> updates and modifications to it are deployed and maintained safely and securely in any vehicles. ## AWS IoT FleetWise Architecture @@ -101,7 +123,7 @@ from and to AWS IoT FleetWise Server. All data sent to the AWS IoT FleetWise ser encrypted [TLS connection](https://docs.aws.amazon.com/iot/latest/developerguide/data-encryption.html) using MQTT, which is designed to make it secure by default while in transit. FWE uses MQTT quality of -service zero (QoS = 0). +service one (QoS = 1). ## Security @@ -140,6 +162,18 @@ enabled. - [Cyclone DDS: 0.8.0](https://github.com/eclipse-cyclonedds/cyclonedds) - [Fast-CDR: v1.0.21](https://github.com/eProsima/Fast-CDR) +Optional: The following dependencies are only required when the option `FWE_FEATURE_SOMEIP` is +enabled. + +- [vsomeip: 3.5.1](https://github.com/COVESA/vsomeip) +- [CommonAPI C++ Core Runtime: 3.2.4](https://github.com/COVESA/capicxx-core-runtime) +- [CommonAPI C++ SOME/IP Runtime: 3.2.4](https://github.com/COVESA/capicxx-someip-runtime) + +Optional: The following dependencies are only required when the option +`FWE_FEATURE_STORE_AND_FORWARD` is enabled. + +- [device-storelibrary-cpp: v1.0.0](https://github.com/aws/device-storelibrary-cpp) + See [LICENSE](./LICENSE) for more information. ## Getting Help diff --git a/THIRD-PARTY-LICENSES b/THIRD-PARTY-LICENSES index 88574298..75ba7b68 100644 --- a/THIRD-PARTY-LICENSES +++ b/THIRD-PARTY-LICENSES @@ -8,6 +8,7 @@ software/licensing: ** ROS2: Galactic - https://github.com/ros2/rclcpp ** Fast-CDR: v1.0.21 - https://github.com/eProsima/Fast-CDR ** Amazon Ion: v1.1.2 - https://github.com/amazon-ion/ion-c +** device-storelibrary-cpp: v1.0.0 - https://github.com/aws/device-storelibrary-cpp Apache License Version 2.0, January 2004 @@ -770,3 +771,383 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------------- + +** vsomeip: 3.4.10 - https://github.com/COVESA/vsomeip +** CommonAPI C++ Core Runtime: 3.2.0 - https://github.com/COVESA/capicxx-core-runtime +** CommonAPI C++ SOME/IP Runtime: 3.2.0 - https://github.com/COVESA/capicxx-someip-runtime + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/cmake/AwsIotFweConfig.cmake.in b/cmake/AwsIotFweConfig.cmake.in index ca016e4d..a5f36eb7 100644 --- a/cmake/AwsIotFweConfig.cmake.in +++ b/cmake/AwsIotFweConfig.cmake.in @@ -7,6 +7,10 @@ if(@FWE_STATIC_LINK@) find_dependency(Boost REQUIRED COMPONENTS @REQUIRED_BOOST_COMPONENTS@) find_dependency(AWSSDK REQUIRED COMPONENTS @REQUIRED_AWS_SDK_COMPONENTS@) find_dependency(ZLIB REQUIRED) + if(@FWE_FEATURE_STORE_AND_FORWARD@) + list(APPEND CMAKE_PREFIX_PATH "@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_LIBDIR@/cmake/aws") + find_dependency(aws-store REQUIRED) + endif() endif() if(NOT TARGET aws-iot-fleetwise-edge) diff --git a/cmake/capicxx_gen.cmake b/cmake/capicxx_gen.cmake new file mode 100644 index 00000000..d9c8fffb --- /dev/null +++ b/cmake/capicxx_gen.cmake @@ -0,0 +1,41 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +function(capicxx_generate_someip FIDL_FILE FDEPL_FILE OUTPUT_FILES) + find_package(Java COMPONENTS Runtime) + if(NOT Java_FOUND) + message(FATAL_ERROR "Java not found and is required for CommonAPI code generators") + endif() + if(NOT CAPICXX_GENERATOR_PATH) + set(CAPICXX_GENERATOR_PATH ${CMAKE_SYSTEM_PREFIX_PATH}) + endif() + foreach(SEARCH_PATH ${CAPICXX_GENERATOR_PATH}) + file(GLOB_RECURSE LAUNCHERS "${SEARCH_PATH}/org.eclipse.equinox.launcher_*.jar") + foreach(LAUNCHER ${LAUNCHERS}) + if(NOT CAPICXX_CORE_GENERATOR AND LAUNCHER MATCHES "commonapi-core-generator") + set(CAPICXX_CORE_GENERATOR ${LAUNCHER}) + elseif(NOT CAPICXX_SOMEIP_GENERATOR AND LAUNCHER MATCHES "commonapi-someip-generator") + set(CAPICXX_SOMEIP_GENERATOR ${LAUNCHER}) + endif() + endforeach() + if(CAPICXX_CORE_GENERATOR AND CAPICXX_SOMEIP_GENERATOR) + break() + endif() + endforeach() + if(NOT CAPICXX_CORE_GENERATOR) + message(FATAL_ERROR "CommonAPI core code generator not found") + endif() + message(STATUS "Found CommonAPI core code generator: ${CAPICXX_CORE_GENERATOR}") + if(NOT CAPICXX_SOMEIP_GENERATOR) + message(FATAL_ERROR "CommonAPI SOME/IP code generator not found") + endif() + message(STATUS "Found CommonAPI SOME/IP code generator: ${CAPICXX_SOMEIP_GENERATOR}") + add_custom_command( + OUTPUT ${OUTPUT_FILES} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FIDL_FILE} ${CMAKE_CURRENT_SOURCE_DIR}/${FDEPL_FILE} + COMMAND ${CMAKE_COMMAND} -E env bash -c "if ! LOG_OUTPUT=`java -Dlog4j.configuration=file://${CMAKE_CURRENT_SOURCE_DIR}/cmake/capicxx_gen_log4j_config.xml -jar ${CAPICXX_CORE_GENERATOR} -sk -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${FIDL_FILE}`; then echo \"${LOG_OUTPUT}\" >&2; exit -1; fi" + COMMAND ${CMAKE_COMMAND} -E env bash -c "if ! LOG_OUTPUT=`java -Dlog4j.configuration=file://${CMAKE_CURRENT_SOURCE_DIR}/cmake/capicxx_gen_log4j_config.xml -jar ${CAPICXX_SOMEIP_GENERATOR} -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${FDEPL_FILE}`; then echo \"${LOG_OUTPUT}\" >&2; exit -1; fi" + COMMENT "Generating CommonAPI SOME/IP files for ${FIDL_FILE} and ${FDEPL_FILE}" + VERBATIM + ) +endfunction() diff --git a/cmake/capicxx_gen_log4j_config.xml b/cmake/capicxx_gen_log4j_config.xml new file mode 100644 index 00000000..ab8736d1 --- /dev/null +++ b/cmake/capicxx_gen_log4j_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/configuration/static-config.json b/configuration/static-config.json index 14105dfa..c8b0a153 100644 --- a/configuration/static-config.json +++ b/configuration/static-config.json @@ -40,8 +40,11 @@ }, "internalParameters": { "readyToPublishDataBufferSize": 10000, + "readyToPublishCommandResponsesBufferSize": 100, + "maxConcurrentCommandRequests": 100, "systemWideLogLevel": "Trace", - "maximumAwsSdkHeapMemoryBytes": 10000000 + "maximumAwsSdkHeapMemoryBytes": 10000000, + "minFetchTriggerIntervalMs": 1000 }, "publishToCloudParameters": { "maxPublishMessageCount": 1000, @@ -54,10 +57,6 @@ "keepAliveIntervalSeconds": 60, "pingTimeoutMs": 30000, "sessionExpiryIntervalSeconds": 0, - "collectionSchemeListTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/collection_schemes", - "decoderManifestTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/decoder_manifests", - "canDataTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/signals", - "checkinTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/checkins", "certificateFilename": "path/to/my-certificate.pem.crt", "privateKeyFilename": "path/to/my-private.pem.key" }, diff --git a/docs/custom-data-source.md b/docs/custom-data-source.md deleted file mode 100644 index 90708ad5..00000000 --- a/docs/custom-data-source.md +++ /dev/null @@ -1,190 +0,0 @@ -# Custom data source (treating data not on CAN as CAN signals) - -> :warning: **Internal code** structures of the Reference Implementation for AWS IoT FleetWise -> ("FWE") **might be fundamentally restructured**, so extending the code might involve significant -> work when upgrading to newer versions. To avoid this problems the external interfaces can be used -> to hand over data to FWE over supported communication channels. For example use a small custom -> separate process to read data and put it to a real or a virtual CAN that is then read by FWE. - -Considering the warning this gives a temporary workaround to get the data directly into AWS IoT -FleetWise _without_ ever putting it on a real or virtual SocketCAN interface. In the following we -will read data from a custom data source (like a tty or a file etc.) convert it to a double and pass -it on to be treated like a Signal read on CAN. All this will happen in a separate thread but inside -FWE. We will use an example where we read NMEA GPS data from a file and handle them as if the were -received on a CAN. We assume we have a the two signals `Vehicle.CurrentLocation.Longitude` and -`Vehicle.CurrentLocation.Latitude` in our signal catalog and in a model manifest. For this the API -calls UpdateSignalCatalog and CreateModelManifest can be used. Now we create a special decoder -manifest. Please replace the `modelManifestArn` with a model manifest that has both signals from the -signal catalog. - -```json -{ - "name": "IWaveGpsDecoderManifest", - "description": "Has all the signals that are read over the custom data source from the NMEA data", - "modelManifestArn": "arn:aws:iotfleetwise:us-east-1:xxxxxxxxxxxx:model-manifest/IWaveGPSModel", - "networkInterfaces": [ - { - "interfaceId": "IWAVE-GPS-CAN", - "type": "CAN_INTERFACE", - "canInterface": { - "name": "iwavegpscan", - "protocolName": "CAN" - } - } - ], - "signalDecoders": [ - { - "fullyQualifiedName": "Vehicle.CurrentLocation.Longitude", - "type": "CAN_SIGNAL", - "interfaceId": "IWAVE-GPS-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": false, - "isSigned": true, - "startBit": 0, - "offset": -2000.0, - "factor": 0.001, - "length": 32 - } - }, - { - "fullyQualifiedName": "Vehicle.CurrentLocation.Latitude", - "type": "CAN_SIGNAL", - "interfaceId": "IWAVE-GPS-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": false, - "isSigned": true, - "startBit": 32, - "offset": -2000.0, - "factor": 0.001, - "length": 32 - } - } - ] -} -``` - -If you want you can copy over other signals collected to this decoder manifest from existing -campaigns. - -The custom data source implementation in the edge code must inherit from `CustomDataSource`. For -this example we created the class `IWaveGpsSource`. The new custom data source class should then -have an init function to set parameters. This init function should then be called from -`IoTFleetWiseEngine::connect()` for example like this: - -```cpp -if(mIWaveGpsSource->init( - "/dev/ttyUSB1", // From this file the GPS coordinates will be read in the NMEA line format - canIDTranslator.getChannelNumericID( "IWAVE-GPS-CAN" ), // "interfaceId": "IWAVE-GPS-CAN" - 1, // "messageId": 1 - 32, //"startBit": 32 for latitude - 0 // "startBit": 0 for longitude - ) && - mIWaveGpsSource->connect() - ) -{ - mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( - std::bind( &IWaveGpsSource::onChangeOfActiveDictionary, - mIWaveGpsSource.get(), - std::placeholders::_1, - std::placeholders::_2 ) ); - mIWaveGpsSource->start(); -} -``` - -Here we hand over the parameters we defined in the decoder manifest. With the interface Id, the -message Id and the start Bit the custom data source can get the signalId if at least one active -campaign uses one of them. For this to work we need add `canIDTranslator.add( "IWAVE-GPS-CAN");` in -the end of the `CAN InterfaceID to InternalID Translator` section of -`IoTFleetWiseEngine::connect()`. This is necessary because we internally use ids instead of the full -name. This parameters can also be read from the static config as the example in -`IoTFleetWiseEngine::connect()` shows where the optional section "iWaveGpsExample" under -"staticConfig" will be used so the parameters can be changed without recompilation. - -## ExternalGpsSource - -The provided example `ExternalGpsSource` module can be enabled with the `FWE_FEATURE_EXTERNAL_GPS` -build option. This allows the ingestion of GPS data using the custom data source interface from an -in-process source, for example when the FWE code is built as a shared library. For example the -decoder manifest for the latitude and longitude signals can be created as follows: - -```json -{ - "name": "ExternalGpsDecoderManifest", - "modelManifestArn": "arn:aws:iotfleetwise:us-east-1:xxxxxxxxxxxx:model-manifest/ExternalGPSModel", - "networkInterfaces": [ - { - "interfaceId": "EXTERNAL-GPS-CAN", - "type": "CAN_INTERFACE", - "canInterface": { - "name": "externalgpscan", - "protocolName": "CAN" - } - } - ], - "signalDecoders": [ - { - "fullyQualifiedName": "Vehicle.CurrentLocation.Longitude", - "type": "CAN_SIGNAL", - "interfaceId": "EXTERNAL-GPS-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": -2000.0, - "factor": 0.001, - "length": 32 - } - }, - { - "fullyQualifiedName": "Vehicle.CurrentLocation.Latitude", - "type": "CAN_SIGNAL", - "interfaceId": "EXTERNAL-GPS-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 32, - "offset": -2000.0, - "factor": 0.001, - "length": 32 - } - } - ] -} -``` - -# Implementing a new CustomDataSource - -## What to do in the init - -Initialize everything you need to collect your custom data like opening file descriptors. You need -to set the filter of the inherited class `CustomDataSource` like this: -`setFilter(canChannel,canRawFrameId);`. After this is set the `CustomDataSource` will start calling -the `pollData()` function, but only if at least one campaign uses one signal from this CAN message. -The interval in which `pollData()` is called (if at lease one related campaign is active) can be -configured by calling `setPollIntervalMs`. - -## How to publish data in the pollData - -In the `pollData()` function first extract the data in a custom way (like reading a file/tty or a -socket). Then use the start bit of the signals configured in the cloud to get the internal signalId. -This has to be done in the `pollData()` as the signalId could change between two calls to -`pollData()`. The inherited function `getSignalIdFromStartBit` can be used for this. Then use the -signalId to publish the data to `SignalBufferPtr`. This can for example look like this: - -```cpp -mSignalBufferPtr->push(CollectedSignal(getSignalIdFromStartBit(mLatitudeStartBit),timestamp,lastValidLatitude)); -``` - -## Debug - -First make sure that at least one campaign uses at least on signal of the custom CAN message you -configured using `setFilter(canChannel,canRawFrameId);`. If that is the case you should see the -following line in the log - -``` -[TRACE] [CustomDataSource.cpp:159] [matchDictionaryToFilter()]: [Dictionary with relevant information for CustomDataSource so waking up] -``` diff --git a/docs/dev-guide/adding-custom-fidl-file-dev-guide.md b/docs/dev-guide/adding-custom-fidl-file-dev-guide.md new file mode 100644 index 00000000..6b300677 --- /dev/null +++ b/docs/dev-guide/adding-custom-fidl-file-dev-guide.md @@ -0,0 +1,193 @@ +# Adding custom `.fidl` file + +Let there be two files that we are bringing in for adding SOME/IP signals: + +- `custom_someip.fidl` +- `custom_someip.fdepl` + +Copy + +- `custom_someip.fidl` to + [`ExampleSomeipInterface.fidl`](../../interfaces/someip/fidl/ExampleSomeipInterface.fidl) +- `custom_someip.fdepl` to + [`ExampleSomeipInterface.fdepl`](../../interfaces/someip/fidl/ExampleSomeipInterface.fdepl) + +**Follow the steps to bring these signals for being used with the AWS IoT FleetWise** + +1. Add `onChange` handler in + [`ExampleSomeipInterfaceStubImpl.cpp`](../../tools/someipigen/src/ExampleSomeipInterfaceStubImpl.cpp) + + Go to the + [`src/ExampleSomeipInterfaceStubImpl.cpp`](../../tools/someipigen/src/ExampleSomeipInterfaceStubImpl.cpp) + and add an initial value for the signal in the class constructor, and then: + + - Either implement the SOME/IP stub functions for methods using `getValueAsync` and + `setValueAsync` + - OR add an `onChange` handler for attributes + + 1. Using `getValueAsync` and `setValueAsync` + + Example: if you have SOME/IP methods `getTemperature` and `setTemperature`, and you want them + to set and get a signal named `Temperature` with type `int32_t`, you would add the following + in `ExampleSomeipInterfaceStubImpl.cpp`: + + ```cpp + // Add the initial value in the constructor: + mSignals["Temperature"] = Signal( boost::any( static_cast( 0 ) ) ); + + // Add the stub implementation for `getTemperature` method: + void + ExampleSomeipInterfaceStubImpl::getTemperature( + const std::shared_ptr client, + getSpeedReply_t reply ) + { + (void)client; + getValueAsync( "Temperature", reply ); + } + + // Add the stub implementation for `setTemperature` method: + void + ExampleSomeipInterfaceStubImpl::setTemperature( + const std::shared_ptr client, + int32_t value, + setTemperatureReply_t reply ) + { + (void)client; + setValueAsync( "Temperature", value, reply ); + } + ``` + + 1. Using `onChange` handler for attributes + + Example: if you have a SOME/IP attribute `temperature`, and you want to link it to a signal + named `Temperature` with type `int32_t`, you would add the following in + `ExampleSomeipInterfaceStubImpl.cpp`: + + ```cpp + // Add the initial value and onChanged handler in the constructor: + mSignals["Temperature"] = Signal( boost::any(static_cast( 0 ) ), [this](){ + fireTemperatureAttributeChanged( boost::any_cast( mSignals["Temperature"].value ) ); + } ); + ``` + +1. Make changes to the existing [`SomeipDataSource.h`](../../src/SomeipDataSource.h). Replace the + current signals with the signals in `custom_someip.fidl` in a similar way as already implemented + in the `SomeipDataSource.h`. + + For example lets consider we are adding a signal `Temperature` of type `INT32` : + + ```cpp + uint32_t mTemperatureSubscription{}; + bool mLastTemperatureValAvailable{}; + int32_t mLastTemperatureVal{}; + void pushTemperatureValue( const int32_t &val ); + ``` + +1. Make changes to the [`SomeipDataSource.cpp`](../../src/SomeipDataSource.cpp) + + This file ingests the data to FWE. Follow the current settings of the signals in + `SomeipDataSource.cpp` to setup our own signals to be collected by AWS IoT FleetWise. + + In `SomeipDataSource::~SomeipDataSource()` which is a destructor add a condition to make your + signal subscription unavailable under the condition `if ( mProxy )`. + + For example: + + ```cpp + if ( mTemperatureSubscription != 0 ) + { + mProxy->getTemperatureAttribute().getChangedEvent().unsubscribe( mTemperatureSubscription ); + } + ``` + + Add a function `void SomeipDataSource::pushXXXXValue( const YYYY &val )` to ingest value to your + signals. + + For example: + + ```cpp + void + SomeipDataSource::pushTemperatureValue( const int32_t &val ) + { + mNamedSignalDataSource->ingestSignalValue( + 0, "Vehicle.ExampleSomeipInterface.Temperature", DecodedSignalValue{ val, SignalType::INT32 } ); + } + ``` + + Under `bool SomeipDataSource::init()` define `mXXXXSubscription`. + + For example: + + ```cpp + mTemperatureSubscription = + mProxy->getTemperatureAttribute().getChangedEvent().subscribe( [this]( const int32_t &val ) { + std::lock_guard lock( mLastValMutex ); + mLastTemperatureVal = val; + mLastTemperatureValAvailable = true; + pushTemperatureValue( val ); + } ); + ``` + + Under the if condition `if ( mCyclicUpdatePeriodMs > 0 )` add checks for proxy availability + similar to the already present implementation. + + For example: + + ```cpp + while ( !mShouldStop ){ + { + std::lock_guard lock( mLastValMutex ); + if ( !mProxy->isAvailable() ) + { + mLastTemperatureValAvailable = false; + } + else{ + if ( mLastTemperatureValAvailable ) + { + pushTemperatureValue( mLastTemperatureVal ); + } + } + } + std::this_thread::sleep_for( std::chrono::milliseconds( mCyclicUpdatePeriodMs ) ); + } + ``` + +1. Add signals with initial value to [`signals.json`](../../tools/someipigen/signals.json) + + For example: + + ```cpp + "Vehicle.ExampleSomeipInterface.Temperature": 1 + ``` + +1. Add signals to [`custom-decoders-someip.json`](../../tools/cloud/custom-decoders-someip.json) + with proper configurations as current implementation. + + For example: + + ```json + { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.Temperature", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ExampleSomeipInterface.Temperature" + } + } + ``` + +1. Add signal to [`custom-nodes-someip.json`](../../tools/cloud/custom-nodes-someip.json) + + Add your signals coming from sensor or an actuator. + + For example: + + ```json + { + "sensor": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.Temperature", + "description": "Vehicle.ExampleSomeipInterface.Temperature", + "dataType": "INT32" + } + } + ``` diff --git a/docs/dev-guide/can-actuators-dev-guide.md b/docs/dev-guide/can-actuators-dev-guide.md new file mode 100644 index 00000000..029f318c --- /dev/null +++ b/docs/dev-guide/can-actuators-dev-guide.md @@ -0,0 +1,533 @@ +# CAN actuators demo + + +> [!NOTE] +> This guide makes use of "gated" features of AWS IoT FleetWise for which you will need to request +> access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for +> more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +This guide demonstrates how to use AWS IoT FleetWise to implement a CAN command dispatcher for use +with the remote commands feature, along with the +[Network agnostic actuator commands (NADC)](./network-agnostic-dev-guide.md) feature. The NADC +feature allows the actuators to be identified by the full-qualified-name (FQN) at the edge. The demo +uses application-specific request and response CAN payload formats to forward the command request to +CAN, including the 'command ID', 'issued timestamp' and 'execution timeout' parameters as well as +the requested actuator value. A Python script called `can_command_server.py` is used to simulate +another vehicle in the network that receives the request and responds on CAN with a response +message. The Reference Implementation for AWS IoT FleetWise (FWE) receives this response, and sends +it back to the cloud. + +The format of the CAN request and response messages implemented in +[`CanCommandDispatcher`](../../src/CanCommandDispatcher.h) is as follows: + +- The command CAN request payload is formed from the null-terminated command ID string, a `uint64_t` + issued timestamp in ms since epoch, a `uint64_t` relative execution timeout in ms since the issued + timestamp, and one actuator argument serialized in network byte order. A relative timeout value of + zero means no timeout. Example with command ID `"01J3N9DAVV10AA83PZJX561HPS"`, issued timestamp of + `1723134069000`, relative timeout of `1000`, and actuator datatype `int32_t` with value + `1234567890`: + +``` +|----------------------------------------|----------------------------------------|---------------------------------|---------------------------------|---------------------------| +| Payload byte: | 0 | 1 | ... | 24 | 25 | 26 | 27 | 28 | ... | 33 | 34 | 35 | 36 | ... | 41 | 42 | 43 | 44 | 45 | 46 | +|----------------------------------------|----------------------------------------|---------------------------------|---------------------------------|---------------------------| +| Value: | 0x30 | 0x31 | ... | 0x50 | 0x53 | 0x00 | 0x00 | 0x00 | ... | 0x49 | 0x08 | 0x00 | 0x00 | ... | 0x03 | 0xE8 | 0x49 | 0x96 | 0x02 | 0xD2 | +|----------------------------------------|----------------------------------------|---------------------------------|---------------------------------|---------------------------| +Command ID (null terminated string)-------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Issued timestamp (uint64_t network byte order)-------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Execution timeout (uint64_t network byte order)----------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Request argument (int32_t network byte order)----------------------------------------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + +- The command CAN response payload is formed from the null-terminated command ID string, a 1-byte + command status code, a 4-byte `uint32_t` reason code, and a null-terminated reason description + string. The values of the status code correspond with the enum `CommandStatus` Example with + command ID `"01J3N9DAVV10AA83PZJX561HPS"`, response status `CommandStatus::EXECUTION_FAILED`, + reason code `0x0001ABCD`, and reason description `"hello"`: + +``` +|----------------------------------------|----------------------------------------|------|---------------------------|-----------------------------------------| +| Payload byte: | 0 | 1 | ... | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | +|----------------------------------------|----------------------------------------|------|---------------------------|-----------------------------------------| +| Value: | 0x30 | 0x31 | ... | 0x50 | 0x53 | 0x00 | 0x03 | 0x00 | 0x01 | 0xAB | 0xCD | 0x68 | 0x65 | 0x6C | 0x6C | 0x6F | 0x00 | +|----------------------------------------|----------------------------------------|------|---------------------------|-----------------------------------------| +Command ID (null terminated string)-------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Command status (enum CommandStatus)------------------------------------------------^^^^^^ +Reason code (uint32_t network byte order)-------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Reason description (null terminated string)---------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + +The table `EXAMPLE_CAN_INTERFACE_SUPPORTED_ACTUATOR_MAP` in +[`IoTFleetWiseEngine`](../../src/IoTFleetWiseEngine.cpp) defines the mapping between the FQN of the +actuator signal and the CAN request and response message IDs, along with the expected datatype of +the signal. (Note: when the most-significant bit of the CAN message ID is set, it denotes a 29-bit +extended CAN ID.): + +```cpp +static const std::unordered_map + EXAMPLE_CAN_INTERFACE_SUPPORTED_ACTUATOR_MAP = { + { "Vehicle.actuator6", { 0x00000123, 0x00000456, SignalType::INT32 } }, + { "Vehicle.actuator7", { 0x80000789, 0x80000ABC, SignalType::DOUBLE } }, +}; +``` + +These IDs and types can also be found in the configuration table at the top of +[`can_command_server.py`](../../tools/cansim/can_command_server.py) + +## Prerequisites + +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Windows, Linux or MacOS machine. + +## Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +1. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + 1. Do not include the file suffix `.pem`. + 1. If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +1. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +1. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +## Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +## Download or build the FWE binary + +**To quickly run the demo**, download the pre-built FWE binary, and install CAN support: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-socketcan.sh + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-socketcan.sh + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE and the CAN simulator: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh \ + && sudo -H ./tools/install-socketcan.sh \ + && sudo ldconfig + ``` + +1. Compile FWE with remote commands support: + + ```bash + ./tools/build-fwe-native.sh --with-remote-commands-support + ``` + +## Start the CAN command server + +A simulator is used to model another node in the vehicle network that responds to CAN command +requests. + +1. Start the CAN command server: + + ```bash + cd tools/cansim \ + && python3 can_command_server.py --interface vcan0 + ``` + +## Provision and run FWE + +1. Open a new terminal _on the development machine_, and run the following to provision credentials + for the vehicle and configure the network interface for CAN commands: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-can-actuators \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` \ + --session-expiry-interval-seconds 3600 \ + --can-command-interface vcan0 + ``` + +1. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + +## Run the AWS IoT FleetWise demo script + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +vehicle model, register the virtual vehicle created in the previous section. + +1. Open a new terminal _on the development machine_ and run the following to install the + dependencies of the demo script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + +1. Run the demo script: + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-can-actuators \ + --node-file custom-nodes-can-actuators.json \ + --decoder-file custom-decoders-can-actuators.json \ + --network-interface-file network-interface-custom-can-actuators.json + ``` + + The demo script: + + 1. Registers your AWS account with AWS IoT FleetWise, if not already registered. + 1. Creates a signal catalog, containing `custom-nodes-can-actuators.json` which includes the CAN + actuator signals. + 1. Creates a model manifest that references the signal catalog with all of the signals. + 1. Activates the model manifest. + 1. Creates a decoder manifest linked to the model manifest using + `custom-decoders-can-actuators.json` for decoding the CAN signals from the network interface + `network-interface-custom-can-actuators.json`. + 1. Updates the decoder manifest to set the status as `ACTIVE`. + 1. Creates a vehicle with a name equal to `fwdemo-can-actuators`, the same as the name passed to + `provision.sh`. + 1. Creates a fleet. + 1. Associates the vehicle with the fleet. + +### Remote Command Execution + +The following steps will send a CAN command via the AWS IoT FleetWise 'remote commands' feature. + +1. Run the following command _on the development machine_ to create an IAM role to generate the + command payload: + + ```bash + SERVICE_ROLE_ARN=`./manage-service-role.sh \ + --service-role IoTCreateCommandPayloadServiceRole \ + --service-principal iot.amazonaws.com \ + --actions iotfleetwise:GenerateCommandPayload \ + --resources '*'` + ``` + +1. Next create a remote command to send the CAN command with message ID `0x123` and expect a + response on CAN message ID `0x456`. This CAN command is mapped via the decoder manifest to the + 'actuator' node `Vehicle.actuator6` in the signal catalog. + + ```bash + aws iot create-command --command-id actuator6-command --namespace "AWS-IoTFleetWise" \ + --region us-east-1 \ + --role-arn ${SERVICE_ROLE_ARN} \ + --mandatory-parameters '[{ + "name": "$actuatorPath.Vehicle.actuator6", + "defaultValue": { "S": "0" } + }]' + ``` + +1. Run the following command to start the execution of the command defined above with the value to + set for the actuator. + + ```bash + JOBS_ENDPOINT_URL=`aws iot describe-endpoint --region us-east-1 --endpoint-type iot:Jobs | jq -j .endpointAddress` \ + && ACCOUNT_ID=`aws sts get-caller-identity | jq -r .Account` \ + && COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator6-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --parameters '{ + "$actuatorPath.Vehicle.actuator6": + { "S": "10" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} | jq -r .executionId` \ + && echo "Command execution id: ${COMMAND_EXECUTION_ID}" + ``` + +1. Run the following command to get the command execution status. + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + +1. You should see the following output indicating the command was successfully executed. Note that + the `reasonCode` (uint32) and `reasonDescription` (string) are extensible result information + fields. Refer to [ICommandDispatcher.h](../../src/ICommandDispatcher.h) for the reason codes + defined by FWE. The OEM range of reason codes begins at 65536. In this example implementation the + `reasonCode` is set to `0x1234` (4660), and `reasonDescription` is set to `"hello"` by the CAN + command server[`can_command_server.py`](../../tools/cansim/can_command_server.py). + + ```json + { + "executionId": "", + "commandArn": "arn:aws:iot:us-east-1::command/actuator6-command", + "targetArn": "arn:aws:iot:us-east-1::thing/fwdemo-can-actuators", + "status": "SUCCEEDED", + "statusReason": { + "reasonCode": "4660", + "reasonDescription": "hello" + }, + "parameters": { + "$actuatorPath.Vehicle.actuator6": { + "S": "10" + } + }, + "executionTimeoutSeconds": 10, + "createdAt": "", + "lastUpdatedAt": "", + "completedAt": "" + } + ``` + + In the FWE log you should see the following indicating that the command was successfully + executed: + + ``` + [TRACE] [ActuatorCommandManager.cpp:125] [processCommandRequest()]: [Processing Command Request with ID: ] + [INFO ] [CanCommandDispatcher.cpp:365] [setActuatorValue()]: [Sending request for actuator Vehicle.actuator6 and command id ] + [INFO ] [CanCommandDispatcher.cpp:390] [operator()()]: [Request sent for actuator Vehicle.actuator6 and command id ] + [INFO ] [CanCommandDispatcher.cpp:209] [handleCanFrameReception()]: [Received response for actuator Vehicle.actuator6 with command id , status SUCCEEDED, reason code 4660, reason description hello] + ``` + +### Long-running commands + +It is possible for commands to take an extended time to complete. In this case the vehicle can +report the command status as `IN_PROGRESS` to indicate that the command has been received and is +being run, before the final status of `SUCCEEDED` etc. is reported. + +In the example CAN commands provided, `Vehicle.actuator7` is configured as such a "long-running +command". After running of this command is started the CAN command server will periodically notify +FWE that the status is `CommandStatus::IN_PROGRESS`. The intermediate status is sent to the cloud +and can also be obtained by calling the `aws iot get-command-execution` API. + +1. Run the following to create the long-running command: + + ```bash + aws iot create-command --command-id actuator7-command --namespace "AWS-IoTFleetWise" \ + --region us-east-1 \ + --role-arn ${SERVICE_ROLE_ARN} \ + --mandatory-parameters '[{ + "name": "$actuatorPath.Vehicle.actuator7", + "defaultValue": { "S": "0" } + }]' + ``` + +1. Then start the command: + + ```bash + COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator7-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --parameters '{ + "$actuatorPath.Vehicle.actuator7": + { "S": "10" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} \ + --execution-timeout 20 | jq -r .executionId` \ + && echo "Command execution id: ${COMMAND_EXECUTION_ID}" + ``` + +1. Now repeatedly run this command to get the command status: + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + + The command takes 10 seconds to complete. In this time you will see that the status is + `IN_PROGRESS`. After the command completes the status changes to `SUCCEEDED`. + +### Concurrent commands + +It is possible for commands to be executed concurrently, even for the same actuator. Each execution +is uniquely identified by the execution ID. In the following example, 3 executions of the +`Vehicle.actuator7` command are started spaced by 1 second. Since each execution takes 10 seconds to +complete, all 3 will run in parallel. + +1. Run the following to begin 3 executions of the `Vehicle.actuator7` command: + + ```bash + COMMAND_EXECUTION_IDS=() \ + && for ((i=0; i<3; i++)); do + if ((i>0)); then sleep 1; fi + COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator7-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --parameters '{ + "$actuatorPath.Vehicle.actuator7": + { "S": "10" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} \ + --execution-timeout 20 | jq -r .executionId` + echo "Command execution ${i} id: ${COMMAND_EXECUTION_ID}" + COMMAND_EXECUTION_IDS+=("${COMMAND_EXECUTION_ID}") + done + ``` + +1. Now repeatedly run the following to get the status of the 3 commands as they run in parallel: + + ```bash + for ((i=0; i<3; i++)); do + echo "---------------------------" + echo "Command execution ${i} status:" + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --execution-id ${COMMAND_EXECUTION_IDS[i]} + done + ``` + +### Offline commands + +It is possible for a command execution to be started while FWE is offline, then FWE will begin +execution of the command when it comes online so long as the following prerequisites are met: + +- FWE has successfully connected via MQTT at least once, with persistent session enabled and the + MQTT session timeout has not elapsed. To enable persistent session, set + `.staticConfig.mqttConnection.sessionExpiryIntervalSeconds` in the config file or + `--session-expiry-interval-seconds` when running `configure-fwe.sh` to a non-zero value + sufficiently large. +- Persistency is enabled for FWE (so that the decoder manifest is available immediately when FWE + starts). +- The command timeout has not been exceeded. +- The responding CAN actuator is available when FWE is started (in this case the + `can_command_server.py` tool). + +The following steps demonstrate offline commands: + +1. Switch to the terminal running FWE, and stop it using `CTRL-C`. + +1. Switch to the terminal used to run the AWS CLI commands, and start execution of a command with a + 30 second timeout: + + ```bash + COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator6-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --parameters '{ + "$actuatorPath.Vehicle.actuator6": + { "S": "456" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} \ + --execution-timeout 30 | jq -r .executionId` \ + && echo "Command execution id: ${COMMAND_EXECUTION_ID}" + ``` + +1. Get the current status of the command, which will remain as `CREATED` since FWE is not running: + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + +1. Switch to the FWE terminal, and restart it by running: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + +1. Switch to the AWS CLI terminal, and run the following to get the new status of the command, which + should be `SUCCEEDED`. Since FWE rejoined an existing MQTT session and the command was published + with QoS 1 (at least once), the MQTT broker sends the command to FWE as soon as it connects to + the cloud. FWE is able to execute the command, since it has not timed out, the decoder manifest + is available (as persistency for FWE is enabled), and the CAN command server is available. + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-can-actuators \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + +1. Repeat the above, but this time wait longer than 30s before restarting FWE. In this case FWE will + still receive the command request from cloud, but since the timeout has expired it will not be + executed and the returned status will be `TIMED_OUT`. + +## Clean up + +1. Run the following _on the development machine_ to clean up resources created by the + `provision.sh` and `demo.sh` scripts. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && ./clean-up.sh \ + && ../provision.sh \ + --vehicle-name fwdemo-can-actuators \ + --region us-east-1 \ + --only-clean-up \ + && ./manage-service-role.sh \ + --service-role IoTCreateCommandPayloadServiceRole \ + --clean-up + ``` + +1. Delete the CloudFormation stack for your development machine, which by default is called `fwdev`: + https://us-east-1.console.aws.amazon.com/cloudformation/home diff --git a/docs/dev-guide/can-over-someip-demo.md b/docs/dev-guide/can-over-someip-demo.md new file mode 100644 index 00000000..94b2fd3b --- /dev/null +++ b/docs/dev-guide/can-over-someip-demo.md @@ -0,0 +1,418 @@ +# Demo of AWS IoT FleetWise for CAN over SOME/IP + +This guide demonstrates how to use AWS IoT FleetWise to collect CAN data that is sent over a SOME/IP +network. This network configuration is common in automotive systems that contain both CAN and +Ethernet physical layers, where CAN data is bridged to the SOME/IP network to make the CAN data +available to nodes on the Ethernet network. + +This demo generates CAN data on a virtual CAN bus, and bridges this data onto SOME/IP. The Reference +Implementation for AWS IoT FleetWise (FWE) is then provisioned and run to collect the CAN data from +SOME/IP and upload it to the cloud. The data is then downloaded from Amazon Timestream and plotted +in an HTML graph format. + +The following diagram illustrates the dataflow and artifacts consumed and produced by this demo: + + + +### SOME/IP Payload format + +In this demonstration, the following payload format is used to encapsulate each CAN frame within a +single SOME/IP message (PDU). Note that FIDL files and the 'CommonAPI' library are not used to model +or serialize this payload format. + +``` +___________________________________________________________ +| CAN ID | Timestamp (in us) | CAN data | +|___________|_____________________|_______________________| + 4 bytes 8 bytes variable length +``` + +The CAN ID and Timestamp are unsigned integers encoded in network byte order (big endian). The CAN +ID is in the +[SocketCAN format](https://github.com/linux-can/can-utils/blob/88f0c753343bd863dd3110812d6b4698c4700b26/include/linux/can.h#L66-L78) + +## Prerequisites + +- Access to an AWS Account with administrator privileges. +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Linux or MacOS machine. + +## Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +1. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + 1. Do not include the file suffix `.pem`. + 1. If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +1. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +1. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +## Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +## Download or build the FWE binary + +**To quickly run the demo,** download the pre-built FWE binary, and install the CAN simulator: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz tools/can-to-someip/can-to-someip \ + && sudo -H ./tools/install-socketcan.sh \ + && sudo -H ./tools/install-cansim.sh + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz tools/can-to-someip/can-to-someip \ + && sudo -H ./tools/install-socketcan.sh \ + && sudo -H ./tools/install-cansim.sh + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE with SOME/IP support and the CAN simulator: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh --with-someip-support \ + && sudo -H ./tools/install-socketcan.sh \ + && sudo -H ./tools/install-cansim.sh \ + && sudo ldconfig + ``` + +1. Compile FWE with SOME/IP support: + + ```bash + ./tools/build-fwe-native.sh --with-someip-support + ``` + +## Start the CAN to SOME/IP bridge + +At this point the CAN simulator is running and is generating data on the virtual CAN bus `vcan0`. +You can check that data is being generated by running `candump vcan0`. + +1. Start the `can-to-someip` tool to bridge this data onto the SOME/IP network from the `vcan0` CAN + interface to the SOME/IP network using Service ID 0x7777, Instance ID 0x5678, publishing Event ID + 0x8778 in Event Group ID 0x5555. + + ```bash + ./tools/can-to-someip/can-to-someip \ + --can-interface vcan0 \ + --service-id 0x7777 \ + --instance-id 0x5678 \ + --event-id 0x8778 \ + --event-group-id 0x5555 + ``` + + These identifiers match the service that FWE subscribes to, as defined in the + `tools/cloud/network-interface-someip-to-can-bridge.json` file used below. + + **Note:** When the `can-to-someip` tool and FWE run on the same machine, `vsomeip` uses a UNIX + domain socket for communication rather than IP communication. If you are interested in SOME/IP + communication over IP, see [Running over IP](#running-over-ip). + +## Provision and run FWE + +1. Open a new terminal _on the development machine_, and run the following to provision credentials + for the vehicle and configure the network interface as SOME/IP to CAN: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-can-to-someip \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` \ + --enable-can-to-someip-bridge-interface + ``` + +1. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + + You should see the following message in the log indicating that FWE has successfully subscribed + to the `can-to-someip` service: + + ``` + [INFO ] [SomeipToCanBridge.cpp:68] [operator()()]: [Service [7777.5678] is available] + ``` + +## Run the AWS IoT FleetWise demo script + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +vehicle model, register the virtual vehicle created in the previous section and run a campaign to +collect data from it. + +1. Open a new terminal _on the development machine_ and run the following to install the + dependencies of the demo script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + +1. Run the following command to generate 'node' and 'decoder' JSON files from the input DBC file: + + ```bash + python3 dbc-to-nodes.py hscan.dbc can-nodes.json \ + && python3 dbc-to-decoders.py hscan.dbc can-decoders.json + ``` + +1. Run the demo script: + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-can-to-someip \ + --node-file can-nodes.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-can.json \ + --campaign-file campaign-brake-event.json + ``` + + The demo script: + + 1. Registers your AWS account with AWS IoT FleetWise, if not already registered. + 1. Creates an Amazon Timestream database and table. + 1. Creates IAM role and policy required for the service to write data to Amazon Timestream. + 1. Creates a signal catalog based on `can-nodes.json`. + 1. Creates a model manifest that references the signal catalog with all of the CAN signals. + 1. Activates the model manifest. + 1. Creates a decoder manifest linked to the model manifest using `can-decoders.json` for decoding + signals from the network interfaces defined in `network-interfaces-can.json`. + 1. Updates the decoder manifest to set the status as `ACTIVE`. + 1. Creates a vehicle with a name equal to `fwdemo-can-to-someip`, the same as the name passed to + `provision.sh`. + 1. Creates a fleet. + 1. Associates the vehicle with the fleet. + 1. Creates a campaign from `campaign-brake-event.json` that contains a condition-based collection + scheme to capture the engine torque and the brake pressure when the brake pressure is above + 7000, and targets the campaign at the fleet. + 1. Approves the campaign. + 1. Waits until the campaign status is `HEALTHY`, which means the campaign has been deployed to + the fleet. + 1. Waits 30 seconds and then downloads the collected data from Amazon Timestream. + 1. Saves the data to an HTML file. + +1. When the script completes, a path to an HTML file is given. _On your local machine_, use `scp` to + download it, then open it in your web browser: + + ```bash + scp -i ubuntu@: . + ``` + +1. To explore the collected data, you can click and drag to zoom in. The red line shows the + simulated brake pressure signal. As you can see that when hard braking events occur (value above + 7000), collection is triggered and the engine torque signal data is collected. + + Alternatively, if your AWS account is enrolled with Amazon QuickSight or Amazon Managed Grafana, + you may use them to browse the data from Amazon Timestream directly. + + ![](./images/collected_data_plot.png) + +## Clean up + +1. Run the following _on the development machine_ to clean up resources created by the + `provision.sh` and `demo.sh` scripts. **Note:** The Amazon Timestream resources are not deleted. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && ./clean-up.sh \ + && ../provision.sh \ + --vehicle-name fwdemo-can-to-someip \ + --region us-east-1 \ + --only-clean-up + ``` + +1. Delete the CloudFormation stack for your development machine, which by default is called `fwdev`: + https://us-east-1.console.aws.amazon.com/cloudformation/home + +## Running over IP + +In the above example both FWE and the `can-to-someip` program were both running on the same +development machine. In this scenario the [vsomeip](https://github.com/COVESA/vsomeip) library uses +a local UNIX domain socket for service discovery and communication between the processes. + +If you would like to run the example over IP, with the `can-to-someip` program running on one +machine and FWE running on a different machine on the same local network, then it is necessary to +configure `vsomeip` using JSON configuration files to setup the IP addresses, ports and protocols +for the service to use. + +The example below details how to configure `vsomeip` for UDP over IP communication. If you are +interested in using TCP over IP communication refer to the `vsomeip` documentation. + +1. Create a JSON configuration file called `vsomeip-can-to-someip.json` for the `can-to-someip` + program, replacing `` with the IPv4 address of the machine running the program: + + ```json + { + "unicast": "", + "netmask": "255.255.0.0", + "logging": { + "level": "trace", + "console": "true", + "dlt": "false" + }, + "applications": [ + { + "name": "can-to-someip", + "id": "0x1212" + } + ], + "services": [ + { + "service": "0x7777", + "instance": "0x5678", + "unreliable": "30509" + } + ], + "service-discovery": { + "enable": "true", + "multicast": "224.224.224.245", + "port": "30490", + "protocol": "udp", + "initial_delay_min": "10", + "initial_delay_max": "100", + "repetitions_base_delay": "200", + "repetitions_max": "3", + "ttl": "3", + "cyclic_offer_delay": "2000", + "request_response_delay": "1500" + } + } + ``` + +1. Run the `can-to-someip` program with the configuration file as follows. If you are running this + program on a machine connected to a real CAN network adapter, you can also modify the `vcan0` + value for the `--can-interface` argument to `can0` (for example). + + ```bash + VSOMEIP_CONFIGURATION=vsomeip-can-to-some-ip.json ./can-to-someip \ + --can-interface vcan0 \ + --service-id 0x7777 \ + --instance-id 0x5678 \ + --event-id 0x8778 \ + --event-group-id 0x5555 + ``` + +1. Create a JSON configuration file called `vsomeip-fwe.json` for FWE, replacing `` with + the IPv4 address of the machine running FWE: + + ```json + { + "unicast": "", + "netmask": "255.255.0.0", + "logging": { + "level": "trace", + "console": "true", + "dlt": "false" + }, + "applications": [ + { + "name": "someipToCanBridgeInterface", + "id": "0x1313" + } + ], + "service-discovery": { + "enable": "true", + "multicast": "224.224.224.245", + "port": "30490", + "protocol": "udp", + "initial_delay_min": "10", + "initial_delay_max": "100", + "repetitions_base_delay": "200", + "repetitions_max": "3", + "ttl": "3", + "cyclic_offer_delay": "2000", + "request_response_delay": "1500" + } + } + ``` + +1. Run FWE with the configuration file as follows: + + ```bash + VSOMEIP_CONFIGURATION=vsomeip-fwe.json ./aws-iot-fleetwise-edge config-0.json + ``` + + If successfully configured, you should see the following in the FWE log: + + ``` + [debug] Joining to multicast group 224.224.224.245 from + [info] SOME/IP routing ready. + ``` + +1. You can now [run the cloud demo script](#run-the-aws-iot-fleetwise-demo-script). + +### Troubleshooting + +Common issues encountered when trying to establish a SOME/IP connection over UDP include: + +- **Trying to use a local loopback address.** It is not possible to use the local loopback IP + address `127.0.0.1` to run the demo over UDP on one machine, as the local loopback interface does + not support UDP multicast, which is required by SOME/IP service discovery. + +- **A firewall blocking open UDP ports.** To open the two UDP ports used in the above example, run + the following on the machine running `can-to-someip`: + + ```bash + sudo iptables -A INPUT -p udp -m udp --dport 30490 -j ACCEPT + sudo iptables -A INPUT -p udp -m udp --dport 30509 -j ACCEPT + ``` + +- **A bug in the `vsomeip` library that causes service discovery to fail** in versions >=3.3.0 + <3.5.0. This bug was fixed with this GitHub PR: https://github.com/COVESA/vsomeip/pull/591. diff --git a/docs/dev-guide/custom-function-dev-guide.md b/docs/dev-guide/custom-function-dev-guide.md new file mode 100644 index 00000000..a266443f --- /dev/null +++ b/docs/dev-guide/custom-function-dev-guide.md @@ -0,0 +1,580 @@ +# Custom function developer guide + + +> [!NOTE] +> This is a "gated" feature of AWS IoT FleetWise for which you will need to request access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) +> for more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +The 'custom function' feature of AWS IoT FleetWise allows customers to define functions at the edge +and call these functions by name from within condition-based campaign expressions. + +The functions may take a variable number of arguments of varying type, and may return a single value +of varying type. The supported types are `bool`, `double` or `string`. It is possible to use a +custom function anywhere within a campaign expression, including as an argument to logical or +artithmetic operators, or as an argument to another custom function, i.e. nesting custom function +calls. + +Within the campaign expression a custom function is invoked using the keyword `custom_function` +followed by parentheses, containing firstly the name of the custom function to invoke as a string +literal, and then zero or more arguments to the function. Custom function signatures are of the +following format including the function return type, function name, and function argument types and +names: + +``` + custom_function('', , ...); +``` + +This guide details the example custom functions provided in the Reference Implementation for AWS IoT +FleetWise (FWE), followed by a step-by-step guide to running FWE and sending campaigns to the edge +using the custom functions, and finally a developer guide for developing your own custom functions. + +## Example custom functions + +### Math custom functions: `abs`, `min`, `max`, `pow`, `log`, `ceil`, `floor` + +These example custom functions implement math operations, and have the following function +signatures: + +```php +double custom_function('abs', double x); // Calculates the absolute (modulus) of the argument + +double custom_function('min', double x, double y, ...); // Calculates the minimum of two or more arguments + +double custom_function('max', double x, double y, ...); // Calculates the maximum of two or more arguments + +double custom_function('pow', double x, double y); // Calculates x to the power y + +double custom_function('log', double x, double y); // Calculates the logarithm of y to the base x + +double custom_function('ceil', double x); // Calculates the 'ceiling', i.e. smallest integer greater than or equal to the argument + +double custom_function('floor', double x); // Calculates the 'floor', i.e. greatest integer less than or equal to the argument +``` + +For example if you wanted to trigger data collection when the magnitude of a vector signal with +components `Vehicle.MyVectorSignal.x` and `Vehicle.MyVectorSignal.y` is greater than 100, then you +could use the following campaign. I.e. the expression is equivalent to the equation: + +$\sqrt{Vehicle.MyVectorSignal.x^2 + Vehicle.MyVectorSignal.y^2} > 100$ + +```json +{ + "compression": "SNAPPY", + "collectionScheme": { + "conditionBasedCollectionScheme": { + "conditionLanguageVersion": 1, + "expression": "custom_function('pow', custom_function('pow', $variable.`Vehicle.MyVectorSignal.x`, 2) + custom_function('pow', $variable.`Vehicle.MyVectorSignal.y`, 2), 0.5) > 100", + "triggerMode": "RISING_EDGE" + } + }, + "signalsToCollect": [ + { + "name": "Vehicle.MyVectorSignal.x" + }, + { + "name": "Vehicle.MyVectorSignal.y" + } + ] +} +``` + +### Custom function `MULTI_RISING_EDGE_TRIGGER` + +The `MULTI_RISING_EDGE_TRIGGER` example custom function is used to trigger data collection on the +rising edge of one or more Boolean conditions, and capture which of the conditions caused the data +collection. This functionality can be used to monitor many Boolean 'alarm' signals in a single AWS +IoT FleetWise campaign. The function signature is as follows: + +```php +bool custom_function('MULTI_RISING_EDGE_TRIGGER', + string conditionName1, bool condition1, // Condition 1: name and value + string conditionName2, bool condition2, // Condition 2: name and value + string conditionName3, bool condition3, // Condition 3: name and value + ... +); +``` + +The function takes a variable number of pairs of arguments, with each being the `string` name of a +condition and the `bool` value of that condition. When one or more of the conditions has a rising +edge, i.e. changing from false to true, the function will return true, otherwise it returns false. +Additionally when it returns true, it generates a JSON string containing the condition names that +caused the trigger and adds this to the collected data with the signal name +`Vehicle.MultiRisingEdgeTrigger`. This signal must be added to the signal catalog and decoder +manifest in the normal manner, and then added to the campaign's `signalsToCollect` in order to +capture the trigger source. + +For example, if you wanted to trigger data collection on the rising edge of any one of three Boolean +signals: `Vehicle.Alarm1`, `Vehicle.Alarm2`, `Vehicle.Alarm3`, you could create a condition based +campaign using `MULTI_RISING_EDGE_TRIGGER` as follows: + +```json +{ + "compression": "SNAPPY", + "collectionScheme": { + "conditionBasedCollectionScheme": { + "conditionLanguageVersion": 1, + "expression": "custom_function('MULTI_RISING_EDGE_TRIGGER', 'ALARM1', $variable.`Vehicle.Alarm1`, 'ALARM2', $variable.`Vehicle.Alarm2`, 'ALARM3', $variable.`Vehicle.Alarm3`)", + "triggerMode": "RISING_EDGE" + } + }, + "signalsToCollect": [ + { + "name": "Vehicle.MultiRisingEdgeTrigger" + } + ] +} +``` + +If the signals `Vehicle.Alarm1` and `Vehicle.Alarm3` were to have a rising edge at exactly the same +time, the collected value of `Vehicle.MultiRisingEdgeTrigger` would have the following value to +indicate that these were the cause of the data collection: + +```json +["ALARM1", "ALARM3"] +``` + +## Step-by-step guide + +This section of the guide goes through building and running FWE with support for the example custom +functions, and creating campaigns using them. + +### Prerequisites + +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Linux or MacOS machine. + +### Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +1. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + 1. Do not include the file suffix `.pem`. + 1. If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +1. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +1. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +### Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +### Download or build the FWE binary + +**To quickly run the demo**, download the pre-built FWE binary: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh \ + && sudo ldconfig + ``` + +1. Compile FWE with the custom function examples: + + ```bash + ./tools/build-fwe-native.sh --with-custom-function-examples + ``` + +### Install the CAN simulator + +1. Run the following command to install the CAN simulator: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-socketcan.sh \ + && sudo -H ./tools/install-cansim.sh + ``` + +### Provision and run FWE + +1. Run the following _on the development machine_ to provision an AWS IoT Thing with credentials: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdev-custom-functions \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --can-bus0 vcan0 \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` \ + --enable-named-signal-interface + ``` + +1. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + +### Run the AWS IoT FleetWise demo script + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +vehicle model, register the virtual vehicle created in the previous section and run a campaign to +collect data from it. + +1. Open a new terminal _on the development machine_ and run the following to install the + dependencies of the demo script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + +1. Run the following command to generate 'node' and 'decoder' JSON files from the input DBC file: + + ```bash + python3 dbc-to-nodes.py hscan.dbc can-nodes.json \ + && python3 dbc-to-decoders.py hscan.dbc can-decoders.json + ``` + +1. Choose which custom function you would like to evaluate: + + 1. To evaluate the math functions, run this command. The campaign treats the signals + `Vehicle.ECM.DemoEngineTorque` and `Vehicle.ABS.DemoBrakePedalPressure` as components of a + 2-dimensional vector and triggers data collection when the magnitude of the vector is greater + than 100. See [campaign-math.json](../../tools/cloud/campaign-math.json). + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdev-custom-functions \ + --node-file can-nodes.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-can.json \ + --campaign-file campaign-math.json \ + --data-destination IOT_TOPIC + ``` + + 1. To evaluate the `MULTI_RISING_EDGE_TRIGGER` custom function, run this command. The campaign + will be triggered when either the signal `Vehicle.ECM.DemoEngineTorque` is greater than 500 + (`ALARM1`) or signal `Vehicle.ABS.DemoBrakePedalPressure` is greater than 7000 (`ALARM2`). See + [campaign-multi-rising-edge-trigger.json](../../tools/cloud/campaign-multi-rising-edge-trigger.json). + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdev-custom-functions \ + --node-file can-nodes.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-can.json \ + --node-file custom-nodes-multi-rising-edge-trigger.json \ + --decoder-file custom-decoders-multi-rising-edge-trigger.json \ + --network-interface-file network-interface-custom-named-signal.json \ + --campaign-file campaign-multi-rising-edge-trigger.json \ + --data-destination IOT_TOPIC + ``` + +1. When the script completes, a path to an HTML file is given. _On your local machine_, use `scp` to + download it, then open it in your web browser: + + ```bash + scp -i ubuntu@: . + ``` + +### Clean up + +1. Run the following _on the development machine_ to clean up resources created by the + `provision.sh` and `demo.sh` scripts. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && ./clean-up.sh \ + && ../provision.sh \ + --vehicle-name fwdev-custom-functions \ + --region us-east-1 \ + --only-clean-up + ``` + +1. Delete the CloudFormation stack for your development machine, which by default is called `fwdev`: + https://us-east-1.console.aws.amazon.com/cloudformation/home + +## Developing your own custom functions + +To develop your own custom function in C++, you must write code to meet the +`CustomFunctionCallbacks` interfaces: + +```cpp +using CustomFunctionInvokeCallback = std::function &args)>; +using CustomFunctionConditionEndCallback = std::function &collectedSignalIds, + Timestamp timestamp, + CollectionInspectionEngineOutput &output)>; +using CustomFunctionCleanupCallback = std::function; + +struct CustomFunctionCallbacks { + CustomFunctionInvokeCallback invokeCallback; + CustomFunctionConditionEndCallback conditionEndCallback; + CustomFunctionCleanupCallback cleanupCallback; +}; +``` + +### Interface `invokeCallback` + +At minimum, if your function does not need to save state information between calls, you only need to +define the `invokeCallback` and can set `conditionEndCallback` and `cleanupCallback` to `nullptr`. +For example, the following would implement a `sin` function to calculate the mathematical sine of +the argument: + +```cpp +#include "CollectionInspectionAPITypes.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +CustomFunctionInvokeResult customFunctionSin( + CustomFunctionInvocationID invocationId, + const std::vector &args) { + static_cast(invocationId); + if (args.size() != 1) { + return ExpressionErrorCode::TYPE_MISMATCH; + } + if (args[0].isUndefined()) { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if (!args[0].isBoolOrDouble()){ + return ExpressionErrorCode::TYPE_MISMATCH; + } + return {ExpressionErrorCode::SUCCESSFUL, std::sin(args[0].asDouble())}; +} + +} // namespace IoTFleetWise +} // namespace Aws +``` + +Then the custom function can be registered in your bootstrap code as follows: + +```cpp +mCollectionInspectionEngine->registerCustomFunction( + "sin", + {customFunctionSin, nullptr, nullptr} +); +``` + +### Interface `conditionEndCallback` + +If you would like to add to the collected data when the campaign triggers data collection, you can +also implement the `conditionEndCallback` which is called after evaluation of each condition. To do +this you will need to know the signal ID to add the data, so you will probably want to use the +`NamedSignalDataSource` to lookup the signal ID for a given fully-qualified-name. For example, if +you would like to implement a function that returns the file size of a given filename, and adds this +to the collected data with the signal name `Vehicle.FileSize`: + +```cpp +#include "CollectionInspectionAPITypes.h" +#include "LoggingModule.h" +#include "NamedSignalDataSource.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class CustomFunctionFileSize +{ +public: + CustomFunctionFileSize(std::shared_ptr namedSignalDataSource) + : mNamedSignalDataSource(std::move(namedSignalDataSource)) {} + CustomFunctionInvokeResult invoke( + CustomFunctionInvocationID invocationId, + const std::vector &args) { + static_cast(invocationId); + if ((args.size() != 1) || (!args[0].isString())) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + boost::filesystem::path filePath(*args[0].stringVal); + mFileSize = static_cast(boost::filesystem::file_size(filePath)); + return {ExpressionErrorCode::SUCCESSFUL, mFileSize}; + } + void conditionEnd( + const std::unordered_set &collectedSignalIds, + Timestamp timestamp, + CollectionInspectionEngineOutput &output) { + // Only add to the collected data if we have a valid value: + if (mFileSize < 0) { + return; + } + // Clear the current value: + auto size = mFileSize; + mFileSize = -1; + // Only add to the collected data if collection was triggered: + if (!output.triggeredCollectionSchemeData) { + return; + } + auto signalId = mNamedSignalDataSource->getNamedSignalID("Vehicle.FileSize"); + if (signalId == INVALID_SIGNAL_ID) { + FWE_LOG_WARN("Vehicle.FileSize not present in decoder manifest"); + return; + } + if (collectedSignalIds.find(signalId) == collectedSignalIds.end()) { + return; + } + output.triggeredCollectionSchemeData->signals.emplace_back( + CollectedSignal{signalId, timestamp, size, SignalType::DOUBLE}); + } +private: + std::shared_ptr mNamedSignalDataSource; + int mFileSize{-1}; +}; + +} // namespace IoTFleetWise +} // namespace Aws +``` + +Then the custom function can be registered in your bootstrap code as follows: + +```cpp +auto fileSizeFunc = std::make_shared(mNamedSignalDataSource); +mCollectionInspectionEngine->registerCustomFunction( + "file_size", + { + [fileSizeFunc](auto invocationId, const auto &args) -> CustomFunctionInvokeResult { + return fileSizeFunc->invoke(invocationId, args); + }, + [fileSizeFunc](const auto &collectedSignalIds, auto timestamp, auto &collectedData) { + fileSizeFunc->conditionEnd(collectedSignalIds, timestamp, collectedData); + }, + nullptr + } +); +``` + +### Interface `cleanupCallback` + +Lastly if you would like to store some state information in between calls to the custom function, +you should use the `invocationId` argument to the `invokeCallback` to uniquely identify each +invocation of the function, and implement the `cleanupCallback` to cleanup the old state information +at the end of the lifetime of the function. The `invocationId` will be the same each time the +function is called for the lifetime of the campaign. For example, if you would like to implement a +custom function that returns a counter that is incremented each time the function is called: + +```cpp +#include "CollectionInspectionAPITypes.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class CustomFunctionCounter +{ +public: + CustomFunctionInvokeResult invoke( + CustomFunctionInvocationID invocationId, + const std::vector &args) { + static_cast(args); + // Create a new counter if the invocationId is new, or get the existing counter: + auto &counter = mCounters.emplace(invocationId, 0).first->second; + return {ExpressionErrorCode::SUCCESSFUL, counter++}; + } + void cleanup(CustomFunctionInvocationID invocationId) { + mCounters.erase(invocationId); + } +private: + std::unordered_map mCounters; +}; + +} // namespace IoTFleetWise +} // namespace Aws +``` + +Then the custom function can be registered in your bootstrap code as follows: + +```cpp +auto counterFunc = std::make_shared(); +mCollectionInspectionEngine->registerCustomFunction( + "counter", + { + [counterFunc](auto invocationId, const auto &args) -> CustomFunctionInvokeResult { + return counterFunc->invoke(invocationId, args); + }, + nullptr, + [counterFunc](auto invocationId) { + counterFunc->cleanup(invocationId); + } + } +); +``` diff --git a/docs/dev-guide/edge-agent-dev-guide-device-shadow-over-someip.md b/docs/dev-guide/edge-agent-dev-guide-device-shadow-over-someip.md new file mode 100644 index 00000000..23f81091 --- /dev/null +++ b/docs/dev-guide/edge-agent-dev-guide-device-shadow-over-someip.md @@ -0,0 +1,239 @@ +# Demo of AWS IoT FleetWise for Device Shadow over SOME/IP + +This guide demonstrates AWS IoT FleetWise (FWE) which supports Device Shadow over SOME/IP service. + +## Overview of Device Shadow over SOME/IP service + +[Franca IDL](https://github.com/franca/franca) is the industry standard schema format for specifying +SOME/IP messages, and [CommonAPI](https://covesa.github.io/capicxx-core-tools/) is the industry +standard serialization format for SOME/IP. Franca IDL files are called 'FIDL' files, which are +transport-layer-independent, and their 'deployment' on SOME/IP is specified in 'FDEPL' files that +specify which SOME/IP service transports each message. The CommonAPI library provides a code +generator that takes the FIDL and FDEPL files and generates C++ code to implement the Franca +interfaces for both the client and server. + +FWE support access on +[AWS IoT Device Shadow service](https://docs.aws.amazon.com/iot/latest/developerguide/iot-device-shadows.html) +via SOME/IP. It supports methods for deleting, getting and updating device shadows. When the +application calls these methods, FWE prepare and publish appropriate request messages on +correspondent topics to the server. It then gather responses from the server and provide result to +the application. FWE also supports broadcasting an event when a device shadow changes (from the +server) to registered applications. For more details on supported methods and event, please see +[DeviceShadowOverSomeipInterface.fidl](../../interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fidl) +and +[DeviceShadowOverSomeipInterface.fdepl](../../interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fdepl). + +FWE is compiled to support the Device Shadow over SOME/IP interface. A Device Shadow over SOME/IP +simulator called `someip_device_shadow_editor` is provided in order to simulate another node in the +system which plays the role of an example application that calls/handles aforementioned +methods/event. `someip_device_shadow_editor` is also compiled with support for Device Shadow over +SOME/IP interface. + +In this demo, firstly FWE is provisioned and configured to run with Device Shadow over SOME/IP +service. Secondly `someip_device_shadow_editor` is used to show how the application accesses AWS IoT +Device Shadow service via SOME/IP. + +## Prerequisites + +- Access to an AWS Account with administrator privileges. +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Windows, Linux or MacOS machine. + +## Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +2. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + - Do not include the file suffix `.pem`. + - If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +3. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +4. Choose **Create stack**. +5. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +6. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +## Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +## Download or build the FWE binary + +**To quickly run the demo**, download the pre-built FWE binary: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz tools/someip_device_shadow_editor/someip_device_shadow_editor.so + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz tools/someip_device_shadow_editor/someip_device_shadow_editor.so + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE with SOME/IP support: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh --with-someip-support \ + && sudo ldconfig + ``` + +1. Compile FWE with SOME/IP support and the SOME/IP simulator: + + ```bash + ./tools/build-fwe-native.sh --with-someip-support + ``` + +## Provision and run FWE + +1. Open a new terminal _on the development machine_, and run the following to provision credentials + for the vehicle and configure the vehicle accordingly: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-device-shadow-over-someip \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` \ + && OUTPUT_CONFIG=`jq -r '.staticConfig.deviceShadowOverSomeip={"someipApplicationName":"deviceShadowOverSomeipInterface"}' build_config/config-0.json` \ + && echo "${OUTPUT_CONFIG}" > build_config/config-0.json + ``` + +2. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + + You should see the following messages in the log indicating that FWE has successfully launched + Device Shadow over SOME/IP service: + + ``` + [info] io thread id from application: 0100 (someipDeviceShadowService) is: 7f7f7974d700 TID: 2832 + [info] io thread id from application: 0100 (someipDeviceShadowService) is: 7f7f72ffd700 TID: 2836 + [info] vSomeIP 3.4.10 | (default) + ``` + +## Programmatically call Device Shadow over SOME/IP methods + +A simulator is used to model another node in the vehicle network that calls/handles Device Shadow +over SOME/IP methods/event. + +1. Start the Device Shadow over SOME/IP simulator: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/someip_device_shadow_editor \ + && python3 someip_device_shadow_editor_sim.py + ``` + +## Interactively call Device Shadow over SOME/IP methods + +Instead of running Device Shadow over SOME/IP simulator with pre-defined sequence of methods, you +can try calling any method manually. + +1. Open a new terminal _on the development machine_ and run the alternate script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/someip_device_shadow_editor \ + && python3 someip_device_shadow_editor_repl.py + ``` + +2. Wait until it successfully subscribes to Device Shadow over SOME/IP service. + + ``` + [info] ON_AVAILABLE(0101): [1235.5679:1.0] + [info] SUBSCRIBE ACK(0100): [1235.5679.80f2.80f2] + ``` + +3. Call any method manually. + + - Type `help` to get familiarity with command usage. + - To get shadow, type `get `, where `` can be blank to get the + 'classic' shadow, **but the space after `get` is still required**. + + ``` + get + get shadow-x + ``` + + - To update shadow, type `update `, where `` can be + blank to update the 'classic' shadow, **but the space after `update` is still required**. + + ``` + update {"state":{"desired":{"temperature":25},"reported":{"temperature":22}}} + update shadow-x {"state":{"desired":{"type":"named shadow"},"reported":{"type":"named shadow x"}}} + ``` + + - To delete shadow, type `delete `, where `` can be blank to delete the + 'classic' shadow, **but the space after `delete` is still required**. + + ``` + delete + delete shadow-x + ``` + + - Type `exit` or `quit` to stop. + +## Clean up + +1. Run the following _on the development machine_ to clean up resources created by the + `provision.sh`. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/ \ + && ./provision.sh \ + --vehicle-name fwdemo-device-shadow-over-someip \ + --region us-east-1 \ + --only-clean-up + ``` diff --git a/docs/dev-guide/edge-agent-dev-guide-last-known-state.md b/docs/dev-guide/edge-agent-dev-guide-last-known-state.md new file mode 100644 index 00000000..017ef227 --- /dev/null +++ b/docs/dev-guide/edge-agent-dev-guide-last-known-state.md @@ -0,0 +1,396 @@ +# "Last Known State" demo for AWS IoT FleetWise + + +> [!NOTE] +> This is a "gated" feature of AWS IoT FleetWise for which you will need to request access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) +> for more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +The "Last Known State" (LKS) feature of AWS IoT FleetWise provides a lightweight method to collect +signal data from vehicles and forward it to downstream consumers via IoT topic. The AWS IoT +FleetWise signal catalog, vehicle model, and decoder manifest are created as normal (for detailed +information regarding these resource types, refer to the +[AWS IoT FleetWise Developer Guide](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/)). +Instead of using a campaign to collect data, a state template is created to specify which signals to +collect - either periodically or on-change of signal value. These signals are then collected by the +Reference Implementation for AWS IoT FleetWise (FWE), and sent to the AWS IoT FleetWise cloud via +MQTT in Protobuf format. The AWS IoT FleetWise cloud then decodes and forwards the data again via +IoT topic in Protobuf format to the customer's application. This is illustrated in the following +diagram. + + + +## Prerequisites + +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Windows, Linux or MacOS machine. + +## Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +1. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + 1. Do not include the file suffix `.pem`. + 1. If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +1. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +1. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +## Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +## Download or build the FWE binary + +**To quickly run the demo**, download the pre-built FWE binary: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE : + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh \ + && sudo ldconfig + ``` + +1. Compile FWE with Last Known State support: + + ```bash + ./tools/build-fwe-native.sh --with-lks-support + ``` + +## Install the CAN simulator + +```bash +cd ~/aws-iot-fleetwise-edge \ +&& sudo -H ./tools/install-socketcan.sh \ +&& sudo -H ./tools/install-cansim.sh +``` + +## Provision and run FWE + +1. Run the following _on the development machine_ to provision an AWS IoT Thing with credentials: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-lks \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --can-bus0 vcan0 \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` + ``` + +1. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + + You should see a message similar to the following in the log, indicating that Last Known State is + enabled: + + ``` + [LastKnownStateWorkerThread.cpp:60] [start()]: [Last Known State Inspection Thread started] + ``` + +## Run the AWS IoT FleetWise demo script + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +vehicle model and register the virtual vehicle created in the previous section: + +1. Open a new terminal _on the development machine_ and run the following to install the + dependencies of the demo script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + +1. Run the following command to generate 'node' and 'decoder' JSON files from the input DBC file: + + ```bash + python3 dbc-to-nodes.py hscan.dbc can-nodes.json \ + && python3 dbc-to-decoders.py hscan.dbc can-decoders.json + ``` + +1. Run the demo script: + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-lks \ + --node-file can-nodes.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-can.json + ``` + +1. When the script completes, you should see messages in FWE log indicating that it received a + decoder manifest. You should also see a periodic `CHECKIN: ` message showing the decoder + manifest. + +## Create LKS Template + +1. Run the following command to create a state template to collect the brake pedal pressure and + engine torque: + + ```bash + SIGNAL_CATALOG_ARN=`aws iotfleetwise list-signal-catalogs \ + --region us-east-1 \ + | jq -r ".summaries[0].arn"` \ + && aws iotfleetwise create-state-template \ + --region us-east-1 \ + --signal-catalog-arn ${SIGNAL_CATALOG_ARN} \ + --name fwdemo-lks-template \ + --state-template-properties ' + [ + "Vehicle.ABS.DemoBrakePedalPressure", + "Vehicle.ECM.DemoEngineTorque" + ]' + ``` + +1. Run the following command to deploy the state template to the vehicle with the update strategy of + `onChange` to cause FWE to send data when the signal value changes: + + ```bash + aws iotfleetwise update-vehicle \ + --region us-east-1 \ + --vehicle-name fwdemo-lks \ + --state-templates-to-add ' + [ + { + "identifier": "fwdemo-lks-template", + "stateTemplateUpdateStrategy": { + "onChange": {} + } + } + ]' + ``` + +## Subscribe to MQTT topic to access collected data + +At this point the state template has been deployed to the vehicle and the vehicle is sending the +collected data to AWS IoT FleetWise cloud. In the next step a Python script is used to subscribe to +the LKS data topic to receive the collected data for the given state template. + +1. Since the data is serialized in Protobuf format, it is necessary to firstly generate the Python + bindings for the message file + [last_known_state_message.proto](../../interfaces/protobuf/schemas/cloudToCustomer/last_known_state_message.proto) + as follows: + + ```bash + source ../install-deps-versions.sh \ + && if [ "$(uname -m)" == "aarch64" ]; then PROTOBUF_ARCH="aarch_64"; else PROTOBUF_ARCH="$(uname -m)"; fi \ + && PROTOC_PACKAGE="protoc-${VERSION_PROTOBUF_RELEASE/v/}-linux-${PROTOBUF_ARCH}" \ + && wget https://github.com/protocolbuffers/protobuf/releases/download/${VERSION_PROTOBUF_RELEASE}/${PROTOC_PACKAGE}.zip -O ~/${PROTOC_PACKAGE}.zip \ + && unzip ~/${PROTOC_PACKAGE}.zip -d ~/${PROTOC_PACKAGE} \ + && ~/${PROTOC_PACKAGE}/bin/protoc \ + -I=../../interfaces/protobuf/schemas/cloudToCustomer \ + --python_out . \ + ../../interfaces/protobuf/schemas/cloudToCustomer/last_known_state_message.proto + ``` + +1. Finally run the following Python script to subscribe to the collected data: + + ```bash + python3 lks-subscribe.py \ + --region us-east-1 \ + --vehicle-name fwdemo-lks \ + --template-name fwdemo-lks-template + ``` + + When a state template is deployed to a vehicle, it doesn't start collecting data right away. We + need to send commands to Activate the state templates. + + 1. Open a new terminal _on the development machine_ and run the following to create an IAM role + to create the command payloads: + + ```bash + SERVICE_ROLE_ARN=`./manage-service-role.sh \ + --service-role IoTCreateCommandPayloadServiceRole \ + --service-principal iot.amazonaws.com \ + --actions iotfleetwise:GenerateCommandPayload \ + --resources '*'` + ``` + + 1. Run the following to create a command: + + ```bash + aws iot create-command \ + --region us-east-1 \ + --role-arn ${SERVICE_ROLE_ARN} \ + --command-id fwdemo-lks-command \ + --namespace "AWS-IoTFleetWise" \ + --mandatory-parameters '[ + {"name":"$stateTemplate.name"}, + {"name":"$stateTemplate.operation"} + ]' + ``` + + 1. Send a command to FWE: + + ```bash + JOBS_ENDPOINT_URL=`aws iot describe-endpoint --region us-east-1 --endpoint-type iot:Jobs | jq -j .endpointAddress` \ + && ACCOUNT_ID=`aws sts get-caller-identity | jq -r .Account` \ + && aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/fwdemo-lks-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-lks \ + --parameters '{ + "$stateTemplate.name": { "S": "fwdemo-lks-template" }, + "$stateTemplate.operation": { "S": "fetchSnapshot" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} + ``` + + 1. You can then try different commands by repeating the command above but passing `activate` or + `deactivate` as `$stateTemplate.operation`. + + 1. Since the `activate` operation takes additional parameters, you have to create a separate + command if you want to set those parameters: + + ```bash + aws iot create-command \ + --region us-east-1 \ + --role-arn ${SERVICE_ROLE_ARN} \ + --command-id fwdemo-lks-command-activate \ + --namespace "AWS-IoTFleetWise" \ + --mandatory-parameters '[ + {"name":"$stateTemplate.name"}, + {"name":"$stateTemplate.operation"}, + {"name":"$stateTemplate.deactivateAfterSeconds","defaultValue":{"L":60}} + ]' + ``` + + 1. Then send an `activate` command that will automatically deactivate the state template after a + few seconds: + + ```bash + JOBS_ENDPOINT_URL=`aws iot describe-endpoint --region us-east-1 --endpoint-type iot:Jobs | jq -j .endpointAddress` \ + && ACCOUNT_ID=`aws sts get-caller-identity | jq -r .Account` \ + && aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/fwdemo-lks-command-activate \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-lks \ + --parameters '{ + "$stateTemplate.name": { "S": "fwdemo-lks-template" }, + "$stateTemplate.operation": { "S": "activate" }, + "$stateTemplate.deactivateAfterSeconds": { "L": 20 } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} + ``` + + The collected data should be similar to the following: + + ``` + Established mqtt subscription to $aws/iotfleetwise/vehicles/fwdemo-lks/last_known_state/fwdemo-lks-template/data with [] + Received message on topic: $aws/iotfleetwise/vehicles/fwdemo-lks/last_known_state/fwdemo-lks-template/data + Received message: { + "timeMs": "1712670086473", + "signals": [ + { + "name": "Vehicle.ABS.DemoBrakePedalPressure", + "doubleValue": 375.0 + } + ] + } + Received message on topic: $aws/iotfleetwise/vehicles/fwdemo-lks/last_known_state/fwdemo-lks-template/data + Received message: { + "timeMs": "1712670091473", + "signals": [ + { + "name": "Vehicle.ECM.DemoEngineTorque", + "doubleValue": 100.0 + }, + { + "name": "Vehicle.ABS.DemoBrakePedalPressure", + "doubleValue": 225.0 + } + ] + } + ``` + +## Clean up + +1. Run the following to clean up resources created by the `provision.sh` and `demo.sh` scripts, and + the calls to create and add the state template: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && aws iotfleetwise update-vehicle \ + --region us-east-1 \ + --vehicle-name fwdemo-lks \ + --state-templates-to-remove fwdemo-lks-template \ + && aws iotfleetwise delete-state-template \ + --identifier fwdemo-lks-template \ + --region us-east-1 \ + && ./clean-up.sh + ``` + +1. Delete the CloudFormation stack created earlier, which by default is called `fwdemo-lks`: + https://us-east-1.console.aws.amazon.com/cloudformation/home diff --git a/docs/dev-guide/edge-agent-dev-guide-nxp-s32g.md b/docs/dev-guide/edge-agent-dev-guide-nxp-s32g.md index 5af8bed0..96d99e48 100644 --- a/docs/dev-guide/edge-agent-dev-guide-nxp-s32g.md +++ b/docs/dev-guide/edge-agent-dev-guide-nxp-s32g.md @@ -104,6 +104,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ && mkdir -p config \ && cd config \ && ../tools/provision.sh \ + --region us-east-1 \ --vehicle-name fwdemo-s32g \ --certificate-pem-outfile certificate.pem \ --private-key-outfile private-key.key \ @@ -163,6 +164,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ ```bash cd ~/aws-iot-fleetwise-edge/tools/cloud \ && ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-s32g \ --node-file obd-nodes.json \ --decoder-file obd-decoders.json \ diff --git a/docs/dev-guide/edge-agent-dev-guide-renesas-rcar-s4.md b/docs/dev-guide/edge-agent-dev-guide-renesas-rcar-s4.md index 8af46241..59b367b9 100644 --- a/docs/dev-guide/edge-agent-dev-guide-renesas-rcar-s4.md +++ b/docs/dev-guide/edge-agent-dev-guide-renesas-rcar-s4.md @@ -115,6 +115,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ && mkdir -p config \ && cd config \ && ../tools/provision.sh \ + --region us-east-1 \ --vehicle-name fwdemo-rcars4 \ --certificate-pem-outfile certificate.pem \ --private-key-outfile private-key.key \ @@ -179,6 +180,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ cd ~/aws-iot-fleetwise-edge/tools/cloud sudo -H ./install-deps.sh ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-rcars4 \ --node-file obd-nodes.json \ --decoder-file obd-decoders.json \ diff --git a/docs/dev-guide/edge-agent-dev-guide-someip.md b/docs/dev-guide/edge-agent-dev-guide-someip.md new file mode 100644 index 00000000..46ba9533 --- /dev/null +++ b/docs/dev-guide/edge-agent-dev-guide-someip.md @@ -0,0 +1,681 @@ +# Demo of AWS IoT FleetWise for SOME/IP + + +> [!NOTE] +> This guide makes use of "gated" features of AWS IoT FleetWise for which you will need to request +> access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for +> more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +This guide demonstrates how to use AWS IoT FleetWise to collect SOME/IP data and execute SOME/IP +methods. + +[Franca IDL](https://github.com/franca/franca) is the industry standard schema format for specifying +SOME/IP messages, and [CommonAPI](https://covesa.github.io/capicxx-core-tools/) is the industry +standard serialization format for SOME/IP. Franca IDL files are called 'FIDL' files, which are +transport-layer-independent, and their 'deployment' on SOME/IP is specified in 'FDEPL' files that +specify which SOME/IP service transports each message. The CommonAPI library provides a code +generator that takes the FIDL and FDEPL files and generates C++ code to implement the Franca +interfaces for both the client and server. + +In this demonstration an example FIDL file +[ExampleSomeipInterface.fidl](../../interfaces/someip/fidl/ExampleSomeipInterface.fidl) and an +example FDEPL file +[ExampleSomeipInterface.fdepl](../../interfaces/someip/fidl/ExampleSomeipInterface.fdepl) are +provided. The Reference Implementation for AWS IoT FleetWise (FWE) is compiled to support these +example interfaces. A SOME/IP simulator called `someipigen` is provided in order to simulate another +node in the system that publishes the SOME/IP data and offers SOME/IP methods that FWE will execute. +`someipigen` is also compiled with support for these example interfaces. + +**Note:** Since the generated code and the corresponding glue code in FWE is all specific to the +FIDL and FDEPL files, changing FWE to support different FIDL/FDEPL files will require code changes. +For more information in adapting the FWE code to support your FIDL/FDEPL files, refer to +[adding-custom-fidl-file-dev-guide](./adding-custom-fidl-file-dev-guide.md). + +In this demo, firstly FWE is provisioned and run to collect SOME/IP data and upload it to the cloud. +The data is received from the IoT topic and plotted in an HTML graph format. Secondly a 'remote +command' is triggered from the cloud which causes FWE to execute a SOME/IP method. The result of the +command execution is then retrieved from the cloud. + +### Overview of SOME/IP data collection + +The following diagram illustrates the dataflow and artifacts consumed and produced by this demo in +the context of SOME/IP data collection: + + + +### Overview of SOME/IP remote commands + +The following diagram illustrates the dataflow and artifacts consumed and produced by this demo in +the context of SOME/IP method execution via the 'remote commands' feature: + + + +## Prerequisites + +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Linux or MacOS machine. + +## Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +1. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + 1. Do not include the file suffix `.pem`. + 1. If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +1. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +1. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +## Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +## Download or build the FWE binary + +**To quickly run the demo**, download the pre-built FWE binary: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz tools/someipigen/someipigen.so + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz tools/someipigen/someipigen.so + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE with SOME/IP support: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh --with-someip-support \ + && sudo ldconfig + ``` + +1. Compile FWE with SOME/IP support and the SOME/IP simulator: + + ```bash + ./tools/build-fwe-native.sh --with-someip-support + ``` + +## Start the SOME/IP simulator + +A simulator is used to model another node in the vehicle network that publishes SOME/IP data and +offers SOME/IP methods that can be executed. + +1. Start the SOME/IP simulator: + + ```bash + cd tools/someipigen \ + && python3 someipsim.py + ``` + + **Note:** When the SOME/IP simulator and FWE run on the same machine, `vsomeip` uses a UNIX + domain socket for communication rather than IP communication. If you are interested in SOME/IP + communication over IP, see [Running over IP](#running-over-ip). + +## Provision and run FWE + +1. Open a new terminal _on the development machine_, and run the following to provision credentials + for the vehicle and configure the network interface for SOME/IP collection: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-someip \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` \ + --enable-someip-interface \ + --session-expiry-interval-seconds 3600 + ``` + +1. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + + You should see the following messages in the log indicating that FWE has successfully subscribed + to the SOME/IP service: + + ``` + [info] ON_AVAILABLE(0101): [1234.5678:1.0] + [info] SUBSCRIBE ACK(0102): [1234.5678.80f2.80f2] + [info] SUBSCRIBE ACK(0102): [1234.5678.80f3.80f3] + ``` + +## Run the AWS IoT FleetWise demo script + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +vehicle model, register the virtual vehicle created in the previous section and run a campaign to +collect data from it. + +1. Open a new terminal _on the development machine_ and run the following to install the + dependencies of the demo script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + +1. Run the demo script: + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-someip \ + --node-file custom-nodes-someip.json \ + --decoder-file custom-decoders-someip.json \ + --network-interface-file network-interface-custom-someip.json \ + --campaign-file campaign-someip-heartbeat.json \ + --data-destination IOT_TOPIC + ``` + + The demo script: + + 1. Registers your AWS account with AWS IoT FleetWise, if not already registered. + 1. Creates IAM role and policy required for the service to write data to the IoT topic. + 1. Creates a signal catalog, containing `custom-nodes-someip.json` which includes the SOME/IP + sensor and actuator signals. + 1. Creates a model manifest that references the signal catalog with all of the signals. + 1. Activates the model manifest. + 1. Creates a decoder manifest linked to the model manifest using `custom-decoders-someip.json` + for decoding the SOME/IP signals from the network interface + `network-interface-custom-someip.json`. + 1. Updates the decoder manifest to set the status as `ACTIVE`. + 1. Creates a vehicle with a name equal to `fwdemo-someip`, the same as the name passed to + `provision.sh`. + 1. Creates a fleet. + 1. Associates the vehicle with the fleet. + 1. Creates a campaign from `campaign-someip-heartbeat.json` that contains a time-based collection + scheme to capture the SOME/IP signals every 10s. + 1. Approves the campaign. + 1. Waits until the campaign status is `HEALTHY`, which means the campaign has been deployed to + the fleet. + 1. Waits 30 seconds receiving data from the IoT topic. + 1. Saves the data to an HTML file. + +1. When the script completes, a path to an HTML file is given. _On your local machine_, use `scp` to + download it, then open it in your web browser: + + ```bash + scp -i ubuntu@: . + ``` + +1. To explore the collected data, you can click and drag to zoom in. + + ![](./images/collected_data_plot_someip.png) + +### Remote Command Execution + +The following steps will execute a SOME/IP method via the AWS IoT FleetWise 'remote commands' +feature. + +1. Run the following command _on the development machine_ to create an IAM role to generate the + command payload: + + ```bash + SERVICE_ROLE_ARN=`./manage-service-role.sh \ + --service-role IoTCreateCommandPayloadServiceRole \ + --service-principal iot.amazonaws.com \ + --actions iotfleetwise:GenerateCommandPayload \ + --resources '*'` + ``` + +1. Next create a remote command to execute the SOME/IP method `setInt32`. This SOME/IP method is + mapped via the decoder manifest to the 'actuator' node `Vehicle.actuator1` in the signal catalog. + + ```bash + aws iot create-command --command-id actuator1-command --namespace "AWS-IoTFleetWise" \ + --region us-east-1 \ + --role-arn ${SERVICE_ROLE_ARN} \ + --mandatory-parameters '[{ + "name": "$actuatorPath.Vehicle.actuator1", + "defaultValue": { "S": "0" } + }]' + ``` + +1. Run the following command to start the execution of the command defined above with the value to + set for the actuator. + + ```bash + JOBS_ENDPOINT_URL=`aws iot describe-endpoint --region us-east-1 --endpoint-type iot:Jobs | jq -j .endpointAddress` \ + && ACCOUNT_ID=`aws sts get-caller-identity | jq -r .Account` \ + && COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator1-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --parameters '{ + "$actuatorPath.Vehicle.actuator1": + { "S": "10" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} | jq -r .executionId` \ + && echo "Command execution id: ${COMMAND_EXECUTION_ID}" + ``` + +1. Run the following command to get the command execution status. + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + +1. You should see the following output indicating the command was successfully executed. Note that + the `reasonCode` (uint32) and `reasonDescription` (string) are extensible result information + fields. Refer to [ICommandDispatcher.h](../../src/ICommandDispatcher.h) for the reason codes + defined by FWE. The OEM range of reason codes begins at 65536. In this example implementation the + `reasonCode` is set to 65536 plus the CommonAPI `CallStatus` code, and `reasonDescription` is set + to the string representation of the CommonAPI `CallStatus` code. In this case the `reasonCode` is + 65536, meaning `65536 + 0` or `CommonAPI::CallStatus::SUCCESS`. + + ```json + { + "executionId": "", + "commandArn": "arn:aws:iot:us-east-1::command/actuator1-command", + "targetArn": "arn:aws:iot:us-east-1::thing/fwdemo-someip", + "status": "SUCCEEDED", + "statusReason": { + "reasonCode": "65536", + "reasonDescription": "SUCCESS" + }, + "parameters": { + "$actuatorPath.Vehicle.actuator1": { + "S": "10" + } + }, + "executionTimeoutSeconds": 10, + "createdAt": "", + "lastUpdatedAt": "", + "completedAt": "" + } + ``` + + In the FWE log you should see the following indicating that the command was successfully + executed: + + ``` + [TRACE] [ActuatorCommandManager.cpp:104] [processCommandRequest()]: [Processing Command Request with ID: ] + [TRACE] [ExampleSomeipInterfaceWrapper.h:85] [referenceMethodWrapper1()]: [set actuator value to 123 for command ID ] + [INFO ] [SomeipCommandDispatcher.cpp:128] [setActuatorValue()]: [Actuator Vehicle.actuator1 executed successfully for command ID ] + ``` + +### Long-running commands + +It is possible for commands to take an extended time to complete. In this case the vehicle can +report the command status as `IN_PROGRESS` to indicate that the command has been received and is +being run, before the final status of `SUCCEEDED` etc. is reported. + +In the example SOME/IP interfaces provided, `Vehicle.actuator20` is configured as such a +"long-running command". After running of this command is started the `someipigen` simulator will +notify FWE of the percentage completion on the broadcast message `notifyLRCStatus`. The intermediate +status is sent to the cloud and can also be obtained by calling the `aws iot get-command-execution` +API. + +1. Run the following to create the long-running command: + + ```bash + aws iot create-command --command-id actuator20-command --namespace "AWS-IoTFleetWise" \ + --region us-east-1 \ + --role-arn ${SERVICE_ROLE_ARN} \ + --mandatory-parameters '[{ + "name": "$actuatorPath.Vehicle.actuator20", + "defaultValue": { "S": "0" } + }]' + ``` + +1. Then start the command: + + ```bash + COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator20-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --parameters '{ + "$actuatorPath.Vehicle.actuator20": + { "S": "10" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} \ + --execution-timeout 20 | jq -r .executionId` \ + && echo "Command execution id: ${COMMAND_EXECUTION_ID}" + ``` + +1. Now repeatedly run this command to get the command status: + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + + The command takes 10 seconds to complete. In this time you will see that the status is + `IN_PROGRESS` with the `reasonDescription` being the percentage completion of the command as sent + by the `someipigen` tool. (Note: sending the percentage completion is just an example, the + `reasonDescription` string field is a free-form string to be defined by the customer.) After the + command completes the status changes to `SUCCEEDED`. + +### Concurrent commands + +It is possible for commands to be executed concurrently, even for the same actuator. Each execution +is uniquely identified by the execution ID. In the following example, 3 executions of the +`Vehicle.actuator20` command are started spaced by 1 second. Since each execution takes 10 seconds +to complete, all 3 will run in parallel. + +1. Run the following to begin 3 executions of the `Vehicle.actuator20` command: + + ```bash + COMMAND_EXECUTION_IDS=() \ + && for ((i=0; i<3; i++)); do + if ((i>0)); then sleep 1; fi + COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator20-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --parameters '{ + "$actuatorPath.Vehicle.actuator20": + { "S": "10" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} \ + --execution-timeout 20 | jq -r .executionId` + echo "Command execution ${i} id: ${COMMAND_EXECUTION_ID}" + COMMAND_EXECUTION_IDS+=("${COMMAND_EXECUTION_ID}") + done + ``` + +1. Now repeatedly run the following to get the status of the 3 commands as they run in parallel: + + ```bash + for ((i=0; i<3; i++)); do + echo "---------------------------" + echo "Command execution ${i} status:" + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --execution-id ${COMMAND_EXECUTION_IDS[i]} + done + ``` + +### Offline commands + +It is possible for a command execution to be started while FWE is offline, then FWE will begin +execution of the command when it comes online so long as the following prerequisites are met: + +- FWE has successfully connected via MQTT at least once, with persistent session enabled and the + MQTT session timeout has not elapsed. To enable persistent session, set + `.staticConfig.mqttConnection.sessionExpiryIntervalSeconds` in the config file or + `--session-expiry-interval-seconds` when running `configure-fwe.sh` to a non-zero value + sufficiently large. +- Persistency is enabled for FWE (so that the decoder manifest is available immediately when FWE + starts). +- The command timeout has not been exceeded. +- The SOME/IP service is available when FWE is started (in this case the `someipigen` simulator). + +The following steps demonstrate offline commands: + +1. Switch to the terminal running FWE, and stop it using `CTRL-C`. + +1. Switch to the terminal used to run the AWS CLI commands, and start execution of a command with a + 30 second timeout: + + ```bash + COMMAND_EXECUTION_ID=`aws iot-jobs-data start-command-execution \ + --region us-east-1 \ + --command-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:command/actuator1-command \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --parameters '{ + "$actuatorPath.Vehicle.actuator1": + { "S": "456" } + }' \ + --endpoint-url https://${JOBS_ENDPOINT_URL} \ + --execution-timeout 30 | jq -r .executionId` \ + && echo "Command execution id: ${COMMAND_EXECUTION_ID}" + ``` + +1. Get the current status of the command, which will remain as `CREATED` since FWE is not running: + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + +1. Switch to the FWE terminal, and restart it by running: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + +1. Switch to the AWS CLI terminal, and run the following to get the new status of the command, which + should be `SUCCEEDED`. Since FWE rejoined an existing MQTT session and the command was published + with QoS 1 (at least once), the MQTT broker sends the command to FWE as soon as it connects to + the cloud. FWE is able to execute the command, since it has not timed out, the decoder manifest + is available (as persistency for FWE is enabled), and the SOME/IP service is available. + + ```bash + aws iot get-command-execution \ + --region us-east-1 \ + --target-arn arn:aws:iot:us-east-1:${ACCOUNT_ID}:thing/fwdemo-someip \ + --execution-id ${COMMAND_EXECUTION_ID} + ``` + +1. Repeat the above, but this time wait longer than 30s before restarting FWE. In this case FWE will + still receive the command request from cloud, but since the timeout has expired it will not be + executed and the returned status will be `TIMED_OUT`. + +## Clean up + +1. Run the following _on the development machine_ to clean up resources created by the + `provision.sh` and `demo.sh` scripts. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && ./clean-up.sh \ + && ../provision.sh \ + --vehicle-name fwdemo-someip \ + --region us-east-1 \ + --only-clean-up \ + && ./manage-service-role.sh \ + --service-role IoTCreateCommandPayloadServiceRole \ + --clean-up + ``` + +1. Delete the CloudFormation stack for your development machine, which by default is called `fwdev`: + https://us-east-1.console.aws.amazon.com/cloudformation/home + +## Running over IP + +In the above example both FWE and the `someipigen` program were both running on the same development +machine. In this scenario the [vsomeip](https://github.com/COVESA/vsomeip) library uses a local UNIX +domain socket for service discovery and communication between the processes. + +If you would like to run the example over IP, with the `someipigen` program running on one machine +and FWE running on a different machine on the same local network, then it is necessary to configure +`vsomeip` using JSON configuration files to setup the IP addresses, ports and protocols for the +service to use. + +The example below details how to configure `vsomeip` for UDP over IP communication. If you are +interested in using TCP over IP communication refer to the `vsomeip` documentation. + +1. Create a JSON configuration file called `tools/someipigen/vsomeip-someipigen.json` for the + `someipigen` program, replacing `` with the IPv4 address of the machine running the + program: + + ```json + { + "unicast": "", + "netmask": "255.255.0.0", + "logging": { + "level": "trace", + "console": "true", + "dlt": "false" + }, + "applications": [ + { + "name": "someipigen", + "id": "0x1414" + } + ], + "services": [ + { + "service": "0x1234", + "instance": "0x5678", + "unreliable": "30510" + } + ], + "service-discovery": { + "enable": "true", + "multicast": "224.224.224.245", + "port": "30490", + "protocol": "udp", + "initial_delay_min": "10", + "initial_delay_max": "100", + "repetitions_base_delay": "200", + "repetitions_max": "3", + "ttl": "3", + "cyclic_offer_delay": "2000", + "request_response_delay": "1500" + } + } + ``` + +1. Run the `someipigen` program with the configuration file as follows: + + ```bash + cd tools/someipigen \ + && VSOMEIP_CONFIGURATION=vsomeip-someipigen.json python3 someipsim.py + ``` + +1. Create a JSON configuration file called `vsomeip-fwe.json` for FWE, replacing `` with + the IPv4 address of the machine running FWE: + + ```json + { + "unicast": "", + "netmask": "255.255.0.0", + "logging": { + "level": "trace", + "console": "true", + "dlt": "false" + }, + "applications": [ + { + "name": "someipCommandInterface", + "id": "0x1314" + }, + { + "name": "someipCollectionInterface", + "id": "0x1315" + } + ], + "service-discovery": { + "enable": "true", + "multicast": "224.224.224.245", + "port": "30490", + "protocol": "udp", + "initial_delay_min": "10", + "initial_delay_max": "100", + "repetitions_base_delay": "200", + "repetitions_max": "3", + "ttl": "3", + "cyclic_offer_delay": "2000", + "request_response_delay": "1500" + } + } + ``` + +1. Run FWE with the configuration file as follows: + + ```bash + VSOMEIP_CONFIGURATION=vsomeip-fwe.json ./aws-iot-fleetwise-edge config-0.json + ``` + + If successfully configured, you should see the following in the FWE log: + + ``` + [debug] Joining to multicast group 224.224.224.245 from + [info] SOME/IP routing ready. + ``` + +1. You can now [run the cloud demo script](#run-the-aws-iot-fleetwise-demo-script). + +### Troubleshooting + +Common issues encountered when trying to establish a SOME/IP connection over UDP include: + +- **Trying to use a local loopback address.** It is not possible to use the local loopback IP + address `127.0.0.1` to run the demo over UDP on one machine, as the local loopback interface does + not support UDP multicast, which is required by SOME/IP service discovery. + +- **A firewall blocking open UDP ports.** To open the two UDP ports used in the above example, run + the following on the machine running `someipigen`: + + ```bash + sudo iptables -A INPUT -p udp -m udp --dport 30490 -j ACCEPT + sudo iptables -A INPUT -p udp -m udp --dport 30510 -j ACCEPT + ``` + +- **A bug in the `vsomeip` library that causes service discovery to fail** in versions >=3.3.0 + <3.5.0. This bug was fixed with this GitHub PR: https://github.com/COVESA/vsomeip/pull/591. diff --git a/docs/dev-guide/edge-agent-dev-guide.md b/docs/dev-guide/edge-agent-dev-guide.md index 6a318be7..a8ecb592 100644 --- a/docs/dev-guide/edge-agent-dev-guide.md +++ b/docs/dev-guide/edge-agent-dev-guide.md @@ -1,6 +1,7 @@ # Edge Agent Developer Guide -**Note:** AWS IoT FleetWise is currently available in `us-east-1` and `eu-central-1`. +**Note:** AWS IoT FleetWise is currently available in +[these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. **Topics** @@ -95,9 +96,13 @@ If you are interested in exploring AWS IoT FleetWise with ROS2 support, see the ## Prerequisites for quick start demo - Access to an AWS Account with administrator privileges. -- Logged in to the AWS Console in your desired region using the account with administrator +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator privileges. - - **Note:** AWS IoT FleetWise is currently available in `us-east-1` and `eu-central-1`. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Windows, Linux or MacOS machine. ## Deploy Edge Agent @@ -109,7 +114,7 @@ instance. [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdemo.yml&stackName=fwdemo). 1. (Optional) You can increase the number of simulated vehicles by updating the `FleetSize` parameter. You can also specify the region IoT Things are created in by updating the - `IoTCoreRegion` parameter. + `IoTCoreRegion` parameter, by default it is `us-east-1`. 1. Select the checkbox next to _'I acknowledge that AWS CloudFormation might create IAM resources with custom names.'_ 1. Choose **Create stack**. @@ -128,7 +133,8 @@ vehicle model, register the virtual vehicle created in the previous section and collect data from it. 1. Open the AWS CloudShell: [Launch CloudShell](https://console.aws.amazon.com/cloudshell/home) -1. Copy and paste the following commands to clone the latest FWE software from GitHub and install + +1. Copy and paste the following commands to clone the latest FWE source code from GitHub and install the dependencies of the demo script. ```bash @@ -150,6 +156,7 @@ collect data from it. ```bash ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo \ --node-file can-nodes.json \ --decoder-file can-decoders.json \ @@ -160,38 +167,20 @@ collect data from it. - (Optional) To enable S3 upload, append the option `--data-destination S3`. By default the upload format will be JSON. You can change this to Parquet format for S3 by passing `--s3-format PARQUET`. - - ```bash - ./demo.sh \ - --vehicle-name fwdemo \ - --node-file can-nodes.json \ - --decoder-file can-decoders.json \ - --network-interface-file network-interface-can.json \ - --campaign-file campaign-brake-event.json \ - --data-destination S3 - ``` - + - (Optional) To enable IoT topic as destination, add the flag `--data-destination IOT_TOPIC`. To + define the custom IoT topic use the flag `--iot-topic `. Note: The IoT topic data + destination is a "gated" feature of AWS IoT FleetWise for which you will need to request + access. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) + for more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). - (Optional) If you selected a `FleetSize` of greater than one above, append the option `--fleet-size `, where `` is the number selected. - - (Optional) If you changed `IoTCoreRegion` above, append the option `--region `, where + - (Optional) If you changed `IoTCoreRegion` above, change the option `--region `, where `` is the selected region. - (Optional) If you changed `Stack name` when creating the stack above, pass the new stack name to the `--vehicle-name` option. - For example, if you chose to create two AWS IoT things in Europe (Frankfurt) with a stack named - `myfwdemo`, you must pass those values when calling `demo.sh`: - - ```bash - ./demo.sh \ - --vehicle-name myfwdemo \ - --node-file can-nodes.json \ - --decoder-file can-decoders.json \ - --network-interface-file network-interface-can.json \ - --campaign-file campaign-brake-event.json \ - --fleet-size 2 \ - --region eu-central-1 - ``` - The demo script: 1. Registers your AWS account with AWS IoT FleetWise, if not already registered. @@ -216,12 +205,12 @@ collect data from it. 1. Waits 30 seconds and then downloads the collected data from Amazon Timestream. 1. Saves the data to an HTML file. - If S3 upload is enabled, the demo script will additionally: + If S3 upload is enabled, the demo script will instead: 1. Create an S3 bucket with a bucket policy that allows AWS IoT FleetWise to write data to the bucket. - 1. Creates an additional campaign from `campaign-brake-event.json` to upload the data to S3 in - JSON format, or Parquet format if the `--s3-format PARQUET` option is passed. + 1. Creates a campaign from `campaign-brake-event.json` to upload the data to S3 in JSON format, + or Parquet format if the `--s3-format PARQUET` option is passed. 1. Wait 20 minutes for the data to propagate to S3 and then download it. 1. Save the data to an HTML file. @@ -306,9 +295,12 @@ This section describes how to get started on a development machine. ### Prerequisites for development machine - Access to an AWS Account with administrator privileges. -- Logged in to the AWS Console in your desired region using the account with administrator +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator privileges. - - **Note:** AWS IoT FleetWise is currently available in`us-east-1` and `eu-central-1`. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. - A local Linux or MacOS machine. ### Launch your development machine @@ -381,12 +373,10 @@ launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, 1. Run the following _on the development machine_ to provision an AWS IoT Thing with credentials and install your Edge Agent as a service. - **Note** To create AWS IoT things in Europe (Frankfurt), configure `--region` to `eu-central-1` - in the call to `provision.sh` - ```bash sudo mkdir -p /etc/aws-iot-fleetwise \ && sudo ./tools/provision.sh \ + --region us-east-1 \ --vehicle-name fwdemo-ec2 \ --certificate-pem-outfile /etc/aws-iot-fleetwise/certificate.pem \ --private-key-outfile /etc/aws-iot-fleetwise/private-key.key \ @@ -450,6 +440,7 @@ collect data from it. ```bash ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-ec2 \ --node-file can-nodes.json \ --decoder-file can-decoders.json \ @@ -460,31 +451,15 @@ collect data from it. - (Optional) To enable S3 upload, append the option `--data-destination S3`. By default the upload format will be JSON. You can change this to Parquet format by passing `--s3-format PARQUET`. - - ```bash - ./demo.sh \ - --vehicle-name fwdemo-ec2 \ - --node-file can-nodes.json \ - --decoder-file can-decoders.json \ - --network-interface-file network-interface-can.json \ - --campaign-file campaign-brake-event.json \ - --data-destination S3 - ``` - - - (Optional) If you changed the `--region` option to `provision.sh` above, append the option - `--region `, where `` is the selected region. For example, if you chose to - create the AWS IoT thing in Europe (Frankfurt), you must configure `--region` to `eu-central-1` - in the demo.sh file. - - ```bash - ./demo.sh \ - --vehicle-name fwdemo-ec2 \ - --node-file can-nodes.json \ - --decoder-file can-decoders.json \ - --network-interface-file network-interface-can.json \ - --campaign-file campaign-brake-event.json \ - --region eu-central-1 - ``` + - (Optional) To enable IoT topic as destination, add the flag `--data-destination IOT_TOPIC` To + define the custom IoT topic use the flag `--iot-topic `. Note: The IoT topic data + destination is a "gated" feature of AWS IoT FleetWise for which you will need to request + access. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) + for more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). + - (Optional) If you changed the `--region` option to `provision.sh` above, change the option + `--region `, where `` is the selected region. The demo script: @@ -542,6 +517,7 @@ collect data from it. ```bash ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-ec2 \ --node-file obd-nodes.json \ --decoder-file obd-decoders.json \ @@ -557,6 +533,7 @@ collect data from it. python3 dbc-to-nodes.py can-nodes.json \ && python3 dbc-to-decoders.py can-decoders.json \ && ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-ec2 \ --node-file can-nodes.json \ --decoder-file can-decoders.json \ @@ -610,7 +587,7 @@ for AWS IoT FleetWise ("FWE"). - [Architecture Overview](#architecture-overview) - [User Flow](#user-flow) - [Software Layers](#software-layers) - - [Overview of the software libraries](#overview-of-the-software-libraries) + - [Overview of the software libraries](#overview-of-the-software-modules) - [Programming and execution model](#programming-and-execution-model) - [Data models](#data-models) - [Device to cloud communication](#device-to-cloud-communication) @@ -995,6 +972,16 @@ FWE sends the following artifacts to the Cloud services: CDR format and packed them in Amazon ION file format and directly upload to S3. Refer to [vision_system_data.isl](../../interfaces/protobuf/schemas/edgeToCloud/vision_system_data.isl). +- **Command Response:** After a command is executed, FWE sends this message to inform the cloud + about the execution status of a requested command. Refer to + [command_response.proto](../../interfaces/protobuf/schemas/edgeToCloud/command_response.proto) + +- **Last Known State:** This message is sent conditionally to the cloud data plane services once a + rule defined by a state template is met. The message will contain the last value of each signal + that was collected based on state template rules. By default, FWE sends this payload compressed + with Snappy. Refer to + [last_known_state_data.proto](../../interfaces/protobuf/schemas/edgeToCloud/last_known_state_data.proto) + ### Cloud to Device communication The Cloud Control plane services publish to FWE dedicated MQTT Topic the following artifacts: @@ -1009,6 +996,20 @@ The Cloud Control plane services publish to FWE dedicated MQTT Topic the followi apply the rules defined in the collection schemes to generate data snapshots. Refer to [collection_schemes.proto](../../interfaces/protobuf/schemas/cloudToEdge/collection_schemes.proto). +- **Command Request:** This artifact describes a command that FWE should execute to change an + actuator's state or a state template. Refer to + [command_request.proto](../../interfaces/protobuf/schemas/cloudToEdge/command_request.proto). + + Note: The Last Known State commands change the activation status of a state template. This status + will be reset (meaning that state templates will be deactivated) when FWE is restarted unless + persistency is enabled. In such situation, if persistency is disabled, a new command needs to be + sent to activate the state template again. + +- **State Templates:** This artifact describes the signals to be collected for last known state + feature. The list of signals is derived from state templates created in the Cloud, although it is + presented to FWE as a single list of signals. Refer to + [state_templates.proto](../../interfaces/protobuf/schemas/cloudToEdge/state_templates.proto). + ## Data Persistency FWE requires a temporary disk location in order to persist and reload the documents it exchanges @@ -1089,12 +1090,29 @@ described below in the configuration section. Each log entry includes the follow | | introspectionLibraryCompare | Error handling when local ROS2 introspection library mismatches with Cloud decoder manifest: "ErrorAndFail", "Warn","Ignore". | string | | | interfaceId | A unique network interface ID used by AWS IoT FleetWise service | string | | | type | Specifies if the interface carries ROS2 signals over this channel. | string | +| someipToCanBridgeInterface | someipServiceId | The Service ID that provides the CAN over SOME/IP service. | string | +| | someipInstanceId | The Instance ID that provides the CAN over SOME/IP.service. | string | +| | someipEventId | The Event ID of the CAN over SOME/IP data.service. | string | +| | someipEventGroupId | The Event Group ID that FWE should subscribe to for the CAN over SOME/IP data.service. | string | +| | someipApplicationName | Name of the SOME/IP application | string | +| someipCollectionInterface | someipApplicationName | Name of the SOME/IP application | string | +| | cyclicUpdatePeriodMs | Cyclic update period in milliseconds that FWE will periodically push the last available signal values. Set to zero to only collect values on change. | integer | +| someipCommandInterface | someipCommandInterface | Name of the SOME/IP application | string | +| exampleUDSInterface | configs | | array | +| | - targetAddress | ECU address | string | +| | - name | Name of of the target | string | +| | - can | CAN Interfaces | object | +| | -- interfaceName | Can interface name | string | +| | -- functionalAddress | Functional address for UDS request | string | +| | -- physicalRequestID | Physical request ID for UDS request | string | +| | -- physicalResponseID | Physical response ID for UDS response | string | | bufferSizes | dtcBufferSize | Deprecated: decodedSignalsBufferSize is used for all signals. This option will be ignored. | integer | | | decodedSignalsBufferSize | Max size of the buffer shared between data collection module (Collection Engine) and Vehicle Data Consumer for OBD and CAN signals. This buffer receives the raw packets from the Vehicle Data e.g. CAN bus and stores the decoded/filtered data according to the signal decoding information provided in decoder manifest. This is a multiple producer single consumer buffer. | integer | | | rawCANFrameBufferSize | Deprecated: decodedSignalsBufferSize is used for all signals. This option will be ignored. | integer | | threadIdleTimes | inspectionThreadIdleTimeMs | Sleep time for inspection engine thread if no new data is available (in milliseconds) | integer | | | socketCANThreadIdleTimeMs | Sleep time for CAN interface if no new data is available (in milliseconds) | integer | | | canDecoderThreadIdleTimeMs | Sleep time for CAN decoder thread if no new data is available (in milliseconds) | integer | +| | lastKnownStateThreadIdleTimeMs | Sleep time for last known state inspection engine thread (in milliseconds) | integer | | persistency | persistencyPath | Local storage path to persist Collection Scheme, decoder manifest and data snapshot | string | | | persistencyPartitionMaxSize | Maximum size allocated for persistency (Bytes) | integer | | | persistencyUploadRetryIntervalMs | Interval to wait before retrying to upload persisted signal data (in milliseconds). After successfully uploading, the persisted signal data will be cleared. Only signal data that could not be uploaded will be persisted. (in milliseconds) | integer | @@ -1103,15 +1121,17 @@ described below in the configuration section. Each log entry includes the follow | | logColor | Whether logs should be colored: `Auto`, `Yes`, `No`. Default to `Auto`, meaning FWE will try to detect whether colored output is supported (for example when connected to a tty) | string | | | maximumAwsSdkHeapMemoryBytes | The maximum size of AWS SDK heap memory | integer | | | metricsCyclicPrintIntervalMs | Sets the interval in milliseconds how often the application metrics should be printed to stdout. Default 0 means never | string | +| | minFetchTriggerIntervalMs | Minimum time after fetch condition can be retriggered memory | integer | | publishToCloudParameters | maxPublishMessageCount | Maximum messages that can be published to the cloud in one payload | integer | +| | maxPublishLastKnownStateMessageCount | Maximum Last Known State messages that can be published to the cloud as one payload | integer | | | collectionSchemeManagementCheckinIntervalMs | Time interval between collection schemes checkins(in milliseconds) | integer | | mqttConnection | endpointUrl | AWS account's IoT device endpoint | string | | | connectionType | The connection module type. It can be `iotCore`, or `iotGreengrassV2` when `FWE_FEATURE_GREENGRASSV2` is enabled. | string | | | clientId | The ID that uniquely identifies this device in the AWS Region | string | -| | collectionSchemeListTopic | Topic for subscribing to Collection Scheme | string | -| | decoderManifestTopic | Topic for subscribing to Decoder Manifest | string | -| | canDataTopic | Topic for sending collected data to cloud | string | -| | checkinTopic | Topic for sending checkins to the cloud | string | +| | iotFleetWiseTopicPrefix | The prefix for AWS IoT FleetWise topics. All topics from other services such as commands, jobs, device shadow will be unaffected by this option. If omitted, it defaults to `$aws/iotfleetwise/` | string | +| | commandsTopicPrefix | The prefix for AWS IoT Commands topics. If omitted, it defaults to `$aws/commands/` | string | +| | deviceShadowTopicPrefix | The prefix for AWS IoT Device Shadow topics. If omitted, it defaults to `$aws/things/` | string | +| | jobsTopicPrefix | The prefix for AWS IoT Jobs topics. If omitted, it defaults to `$aws/things/` | string | | | certificateFilename | The path to the device's certificate file (either `certificateFilename` or `certificate` must be provided) | string | | | privateKeyFilename | The path to the device's private key file (either `privateKeyFilename` or `privateKey` must be provided) | string | | | rootCAFilename | The path to the root CA certificate file (optional, either `rootCAFilename` or `rootCA` can be provided) | string | @@ -1175,8 +1195,8 @@ step/safety cores. You can use the cmake build option, `FWE_SECURITY_COMPILE_FLAGS`, to enable security-related compile options when building the binary. Consult the compiler manual for the effect of each option in `./cmake/compiler_gcc.cmake`. This flag is already enabled in the default -[native compilation script](./tools/build-fwe-native.sh) and -[cross compilation script for ARM64](./tools/build-fwe-cross-arm64.sh) +[native compilation script](../../tools/build-fwe-native.sh) and +[cross compilation script for ARM64](../../tools/build-fwe-cross-arm64.sh) Customers are encouraged to store key materials on hardware modules, such as hardware security module (HSM), Trusted Platform Modules (TPM), or other cryptographic elements. A HSM is a removable diff --git a/docs/dev-guide/edge-agent-uds-dtc-dev-guide.md b/docs/dev-guide/edge-agent-uds-dtc-dev-guide.md new file mode 100644 index 00000000..87dac3cb --- /dev/null +++ b/docs/dev-guide/edge-agent-uds-dtc-dev-guide.md @@ -0,0 +1,572 @@ +# Diagnostic trouble code (DTC) Developer Guide + + +> [!NOTE] +> This guide makes use of "gated" features of AWS IoT FleetWise for which you will need to request +> access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for +> more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +## Table of Contents + +- [Overview](#overview) + - [Adding a new diagnostic interface](#adding-a-new-diagnostic-interface) + - [`DTC_QUERY()` function implementation](#dtc_query-function-implementation) + - [Parameters for `DTC_QUERY()`](#parameters-for-dtc_query) + - [Examples of DTC queries](#examples-of-dtc-queries) + - [Combining multiple status masks](#combining-multiple-status-masks) + - [Serializing and sending JSON messages](#serializing-and-sending-json-messages) +- [Example UDS Interface](#example-uds-interface) + - [Static configuration](#static-configuration) +- [Adding your own UDS interface](#adding-your-own-uds-interface) +- [Demo](#demo) + - [Prerequisites for the demo](#prerequisites-for-the-demo) + - [Launch your development machine](#launch-your-development-machine) + - [Obtain the FWE code](#obtain-the-fwe-code) + - [Download or build the FWE binary](#download-or-build-the-fwe-binary) + - [Install the CAN simulator](#install-the-can-simulator) + - [Provision and run FWE](#provision-and-run-fwe) + - [Run the AWS IoT FleetWise demo script](#run-the-aws-iot-fleetwise-demo-script) + - [Time based fetch demo](#time-based-fetch-demo) + - [Condition based fetch demo](#condition-based-fetch-demo) + - [Clean up](#clean-up) + +## Overview + +The [`IRemoteDiagnostics`](../../src/IRemoteDiagnostics.h) interface facilitates the retrieval of +active DTCs, snapshot records, and extended data from ECUs. AWS IoT FleetWise utilizes this +diagnostics interface to collect DTCs and related information. + +![](./images/uds-dtc-architecture-uml.png) + +To enable UDS DTC information collection `FWE_FEATURE_UDS_DTC` option needs to be enabled in CMake. +This option enables `RemoteDiagnosticDataSource` module to process DTC queries from the available +UDS interfaces. + +### Adding a new diagnostic interface + +Fetch manager leverages the `DTC_QUERY()` function from `RemoteDiagnosticDataSource.h` to gather +DTCs based on specific conditions or timeframes. `RemoteDiagnosticDataSource` will then call a UDS +interface to get the DTC data from the canbus. In the reference implementation, it is +`ExampleUDSInterface.cpp`. + +### `DTC_QUERY()` function implementation + +The `DTC_QUERY()` function is defined as follows: + +```cpp +using CustomFunction = std::function params)>; +``` + +#### Parameters for `DTC_QUERY()` + +- **`signalID`**: Identifier for the signal. +- **`fetchRequestID`**: Identifier for the fetch request. +- **`params`**: Request parameters, including: + +1. `targetAddress` (the address of the ECU to be queried). + - If the target address is -1, FWE will query DTC signal collection for all the ECUs available in + the configuration file. +1. `udsSubFunction` UDS DTC Sub Function, refer to ISO 14229-1 for more information. + - For DTC and its snapshot collection following sub functions can be used: + 1. `UDS_REPORT_DTC_BY_STATUS_MASK` = 0x02, Retrieving the list of DTCs that match a client + defined status mask. + 1. `UDS_REPORT_DTC_SNAPSHOT_IDENTIFICATION` = 0x03, Retrieving DTCSnapshot record + identification. + 1. `UDS_REPORT_DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER` = 0x04, Retrieving DTCSnapshot record data + for a client defined DTC mask. + 1. `UDS_REPORT_DTC_EXT_DATA_RECORD_BY_DTC_NUMBER` = 0x06, Retrieving DTCExtendedData record + data for a client defined DTC mask and a client defined DTCExtendedData record number. +1. `udsStatusMask` UDS DTC Status Mask, refer to ISO 14229-1 for more information. + - The `udsStatusMask` value can be any of the following: + 1. `UDS_STATUS_MASK_TEST_FAILED` = 0x01, indicate the result of the most recently performed + test. + 1. `UDS_STATUS_MASK_TEST_FAILED_THIS_OP_CYCLE` = 0x02, indicate whether or not a diagnostic + test has reported a testFailed result at any time during the current operation cycle. + 1. `UDS_STATUS_MASK_PENDING_DTC` = 0x04, indicate whether or not a diagnostic test has reported + a testFailed result at any time during the current or last completed operation cycle. + 1. `UDS_STATUS_MASK_CONFIRMED_DTC` = 0x08, indicate whether a malfunction was detected enough + times to warrant that the DTC is desired to be stored in long-term memory. + 1. `UDS_STATUS_MASK_TEST_NOT_COMPLETED_SINCE_LAST_CLEAR` = 0x10 indicate whether a DTC test has + ever run and completed since the last time a call was made to ClearDiagnosticInformation. + 1. `UDS_STATUS_MASK_TEST_FAILED_SINCE_LAST_CLEAR` = 0x20, indicate whether a DTC test has + completed with a failed result since the last time a call was made to + ClearDiagnosticInformation. + 1. `UDS_STATUS_MASK_TEST_NOT_COMPLETED_THIS_OP_CYCLE` = 0x40, indicate whether a DTC test has + ever run and completed during the current operation cycle. + 1. `UDS_STATUS_MASK_WARNING_INDICATOR_REQUESTED` = 0x80, report the status of any warning + indicators associated with a particular DTC. +1. (optional) `dtcCode` specific dtc code to query a snapshot, refer to ISO 14229-1 for more + information. +1. (optional) `recordNumber` record number to query a snapshot, refer to ISO 14229-1 for more + information. + +**Note:** For `statusMasks` or `udsSubFunction` parameters: + +- A value of `-1` for `statusMasks` translates to `0xFF`, representing "all." +- A value of `-1` for `udsSubFunction` triggers appropriate UDS DTC queries based on other + parameters. + +#### Examples of DTC queries + +1. Query all DTCs with status mask for all ECUs: + + ```cpp + DTC_QUERY(-1, 2, -1); + ``` + +1. Query a specific `confirmed` DTC ("0xAAA123") with status mask for all ECUs: + + ```cpp + DTC_QUERY(-1, 2, 8, "0xAAA123"); + ``` + +1. Check if the `confirmed` DTC ("0xAAA123") is present for `ECU-1`: + + ```cpp + DTC_QUERY(1, 2, 8, "0xAAA123"); + ``` + +1. Query all DTCs with any status mask and their snapshot records for all ECUs: + + ```cpp + DTC_QUERY(-1, 4, -1); + ``` + +1. Query all DTCs and their snapshot records for `ECU-1`: + + ```cpp + DTC_QUERY(1, 4, -1); + ``` + +1. Query all DTCs with any status mask and their snapshot records for `ECU-1`, with `recordNumber` + `0`: + + ```cpp + DTC_QUERY(1, 4, -1, 0); + ``` + +1. Query if the DTC ("0xAAA123") is `pending` with `recordNumber` `1` for `ECU-1`: + + ```cpp + DTC_QUERY(1, 4, -1, "0xAAA123", 1); + ``` + +1. Query all DTCs and their respective snapshot records for all ECUs: + + ```cpp + DTC_QUERY(-1, 4, -1); + ``` + +1. Query all DTCs and DTC extended data for all ECUs: + + ```cpp + DTC_QUERY(-1, 6, -1); + ``` + +1. Query if the DTC ("0xAAA123") is `pending` and collect extended data with `recordNumber` `1` for + `ECU-1`: + + ```cpp + DTC_QUERY(1, 6, -1, "0xAAA123", 1); + ``` + +#### Combining multiple status masks + +1. Query for all `pending` (4) or `confirmed` (8) DTCs with status masks and respective snapshots + for all ECUs: + + ```cpp + DTC_QUERY(-1, 2, 12); + ``` + +1. Check if the DTC ("0xAAA123") is a `warningIndicator` (8) DTC or `failedTestSinceLastClear` (1) + for `ECU-1` with `recordNumber` `0`: + + ```cpp + DTC_QUERY(1, 4, 144, "0xAAA123", 0); + ``` + +### Serializing and sending JSON messages + +After completing the `DTC_QUERY()` function, the diagnostic module should serialize the results into +a JSON message. This is done in the `convertDataToJson` function defined in +`RemoteDiagnosticDataSource.cpp`. + +Once serialized, the JSON message is sent to the `rawDataBufferManager` for further processing +with`pushSnapshotJsonToRawDataBufferManager` function from `RemoteDiagnosticDataSource.cpp`. The +`rawDataBufferManager` will then forward the data to the cloud through other modules. + +## Example UDS Interface + +For reference, an example implementation is available [here](../../src/ExampleUDSInterface.cpp). +When building with this example, ensure the `FWE_FEATURE_UDS_DTC_EXAMPLE` option is enabled in +CMake. + +**Note:** If no interface is specified, the default interface will be employed to gather DTCs. Be +aware that the default interface (`FWE_FEATURE_UDS_DTC_EXAMPLE`) is a reference implementation +limited to retrieving available DTCs and snapshot data based on the DTC number and record number. +Other DTC-related functions are not supported. + +### Static configuration + +Include the following configuration in `networkInterfaces` of the FWE static config file: + +```json +{ + "exampleUDSInterface": { + "configs": [ + { + "targetAddress": "", + "name": "", + "can": { + "interfaceName": "", + "functionalAddress": "", + "physicalRequestID": "", + "physicalResponseID": "" + } + } + ] + }, + "interfaceId": "UDS_DTC", + "type": "exampleUDSInterface" +} +``` + +Condition based fetch trigger frequency is limited by `minFetchTriggerIntervalMs` field in static +config file. This frequency limits repeated fetch requests while fetch condition is evaluated to +TRUE. The limitation is set to 1s by default if not specified in the config. + +## Adding your own UDS interface + +If you want to add your custom interface: + +1. Add a new source file and implement request functions declared in `IRemoteDiagnostics.h` file. +2. You can use `FWE_FEATURE_UDS_DTC` option (make sure `FWE_FEATURE_UDS_DTC_EXAMPLE` is disabled) to + compile `RemoteDiagnosticDataSource` module without the provided example interface. +3. The newly created custom interfaces can be bootstrapped as follows in the IoTFleetWiseEngine.cpp + similar to `ExampleUDSInterface`: + 1. Initialise and start the custom interface: + - ```cpp + auto customDiagnosticInterface = std::make_shared(); + customDiagnosticInterface->init(); + customDiagnosticInterface->start(); + ``` + 2. Register the custom interface handle (`customDiagnosticInterface`) with + `RemoteDiagnosticDataSource`: + - ```cpp + mDiagnosticDataSource = std::make_shared( mDiagnosticNamedSignalDataSource, + mRawBufferManager, + customDiagnosticInterface); + ``` + +## Demo + +### Prerequisites for the demo + +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Windows, Linux or MacOS machine. + +### Launch your development machine + +An Ubuntu 20.04 development machine with 200GB free disk space will be required. A local Intel +x86_64 (amd64) machine can be used, however it is recommended to use the following instructions to +launch an AWS EC2 Graviton (arm64) instance. Pricing for EC2 can be found, +[here](https://aws.amazon.com/ec2/pricing/on-demand/). + +1. Launch an EC2 Graviton instance with administrator permissions: + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdev.yml&stackName=fwdev). +1. Enter the **Name** of an existing SSH key pair in your account from + [here](https://us-east-1.console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:). + 1. Do not include the file suffix `.pem`. + 1. If you do not have an SSH key pair, you will need to create one and download the corresponding + `.pem` file. Be sure to update the file permissions: `chmod 400 ` +1. **Select the checkbox** next to _'I acknowledge that AWS CloudFormation might create IAM + resources with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is **CREATE_COMPLETE**; this can take up to five minutes. +1. Select the **Outputs** tab, copy the EC2 IP address, and connect via SSH from your local machine + to the development machine. + + ```bash + ssh -i ubuntu@ + ``` + +### Obtain the FWE code + +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +### Download or build the FWE binary + +**To quickly run the demo**, download the pre-built FWE binary: + +- If your development machine is ARM64 (the default if you launched an EC2 instance using the + CloudFormation template above): + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-arm64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge + ``` + +- If your development machine is x86_64: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build \ + && curl -L -o build/aws-iot-fleetwise-edge.tar.gz \ + https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-amd64.tar.gz \ + && tar -zxf build/aws-iot-fleetwise-edge.tar.gz -C build aws-iot-fleetwise-edge + ``` + +**Alternatively if you would like to build the FWE binary from source,** follow these instructions. +If you already downloaded the binary above, skip to the next section. + +1. Install the dependencies for FWE: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh \ + && sudo ldconfig + ``` + +1. Compile FWE with UDS DTC support, this will take around 10 minutes to complete: + + ```bash + ./tools/build-fwe-native.sh --with-uds-dtc-example + ``` + +### Install the CAN simulator + +1. Run the following command to install the CAN simulator: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-socketcan.sh \ + && sudo -H ./tools/install-cansim.sh + ``` + +### Provision and run FWE + +1. Open a new terminal _on the development machine_, and run the following to provision credentials + for the vehicle and configure the network interface for UDS DTC collection: + + ```bash + cd ~/aws-iot-fleetwise-edge \ + && mkdir -p build_config \ + && ./tools/provision.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-dtc \ + --certificate-pem-outfile build_config/certificate.pem \ + --private-key-outfile build_config/private-key.key \ + --endpoint-url-outfile build_config/endpoint.txt \ + --vehicle-name-outfile build_config/vehicle-name.txt \ + && ./tools/configure-fwe.sh \ + --input-config-file configuration/static-config.json \ + --output-config-file build_config/config-0.json \ + --log-color Yes \ + --log-level Trace \ + --vehicle-name `cat build_config/vehicle-name.txt` \ + --endpoint-url `cat build_config/endpoint.txt` \ + --can-bus0 vcan0 \ + --certificate-file `realpath build_config/certificate.pem` \ + --private-key-file `realpath build_config/private-key.key` \ + --persistency-path `realpath build_config` \ + --uds-dtc-example-interface vcan0 + ``` + +1. Run FWE: + + ```bash + ./build/aws-iot-fleetwise-edge build_config/config-0.json + ``` + +### Run the AWS IoT FleetWise demo script + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +vehicle model, register the virtual vehicle created in the previous section and run a campaign to +collect data from it. + +1. Open a new terminal _on the development machine_ and run the following to install the + dependencies of the demo script: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + +1. Run the following command to generate 'node' and 'decoder' JSON files from the input DBC file for + CAN signals. + + ```bash + python3 dbc-to-nodes.py hscan.dbc can-nodes.json \ + && python3 dbc-to-decoders.py hscan.dbc can-decoders.json + ``` + +DTC fetching can be triggered by time based fetch as well as condition based fetch. + +- Time based fetch + - Time Based DTC Fetch triggers the DTC fetching and collection at specific intervals of time, + corresponding to the "actions" in the campaign. +- Condition based fetch + - Condition based fetch triggers DTC fetching when the required conditions are met. + - There can be different conditions for DTC fetch and DTC collection, which are independent of + each other. When the DTC fetch conditions are met, the FWE will trigger the fetching of the + DTCs. Conversely, when the DTC collection conditions are met, the data fetched by the FWE will + be uploaded to the cloud. + +#### Time based fetch demo + +The campaign file +[`campaign-uds-dtc-condition-based-fetch.json`](../../tools/cloud/campaign-uds-dtc-condition-based-fetch.json) +is setup to trigger fetch of DTC codes and snapshot data based on the CAN signal. This campaign will +collect both signals. + +1. Run the demo script: + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-dtc \ + --node-file custom-nodes-uds-dtc.json \ + --node-file can-nodes.json \ + --decoder-file custom-decoders-uds-dtc.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-custom-uds-dtc.json \ + --network-interface-file network-interface-can.json \ + --campaign-file campaign-uds-dtc-condition-based-fetch.json \ + --data-destination IOT_TOPIC + ``` + + The demo script: + + 1. Registers your AWS account with AWS IoT FleetWise, if not already registered. + 1. Creates IAM role and policy required for the service to write data to the IoT topic. + 1. Creates a signal catalog, containing `custom-nodes-uds-dtc.json` which includes the DTC sensor + signal and `can-nodes.json` containing CAN signals. + 1. Creates a model manifest that references the signal catalog with all of the signals. + 1. Activates the model manifest. + 1. Creates a decoder manifest linked to the model manifest using `custom-decoders-uds-dtc.json` + for decoding the DTC signal from the network interface `network-interface-custom-uds-dtc.json` + and `can-decoders.json` for CAN signals from the network interface + `network-interface-can.json`. + 1. Updates the decoder manifest to set the status as `ACTIVE`. + 1. Creates a vehicle with a name equal to `fwdemo-dtc`, the same as the name passed to + `provision.sh`. + 1. Creates a fleet. + 1. Associates the vehicle with the fleet. + 1. Creates a campaign from + [`campaign-uds-dtc-condition-based-fetch.json`](../../tools/cloud/campaign-uds-dtc-condition-based-fetch.json) + that contains a time based collection scheme to capture the DTC signal every 10s. + 1. Approves the campaign. + 1. Waits until the campaign status is `HEALTHY`, which means the campaign has been deployed to + the fleet. + 1. Waits 30 seconds receiving data from the IoT topic. + 1. Saves the data to an HTML file. + +1. In the collected JSON file, you will see the collected CAN signal along with DTC information with + the snapshot that was collected Example data: + + ```json + "signals": { + "Vehicle.ECU1.DTC_INFO": [ + { + "value": "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"FF\", + \"dtcCodes\":[{\"DTC\":\"AAA123\",\"DTCExtendedData\":\"\",\"DTCSnapshotRecord\":\"AAA123AF0101AABB01\"}]},\"ECUID\":\"01\"}]}", + "dataType": "VARCHAR", + "time": "" + } + ... + ], + "Vehicle.ECM.DemoEngineTorque": [ + { + "value": 1000.0, + "dataType": "DOUBLE", + "time": "" + } + ... + ] + } + ``` + +#### Condition based fetch demo + +The campaign file +[`campaign-uds-dtc-time-based-fetch.json`](../../tools/cloud/campaign-uds-dtc-time-based-fetch.json) +is setup to trigger fetch of DTC codes, snapshot extended data periodically every 5s and upload if +DTC code is present. You can use expression function `!isNull()` to detect if a new signal is +available. + +1. Run the demo script: + + ```bash + ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-dtc \ + --node-file custom-nodes-uds-dtc.json \ + --node-file can-nodes.json \ + --decoder-file custom-decoders-uds-dtc.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-custom-uds-dtc.json \ + --network-interface-file network-interface-can.json \ + --campaign-file campaign-uds-dtc-time-based-fetch.json \ + --data-destination IOT_TOPIC + ``` + +1. In the collected JSON file, you will see the collected DTC information with the snapshot and + extended data that was collected Example data: + + ```json + "signals": { + "Vehicle.ECU1.DTC_INFO": [ + { + "value": "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"FF\", + \"dtcCodes\":[{\"DTC\":\"AAA123\",\"DTCExtendedData\":\"\",\"DTCSnapshotRecord\":\"AAA123AF0101AABB01\"}]},\"ECUID\":\"01\"}]}", + "dataType": "VARCHAR", + "time": "" + }, + { + "value": "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"FF\", + \"dtcCodes\":[{\"DTC\":\"AAA123\",\"DTCExtendedData\":\"AAA123AF0101CCDD02\",\"DTCSnapshotRecord\":\"\"}]},\"ECUID\":\"01\"}]}", + "dataType": "VARCHAR", + "time": "" + }, + ... + ] + } + ``` + +### Clean up + +1. Run the following _on the development machine_ to clean up resources created by the + `provision.sh` and `demo.sh` scripts. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && ./clean-up.sh \ + && ../provision.sh \ + --vehicle-name fwdemo-dtc \ + --region us-east-1\ + --only-clean-up + ``` + +1. Delete the CloudFormation stack for your development machine, which by default is called `fwdev`: + https://us-east-1.console.aws.amazon.com/cloudformation/home diff --git a/docs/dev-guide/images/can-over-someip-demo-diagram.jpg b/docs/dev-guide/images/can-over-someip-demo-diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1c829ffe3d82cb198eabe6322657ac8c93f98dec GIT binary patch literal 108690 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/dev-guide/images/collected_data_plot_someip.png b/docs/dev-guide/images/collected_data_plot_someip.png new file mode 100644 index 0000000000000000000000000000000000000000..5549359edc6561fe65a6e5846a4a6395f77bed5f GIT binary patch literal 60597 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 literal 0 HcmV?d00001 diff --git a/docs/dev-guide/images/last-known-state-diagram.jpg b/docs/dev-guide/images/last-known-state-diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b5a93791df91aff4458f3455cabd7855276c0b4d GIT binary patch literal 118879 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

qafv9b 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 Up6UYZfif_7y85}Sb4q9e0QnN)i~s-t literal 0 HcmV?d00001 diff --git a/docs/dev-guide/images/snf-storing-data-diagram.png b/docs/dev-guide/images/snf-storing-data-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5db759d2c26c796f11ad298a8acf7625bd5abeb6 GIT binary patch literal 26872 zcmZ^K1z23Y5-7!?6p9yjcPkF1xD+d{#ogVZSYdH3?zXtQySux?;_mFD|9|b>``&(M z&&f$fGRY*9Gs%R=%Zh(M#6^UFfcPLOA)*KY0R?gwqD4P{7|n26!KfbMG%oETG)g$|}V%&E& z0Gb0~VGt>VZd6V(U=Cvapqy7&k7tq+kOF4TzV!fst)u_`@*MLZV}p z+&^X}6z$_ls;<0xTvED1_69sssS?b1iy=P&3>lO#8!W8+g0PYgGITIa&H_%rLb41j z?AQ-j!_3BPTW<7&>!qByk{XpB8bj|#m1|eWIpimzEl4CJ z;<|zKEp`L43O=$(bSS8!Rxntw_}ysoNXNNL7Z}`*{Q?HNu*hE#zy?9NL1^rl;(L77 zXL)OUcOx}P<8R*}Xy4NC5YX>nA)ww;?;s%GZfO5Vzk5ql|2sd}3}q9c?ZD&8Px==G&s+LWHWMlF zUm%Va{G@8%ty%&se19?czw-WFy}#iVjT~$&o&HFnY;EQ!!1@oA|H}SvEcv$% zvw3TBdou$`YeysdHwI&l{qnz`qsmAISe%_}_?su~h#z%QqGIPnQ2t z@()Ztravn2A1d;<{{5Bvrho#7d`$mRW&uQ%gTS|e^#wvwL`cc?-EkV+4-8T4e(+&d z;sm<;nRYI=C@LmQ3C4R`(qFzmwH2U6=`$4bs9U9DNn7t^5BO^)zP?8|9K2f#2qz)` zxWT9u1O8%t=WJrK_H688z1G56E!)=CY|!xqlKg)z{tf~m>c1{uVbsiQ zko>HW-hWYgdo3)yB>W$~XtvalN=gJk9%`ijB}Rw(5BgussXh{*`)_OzrwaNYKi!>l z55^X3?41Tlzk65IY8saTtr+L4TBh|2KJv?ESD-?fr`yh8EF2D-@z3hm@1a9>f=On) zw%cMAW^lJBs}@V|R5?|(+8htPW41ZSqpT6Qq zX!DQ8Kse<$!o&zCv7NCrxaerqqBc)@2!vencdDwKS}m?Zu--o_Jr@-*=#@TN zE!73rN209krsf3My3NTALd4fW709M_8ANgz@op5xi#Fn?f9Z1WD9DU7gY!hhl9_b& z4Fz_u(_%#Z)dR#dkekubYVFP+>a13O!hXQZG9DF=2_4#g>+CzBAJh5Y)LPwP%;(Ge z#VzIoivh!4x%%tU3&;zJOy8;ef@hK6`oC5DZ!u^QO!pnft##?2T?7Qc&&5jWu!!Fv zivqv2dwZQ9SC5DA+QI}Ys*q)8SC~XHy`~99o_sNHOrkacQcTpJ)tgRkJz!YCHT+ft zwklI|{pq7z8%W-Io9?PS=>_g@>-F!XGrmH%-EUdYt1+Xr@w(*8M+^>&E&JsGu&W6q z2pRiG{oqK?cN1H`*AQf8KYp3*1xWs-XoRqS73a2LxE%OaOSL|q7TM&`Bod*Q>byRX ziM{&SCSjk#lkky3l8GT1@NU4z3*k(CcsGqa;E;%R0)aW}=%87d`A7MqoZ(BSVA4r zXgDF!x#JZ|MM>#V5UYzj2m~#gS(C=%mFyv{OHJ8`Y>j{L2QTn1lcmi4JdQ`~9)-IxAhFgps6^M1=2@|C&Gi;E2D7-n`qm zn5q%aOfJ!{Ga1*JF`s}=Vl#<_R8;@wQJXp@O)ML_ z=j6df>i49gl$M>nkHx?EK8q@X6ePy{Ra89+j6@!#4{hiPEql2cL&L`R9#i;7v|@He(;_(-s0<`F zfR#gft;OTHy(dVjmRGMuP@~+Ehn7kxg&hy$zj!do$XUO8i6TpJ|H%CB3DMW!V}pt4 zECW0AX>&dCeM`H7Y}!idW=!XYCL?0MIMR>ksNscC@FKy`WcMA`hAr+mdYIva)(7EI-=n*=(TXmoj}2n0>>e`J})?nMI}6nH}Y8j zknv3!P9+tbuv~QKM=d_uy|AY8Yi;Uq@j}`gehGL#T6Flx=q?fH0tgj4r6YmowZq)p zAMdykgU<#D1es!Er${Ij%IjS}lrae$-r%}l69P)qA83(Me2qwvMTu=*yY4rt1t)G^ zI9Y5b&IZ!?;Y^Hql|Cr^)0{|TK*w0YDsv8xzG1GN_26|NQ6A-Y}dbar$KPk;fcwl_mCNikNa{f5lzY_BtcD|D zM6h6h^=i}R%wX)Vz09y2r4PIRFj#($lEjOl!08aOUm+1e;+-Y%Hxjo-hiJ6#(=MT} z)3475jZYQs2`U>Q9`&~ezvt@>$Z2o*JpQIGp8P!Sj}X(*O}TM={F4Zlp=Z9ey)#QV z7`6B5Sx*(=it6u?Dct5~wWN8Pp=6@D1Y!$u5Nr&g2Dpf9X@ zM?n|Bcr0x+YaN$#-c332RFHJPZUa^6F-|BVGGsCE>EC9TI1qtcQz0BNR9Oj%ZDsmV znnnxXI58bTD{;1fLj(qH5wm+DtDD*7(T1zWhAlgr^duP@{mpuaZcLw1{k z?b?kuMEdsfu&_{1P zV)mDe-}A8!@!RYg^6}ucS3)L(8~bS9O2id9epck;#m9fzd*3wpw~Bg2Ju7q1&MHDN z>hx9`m>Ay7Y#5fN1LxI>OZwK^iPTFR9l3`WYEG%*7c2;s5}w8%Zs{26-1q!;Lk-Y} zSEa_6*3vy#BYN_Z!WxR`haz9#e$3}LE;PE)kdk&OCS7ABLdS?_I*j|NRe3U~l&FPN z*gl~^QpnNvk^XZolQ@DtE-wttt{T2$UOQxmAXo?Evjj%Ra=|OAUI9S?26L$#4bBXo3gmRZ9I3W-%~`>n(J{0hRF?nl02nZ% zh=l1<>$sEr=|+}6d|UMA(~BB6P9B{i*L5zPGq{4BtPsHAfD?zo|r7RO~ph-2Sdv$?W`lu zphK*GzW(SzNgkWre*Mkt#uAD-8p)c6xV^aNq%c9Hntef1-ZWo>mY);78|FPg%EOAH zxHD;6;EFJQc*80EqbwYpWp6HVj62F!=|DS@VD)ryH=^v?EdeP3P<@2(Vf^tI!eN}D@5&Ekz#nbzXA>-y1+N>g$~ z4s71VML&_jMu(7EJ)aCl1_XUF?`H)&husUY_VxWW`!bV){tJmTs`DueqILUcJ@2G@ z6G8m7c7pY1zV2U@u4|@AvAmF*o0U+~jErTb&pXntm0sh#q1;<{a(1sJL~S5Jm<(sL zpdk*t%~?)?)@_cEG(I~^B4nZkzy6f^-mQmiW{*o^q_hf(%ECdOI{itE5}n6-^;Y$a ztd@uOS+52Rlh=pyF)iK~EtS7%9$s1OEMn}A?Y<7bT-Dq3Qk)zghiSQa$`_x0cijtg z*^Z#9t#%kjRBz-&oVDOjSBf(vvRS{tlfe^&&;ySM157f+`jBq5)#NftQ@-g|8F@zi zf`WUM(@LG+yfkuCZ2-KNilYy?J<;tai=1H{I!a${GhKb`g&W*zE3>-Y+0JJmVzuOw z!eBy8>~mSa=}exbn|wYrnP`0qw(D<<^@QXho&-IZTnrXuY!|$H+4i2_F34;bUiI({ zC`dOa1lz8(<*aeqLFQkk#aY8C;+3ga8)s`Z+E1C#88}Ri-)D5Dl-KvGwzl^!R=?g` z%t+8{)hI2cphS_0BVQ+MBh{m+RqAtl-?x>2>fZ}li@lhOeRkG`J}xViO=??t9=JVu z`Rs|f$MSWq^gAl&*RBm8#tzT*79%1`S#46rPN9KRZm4t{1)^_6#Yu*KGof7OBc56< zwlL~dMk2#lALQGujY5c^YM&W&D@R*YvJ4T39FNbDyovZ$b2(fOWE@Xt(AqpAH=>h` zqB(73Wu81>63P}rD<;X zu^MASRU2%*bH0>mp8@C+%X8$jT3o@c3Gm3sy_tG!QJiwJ`26m$vSYHYRb~t=6TmD5 zXw{OX5N?NgVlHavhf9ymRpjyPOf5WnGJN5n@t(F9Z(BPjvDqOC1#yfanD?AGMz7tG za5*ZKg2uBAlgzm`>xq^*8GBc7y6v|6YMYU4dw%q8(fo>f z2dha9)Tm2b@(JVq4yOmIX}_2Pl%6~adupuhCi9yhUwLv%KN*ZOd)&&5r0`;gr|<G@Ok2 z=bN^5*>6VTuv}1lt8rg*NmL;`0&SYp@&X>!*9vv9xCBb$)RL zrILzxy7-QuZyqdWAVtqt$tTSnNm`wwgm7lJCB>N%9oXF=tWI-`6>eJ#CcO7<;E(o}sh4i6MMo3r`pMBa=sVU|r8J3YZh6Q}|M+U}O?5_pY+U+8l zf%$#;;sk6>Gs2$l(VBtL4+Glm@%`p!DZEfpID3>Ezx)>$V5UJ%({4M&Mp^M-VMCKyfcd zkvL!(jwZY(;PEk8u@y&oLZpf~!K91A0}3u-lI>yj}iIJXTyZc+mVUYaDe*81Jn96eeb?&(T7BQjPKy; z$g&u|kkSq5VD;VpSSoY>-s=n8ob*x_NvHnF)n}Dy{XE*X2PFocrnpsl(g(2Xr0i>28nSJ(AOH9Db@9Uz`5KT4z6Ewhl6QpJpoocKBO`G+m&jx z6Q|SUO&02r{qc86C(w5VeK=wluHU>a`hMx5{;{?F5K3+wyilfDk9F4elrHJ8JA}!4@X21fmO-}-quOMgb$*{o z9zss=g+7_n2Ul9OGmX_KCc58cUiK=ieJP(InZtut@?i$8!uZ5IjEGYO)~;K=N{Pno zNiNGFv8+(3QZZ(iK){na0-u{U;rtU@#={d4>;_<5FuEl<<(U$`9&qxe8=OsINda1T*HMV|w>AIj2hy5}!Yw;GFwuf8Zosnqt?f%HXH7eB};f}3C?T+O* zs#?zg0PFRxNyZRcJQ2Yfh`KMZWWg`^AZFV%B@Zer3odmJtza?MRnsD-P$><>X1ncRqfw zh^BHOR03*YYB#mmRDJqv`D>>h-RGGFZ5<-pDN#@vdsdikf>MA$U^3bkPQ#f9)pGVr6 zFVe&&@|@__Vly7mRihYBuC6X-A@4=#CrJ5N@q|B%$BHuB-Tm@euQYnZh|c9x;(gL^qyC}b`G2pHzFc(r-R(Q@RMpr2bF!Af~82bR;zQtA^XkY3)b4v zeM>{90BypuJi8f&1%VQU0)v-|{QIonV7#E`HK_7htDsw}bdeuFn4Qn~t+Nfs4nlMv zUHoWg-waHjTjd5N78<}sGVaX??rpG;tOLgME1xr^OVD;*K%Og&rOPP#`6Gw5uf`)fMtgS} zr~;r@M4q-6dJR5paUL|1sX^raIs&+G8GetJpBYasVz-t_r3dv^=*`}}z>_zR3}ACc zd#KR=W3zL=MzD=^hY)gu@g(jAT`DZ_e zKWyVocVIDTi%#A4GI_1Bn91Ng^Jsl{|} zGA!f1D)a1*e$u-v1FO+#A#(+1wO@a1v{|tazuA?wm%)93sFzP3%l9Grfi!d(`6G6jR)dz9__FwGikKV1FF@IF!IvPYs_W(wIsT zdSe{hS6iHzYaRUC_2Ou9agm~oaGO=OBF70>_V`*5fh(&?F+R0RgbGuS;8|bV_pDBq zGW$_(~V4l#mvCw zUQ_PQcPzn|1>L1v`NQJ`Ou(f(KCin{dOHLTzHC#A$+(n>${3M==O<;LQhaCM$LjL5 z6#zDq{?@%+pOJk0H|2@i?}eK?gXsC+L7SWJj7rivEA?kvC`(=$)RoWVnOPy*|eYv6qfd`n;IM(VZB8ez@5+X22oc z3oYgCUbcS3<*+8z!A_Y3!x03u>ZMoi4kwTlsLH-=OkrnuPko22l?}C5>*YgP@~K=( z>}i07)?$6o*e}y=ru45&FH|e4s!@G+9hu3XF7?!;aS9(sC6QAF&9B(g2~=I;ee0AAyh z!mYxKQod`~wicayy>@qo0TDVK^3Oiysg(q2zkY$^fU;+N!Bm9hW)v>VbuHBWxK$>A zR+Z`ND_(&47OW)3Yqhc{W77$I#mm$%i7O_r)7Kx*D2lgl#|=h z;av2D_ZgnNeV# z{ARNkhxsqXf~?hL-E7&MeP${8eB8BzYlbS1UB7xcgq%-jSF65I&73*E)R0SCoCJ!3 zia9Y=gs8adJU2xuSFEw~?C6Z;2uS^5cPC!}n+x<>ILt0v}}vy9^Qf3ib^lr55{KvvMn>|e~aXGg%kJno#kB`LH zo&zUOrnAJ>Hru_z`qRz*O*C8r#y0usp)i^IR(Dmk_`xKa7 zATHF1G4Ki5-i`b!&}=2UQt8$FN`HT(GS--kd&QR55U?Q`|LDim}6Num4#@bo&>nrOR>qZ zjqVzhq+GO{x%@=x)aZDqm~_`T5h;bq9kh^_GYc3bLvld)LNv4xtWTr}&I-AKaCqAw8qSCHa)%1R;gL7x` z3yJee@dV^V9poaeP3llXideW-d4@Ezjx+!x>Z~Js-6?YQwJL4sE0dDfqnJHQOMs zJWBvyrfxO5El4A0t!u1tmaSJ>FfzOz;xE!^7@z_LJ=al|1d8lSP9_R6y!EnQE{7p3 zV})Wk*F#%@-zWSrfw~C3H{$RZw*dcoPIN#q4EIoRQ<<9X02|@)+OX6&z=IA_$Cl;k=CP7^>m2r-aU2IJ z`$=u2>l_l};v837qL0gF*2CyZ)`S6-nf@X@v^gqJ{%)_rBK!ptR$2 z=JD)O@dY5E@S_WDeFMSKi%IE@W(SoDR(O=8R(_5lW5$@mXq@_9iVxQE;aaT~MFLYY z{m>&;Wo|@Ay!3t^A!1+N;$?i9u`Z_JNpMjNkfgc++X5a1g~Pg)CknM4`8s$`gP<`$ zt%AewW1@`}_-&`%xUE>TKxf~Fw$eS0uZM%H$o4RM#0J|f6fmpAP=lv38V^$*5G#;> zf#>gv(9qmWMW@puecGg1peJ1t?It4#ex}n(HP~mPaEe~H6U5O9Ir&0`+0S!IVQu!d z@Qv~7hh+Uma=WOA&FQ51UAm)~Q(Lg~9;HbcyF#4MZDvGB8OFMiI$()gr^>N1Y=ES_BvebFV*=&Nw&&%muXC5Mf@U}ww7(904)b)hA#fl88x zDU8!u-OtF}ECS_ep~MCX>ApG^P)rHpL6$#U_;6N!bW}4%aNV%VL><@07L38kY?A_o zvhXhXUV3sYrywLFY)4h$QP$6`gwoBbmEj2Yb@nKaYN=V^R$UwNkDi5`* zQ}3?sL-S?d$q!e%E1Sw&WWp+c;wV&B0heT2sn`oYd(Lbd#8aDqiZVNg#P7yKEGpk7 zw02X81xEFgLJiK@_agB|X@HWU^=;NqE(IUwy+bU#^*867$*RpbF9FT%$N?Y>ln}ovwi5pe+jhWfCSPrmMH(o@gGo#~joABkvyM8I_b6aO)N+ z<^v{XdCLh0&?F=Ph0urrQu0AO*mgwY>_sMC5Wsw;VgDP$+j9(aI zHLdJoSBU}i6iCXnn4AuRs%0vix^mlS#nxp!1y|9yMwMRsdSRb9R7)b=SgiW&`6#X1 z%gPujlUko-2eig6?Ig{gt$303dNLeFe|~+BWi4W@kkDczd*TnPZFV0S?O@1~mCw4Q zEVDO!%C4eIr=2EUETpZ&p#)S@^p+@3YD}x%FBPh#&111R)v$Bn3QpAZl;NQMCg^9e>@aQ|Wos8lz4>GHqNE zHZ8z4 z8nwcp{-f?DeKN-7cfMv~W4`ZSD-)K?KgjTawk!hg-F8RfiKrq@Ii(Ug5*2ihszmmh zfc7a=xwAy9)4K3Ecc&$%XzC}oH@mK~<3b!|jqSi)Oa2&MAD04wE zrq?3l0g5#89Nc%=`q8#q3}2xuxyCx>x)TPp=I~ifYNAIJN<*v(KmatSN$MgVn+3%8 z`_D9?XKt>$V>Sd$R!1~gqxd>*la)^p4zl}{8Ge}w>N6!MF*Bgsc!(Rzhf5WXzy3O9<(g#vB&L1ul zA`^1`h*R4>ZnYeuDHn{zK! zixhih@}}5S%IOg`95Dx z89p@TrhOkrVCYuW^)iSk8ooK4E*SAmC!AQ%3?Oky*Q#%L0(H>(TOllHD=6oky~viH z?=L)jVtRjODz7?Zk`6@Q*ICL{;GoxS{H9VeOGGt5+YmXRReo$6@5-Yp$q?Y~Z`xlS zpr+yEw4CIivhAz3TC)k%EP%wFE2mF8w57-HwYjJojRBxLqLQTT7n!{cKFax$IE3Xz z8Nx^6L#a+_Lxt>KR!+xgHKSD{p@O?Lx3z)7hsLJoc{|$#FX>Ip=rDR*mLgXWPqXbH z<#swg8^>JUqP^(ck)sfW7k{LN0p3ip<<9)JhY<^nvboa*m7JVQ%Yz2nHu9>X%GWif z(9|a0BYa-%TuD{AwdQdY9Fb_y)3$5NT3M39SN1e4x%u8x{{F3%9EZJuP?eaV;y7uX z=ElC)U(F$CHf_y~zftK|=T+AZbf3@Fwn40dq;gu1?y^V14;~H8n^5WsYH;}Z16xYg zF$qCxpDPdPY?ODd#}O~e&+g}{50-se)YYUODU3TdS|b=r3-4N#>l@S|R1}_rd_`_@qjceI9kK zqVkbg1_l|ip={}K@}%4~u&7iVHuf5U{ySaklKd-^u((+u+n~P0YD~jv`&U!9n$vIm zLy*$uR;`90JN6pFS!MNZ9fe1K8!d5(b0@p{G%~=XmPoTp8~grG|MJe&ZsDatnfv`S z68l!hh-mS#L4WeYqf`SY7+Icl$-Q*JpZ<_6)(Zu$4|#KiI{b4}o&_TGbaQE2J%R=4 z2u+a18n_HFH?{M;nKDVN7deZDy1Hi-=j%Qf({e9H7+^^~OmYDEi`5cm+EZQve=FQ9 z1B2cKa!k{+T!sK9hdJL*v+05_n5A_mI&2QrHCNj-QfK#e1|!eZObV=LRD81~+(AgV zraakmq=G6u=8}_9RB}2GR@^jIGHkNw<}WSZCmIdvhxY^XcT+-|&ybR?=GN&eva2*V zWp``0)HF|!yypw*?U-gFN(GpJ_oZ)JZFqC#Y$y2~{p$PKvgv%2&tiqyx7tJya{pZSeo|i9;g#5LPgWbn9`qHF?;;hLaM}G{FWU+y zMaGS7gU;!q%v!Tg~U)J_S|Qjr|nH6x8E8VA4VYdN9M70h+p;YduHd}ByT`E1QT z9_>zX;7G15PNp}(eZ>RkS;;4tWLP0n`r90ap6+Tg7SD05kdZXki^nUS4xpdJe1Pxt zVTmz~#|u<;65}3X8C$0$9NV9qTZ-ZYx(!$SGDM76KwHWKo}p7Msr|mK zeiF0>#;sZ5Imoqo0DHeIwz$j8Lr(M_&opH1-eC>{6Q~>#Mm`BThpG3JM-Rj+6{@!T znFT9})zX)%x3U%$@_A?c`FI6t-582$zwj~)sW6NW@`*jr-~?)LU)K{vPLGJ`-2A$QrYl%|x^Ux25&4I?b-Q&e`#E+N&{D z6GstR;M%Y*1O0}y`CU5c@&XR*MPchEsiPo`)x72ZxB0{A- zg4O6akPhYtN5*HlTQ2s0c)#@GZPD5xB<{q2zED4~uoIKOaqQJ$s+7{4e>?jDjo$BV z6z9l4x0R%Ztzh)Le=fI^bsgNCa>P)~IR1NT9&Mfa;-bBOJraOn(=VlqQp9RKYNSn8 zzB%dEr|W&29J1PYRL-hT(7bfzW0nvR%d~m(!0M`ZZnJ8$ZG`49fuaD#{P?y7tE@F? zAA(t%<|yNoH^5|gDtuIz@nbfoC?7SFsg1U#$u$!5x_@4jXKTE5Mij)X7FP{pTvk+* zlb@5D(9LP+w2l+|YN7(8GF%L6g0nF(@tRi7d>grzEpy8v6%t8Z%hY~RslKV|^X1z8 z12B$2pn~8?bsSyPBz;8?XJ?j@KV10&!OiEuJE_3DF~VJfFoEOietkOcQSdHUwyz<2 zJ*kE@l(pbt4~+t7?95c6^BJnx@bnN!!6tg)AiP}P#(bfrz0%W!MpvMG$~!oD+=$FY z>cFx8D^MPru23t7#C*n@wwUUSy%-bap@>(b_5`cHrigkh_|A&$f}=aNisvGhVU5`h zuZ;23SXXf5O9*o@N!f%r%2^MvcbdiZi%U0swVE*zX7TkK9DBlcc$eu-mAcp{qgrl_ zx|tewN~qvKHk2tSedlyD`HkF)=G>Jx?7epGMXzk97Y8@UKHLSJq-8AUXK~>d1mM z*;fNpK4h=&>exj)YhF!wNW?pRc-s+T8?w(yDRuzaY3~$>0`^%v=ICWonb&&g7=~KP zN_f>UmQ6Hj8g|!|6M058wa3vDZ88)pmIsD909dbp^<4i{VU_6F4aV>6r%F2U;&6(k znFO;0%k_9JyIAA3>{pF#kCzT)a)+!&qoni~tK#wT&ZNqy$CKMRT+$!}K|ytl{EW?s z2FX}1X+^~qCkU=K{GhCJrK5_m3{6_bRgb&l1izz|pun~d#NHE+vli^^rVA1!-@w1Q zdw3$%v=5q{W9x)}zMX~SaZ%YPiC}q8{bH1#bPDMV2)szF<1#r}=E6 zaz>?flh06B?BciO+HJX;rwh6I^M?gt@N2)8WRvp+e+7o;oWYQ z5M29pTE2$073WOOkDgy@XTL^S?{kM5X+Z?N3_7Yq-nuGphtsCzGUv=cEFn83D#w;O zFD*4mrxXh)SfglFUGvdBJ$&e$0r2LxUkv>^u?Ws-*nH{(6#L)>yI8n(M zqmH+k-^t9PG28br&y+JEDu*XGkmj_t8nVP8pMia!Irq&eRpGVyRk4xGnaVjPGvA-! z3vL14&ffcu!Ogl1aCn?DIGkRgr2Dmn?w*th{yuxOdmbRPlN>8rE>jL;Wk_Ib|Lt(GBkNjC!W!=O^CEiH*1D6yZtIQA*7_-VaDKdY z+*rWnVarTFKf6=>urg2NT(L9S_{CgK@|>BV7l%A$Wo|;tO|Iv3AW$hpCbZWId%$m6 zKWi=!ur;3YjeadHf})-fJwIz#AR?VDjHZsXFsXk7L75mCwBC&8pCbx77JQsf-j`)8Uf={ z3=6;oWPJ7>HE*X%S_|Kfp?&Y237!Vha`>v9im9e)73}IJJF@!(ZeJD8KTD3#8wuSU zEFXa^iYtHXlZ$7HI7w^^MO7Ck7TKYn$!PZEaYuSy^DsU$M>Kj~pciZ~x2me19102s zqt;GDHmKT1Y3e$<2?J40_`BWtS7wP&{!RV?hjo8Fh(V~mfQ%A(Btxg}-`Ur@Ho zt5pn+=BA#EKaeWfS%0mZC{jKw1s{SaJuS|xxDYwGaB8JSYx@6h1Lj-@e*`bAd3}Qk|;><%&gr=0f{(g+dv9(i_5|2ju{X>7E zpg=uuuQct+eLoZwlitML*~aqOW0xkzCl45}IvyvGW%*95j4H2%3#fuZDQ1f*mmsc( zhsHfTFW3CJ2V>*@kZ-{IIihIKgUYSc^N*g4X%zo1eqY9@lY90fjLAcBw|45AIi4=J z%m7ao{l)Yz-Mah!GjU`F@#m@@Ii?~n3)H-xFm#CUgv^+j@MezP>k>=;QWDZS7!06Z2TR+MMGlm zC41?;)^($b=q%#JRb7PoaxPb|+<2%*{mF}#Lp{5jK*C$VAVwRyV_ONWlNk%U$5*Y| zT_%1QC-I3uv8EwFVLM0=6UTAF;v9J&?}j3FmqiIBH?GN)wP%?w_Zii*(=3y*`^vOAN6id}J82bRG&051Q^Xb1#-8>jyMj zF39Lh$a^xlI{mracb_;wOu?~{kkS8)H_Y{d98^(SwI7gn?9F36zE*A!p*RZvY?eVN zeZc${&V_V^!hTnn($YRbNFsy9z0_jHM)@s0BC?&pAd>z!B?6x#q(XKs;Yus2^nQ3p zho3)T6CcLEf~WtkG48Q8^w=e0Kw+ls4z~*dW3=Nt*^K(I?Rbr|UcUXGBdv#|XAel% zuQoJ}nv!^oQlR8n3uf^8?!n_BvnIu`qeMp*yhv!j@7dh$LW?`CH|}}0@Sxbg!>XsS z{g&tb<+7hQPYDMUieP`%mx>>R&|iI(1+x4XncMPZB&F#oh@VnP&JD6 z6Za>52}u7C5DTDNp_>&cOlkhs3;!OaKPGRu!f9FH|MYg1QBii$8W3Rsl@tW&5CjB~ zPKR!h?iLUvB?T1*L^_mC38|q=kQPLwMY=&sx+ExBM3#NXU*vF3-4499J(bsviv=F>$cjc1vR~H~PBM((LWW%6PxL z3h2V?6VKi~apjj_lwvy4n{O(aR@m&MVKR;=6Dxnj-YCksIi`1wdc#z9jpVIi7 zUh!&T)Eu_y?tS*QeEloD!yBFvR@zDK);Z4l(oZ#@_q`T)_yupaLziGTUMTEKI5lQ; zblR(i6MXyEk{k1iXO|g*p3MAMcvD|5;db?xvr({yUIFsIV58aBoUn$svV;z6WBd{l zXuU1Z-yC8?17xX5(I30dNi}aZ_pG#o~EX` zF9=KxJ4sQOyH50KI~P|j#zK>Yo}?~!ZeN{PJh)-hLxl}{_{8tYM9mW_xd|*zb^f6% z^luy2>5V+m4#(!YOu+>O`WxJSZwb>IO@G>WMUk3*7Nfwhe(lR06+Qx)AarkUZ#)8m zB|OP$nq|2n3VVNe3f^LHLwUm^2+rkFkDQ$B5eg2@pKQ<~Zp0V((_0xRx{U2R0H@Z( zsHvxDnKj@~W3+BM>WsozV%Rej%sYJ1WvD|NGQ3bMF`1j6RSaIas_ya1tm>x?E_C7b zP*m%&${yQAPGdmK`-o*dRzx{5agpF|T%&Y3#ZZy4IQBx@3V6Ukp&=&K8*dj(QOW)_ z|2Wit7Rf6gi_7--S}2uZ;KpZ&E@LOcl-JaFl=z|G5^}1;%njL(GQ&;Gn4_>rv#v22 z=qQZvc}q``vT1cwN}84H$Bv(n)i8h_NWvbv!vEN4E^yAAWp4h209cPtf>?(jE{pfVaxj>E}0e@Q~X=~5`Z?+exktqHp~ zW*LdPxw|*^H=nM%$UCPn-NqWjgl&A5dEw_f{V`zXnt;=7jLT#g7K|+(H`R_^QjAi? zHa0el1nsGIJLP4lInWND`o4VmZC3WK<%Q=h;WSOEc|fcnUMpnd+CRRA!f7|^C!dQd zI0bjaS$eBv$cdexBuN^Fgoetg&!rU6Zxmti3AQ`ZQK&wX-2`8=7F!Riq#rmh56l<` zxeOkcQ}~AlO3}qN6|8zi9GeQ-Ygz|G(JJtZMqt-|aFmar0vQ{p_0wq^ht(%Q3PO1C zj?F)?03)I+a__WajXlG?W6>3uvJ7L=NTYYcdIriaY?g4NEefY4lw2yaBMzT-{pPc! zMGNJ6gOgdtjc`*d!P}k_lDP9mQQHmSs>Hrw@w|X3f4(zeC zR-rEy;_%!{DIVUh7Y)Rbl;m!H#VP(1l{^T(>^Q(pLup83x1vEuaJfJNSz znx7A4)h?y>JU#x*fWw$EU@tpS=stb?1pVSgn&4G0agCXZ+Ip#VyCc50){FW~xUNd1 zJDo3Ter(ZYvFaTZc`rA$#s2sgv%G{M->qI8wjksjo@2Sq?RcMZ@UfvX#;xlW+Ab9j z2(q=7v5;z>C{{bSzy4jue@6m6RFYd=)n)P^8b|VVql1676Thmo3Ii%L zJf~!YiR%yG{T%PDJ`g17wH+^c@CVO+3Pgl6W8GI)jOv$4{Y^jzX@dAiy-EP0#br5~ zf$kTX_!n_pO%|8{&*K528-I;w83Ve~GTjPR(*Kws2TV{kD_4H_r#Ac?4|J9QSjVf( zTK|3WuldMOt0PQc&J7X!EvxX$KO+plKdS?`P9Q`#z9>yZibrC1GbYXDG(=Qh` zsBoxx;Xjnz;%-ytL1=I+BI4dD9e|5csFD zA&vsc+i#`O*vp<{0ahKCB#Ag2i}cQe2Aq6Dg)ly?`D(nBGrLA@Z1>QFp-qS|8P*9QxHXNEf$_`ZSoz&G>sr!=C z<$H7$3|ITPR9>-SAVv4kYp8$aH%fuW5YUk{ZO%M*n{Fmi&blY;SE7e;5p#Odlf($n zQooVupX=zurAd+?!5B^dxiq(v7wQUEp>V|_=H{U9nD%RghcGFpLISU-Up z#jNh4_2kwEm(_`Y;&kcsM5a8jwZ)3#H1keiF??*YDQlw(pf>*#6FahP0npeE@o{Fh zIv1cJ7%^f|gM=U1qfzlPu6+2viYIbl2QPW&(*a(zyn&zQ%w`Gv|$lw0td zO>ys$<3yYQ5(H@pUhI7(EbYSOxn$`VxheW0UicA)SaZgYi*XfjY*z6lYG{zvQZ^&l zM4w-K7n-6cGdH%Azm{dcWiDFKefOzO*rRhyw;JU^nhOmGOE#@ICM3u4iJ&IM2!LA+ zrS`99y`8tmTX!EQA!;B%8 zPS-`PQ29c8%q<7{Vy1Qqpcz(sfjs%Q7zU!=z{(ViR+sv-W3eY*Sp^9&sb-Q7$UGrc z!9>)~^kgOmB)f(Z8+y{E5v;p}tYv?!Xl8w~=g(UxlmUMp0{$QXI!%KFIWoBDV`F3C z-174~_thQ{?i;@B69*u=l*1w?hvXt22K&u#l0faEHPceS%0x%w#8id?Q3pfa(<0CN zlHSM>je~>Z{{G|+UMRUpBxPWnd7F$Rcc&Tyju_IA`}d3n3zGbaeYtO8-kMAnuI_z| z->Wwa#(d9g21BZ?)IZQ&*2ZAHS@#&uyw$F4&jt_<)sFIBl}mmunVA;jIEeV`IzLQUIG?%KZ>Mc+T3Q;peJ)YD;xFCJqUZ6c-SRGdY! zWZlyrA!fT$G7y!cVD%`&s6HAWPoPA`3mBO3QHgC}i51PRN;x3?VFM-lQeRFG_El!% zmDqYb>Ou#sF#jTH9v3CmxjOr4G^sOg?0$GZbX9tJNLb0-$OBX_o9@V)NC5_g(|^eu zR?SwepsaHBpn9ZH*jT;{gM=wiVCS|y;ocwvf-5h>+@E^A3?ZA@Y)Qy$b5HpKc9#jVf4Hj&$Q1GpXGMlj8X%*r zke;57|CsLD=sP=~$xTzO3iHd!mYd{t$4l2|yJNZsv^i`2WI&-1f6<+Xv|7~McpF{o zA({r3rl&kMBmUfvG-iMfK$`pR(&s2a+_soP6gxXRQycXRb)V8w&P4s$?pD&RDvcj& zCZR2w=K1};;K_J&hH0j0_!L(|va^{8c~ywO<#T8v|4dk|$39{s`_69kB~T$qH8?r^ z4niZKC9Dk5-Hha_Mk9AOD^e*M`O>MLWG?H&Aty(B#+@VmL$!}cgoK1caM(V+i)V=; z!S>%;TAL5e(=Mg;*dw1W=PgL9ad$I^YCZHXj)bFm?6#Sqw+X2_FA6&z60zx5vF5ZG z4R$3#aBw6U9F}+Y_C!mwtkNvHl@un9WkwNMzj|o4h?JoXw&+eD(hh=%FQTJQ?ZowN z+;zmT=spbEdP30>M$zo7EzGAhQNvexhC-W8t6cKnCb`H{nfa=!RJFLTm2Rg(fP?0p z_DyAlb$+nX7pJME1(Ut*;5Adt`_&tF30?F+z*AJ%G9h>AZ<*0RJ`yzFA%Xi}jD(?6 zzCbZl+NR!dyM=^_h&ln#_r7vDzse06?LTrm((K3`_a|N9XSBd9a;GI7-ZvAw|u6qLjN z%d{+gfKE!*hD7g;isN#-L5wCp+}Z^+N+BmaPICp`LB%r*g?h!-t_QSrs8rM-)bRI^;vhFdSA{a)VPWOq2fPW=G^ znuO(enI#=)n*t)lXF{+2AVCNR7Mn_&PEOQ5?9HUeR7|k?M#MA~(iW9F=Ax%mpkMlF zcDtRuKCbeE=m)KL)&awVVLUdY0lV)mJas)vTksoq(2Ja3HuhHZkji$iB)8 ztz%eq4o#H@(QNuR)&ifJJX0T>20e0uX|sGJmoB&>UK#?Fse{^+`jikoNuhZ3@16EP zZ&u_|UNHIYE)7)Q3M1{!GCr%Tbt~LH%GN9tkqRXbUA?p3hYb{e2G)daIL$g95>0?j z)JJ{Sr9Ro&jzwQ`e#dxIHa0egh02IML|OK5ZX-ZC*MJ7%L24);vgS-oOn~e3w|Mdl zG1aMkynXE2Cx?fdwIq1$CY;*ezYQJC)>hTPp73goU~CiW7aL-=Z7!$Gf7V`p{MrS} zo+IPG2Mj`ZsJO1U>Has&YOOc*ply?1xl5Xzi4Y56*sdjJd!f&eHsM*vtI9>y{q(eqa=a+T5_mxPZ<_5yX@(a;rdMnsdqO~uLuqiF%lx3 zy1;g%8&p)vXZamJcF`yXikZm4KPfJ`9hXJOMYQx%qE?+|)(V&fAe9ir8IC$)Ukc9+ zYed=~0K|ok&>;>``ftih{0-AN@rVNy{bxkUNIch3+u{fTmq7Rl!@ps|n~^l_(k$}VSADBo z(VR|dUCnE}k^J(N8zvbKm4+AsXGY3%l{4hjcMcB-9oKRT z>%7k5I6o)wvX8ub6ow$(gM)X{M=LoIjGJA8>n7jBMYFYv!50JB{+6-U1**+Fw6th< zcc{GNqL^p4qT1c&GDr~I#o`+G=FIUUT}Q=G@?j2#Mz?e(#$6E&N7TD2mqB^V$%Hfg zwFSh0c?F>|sgylTynPXenCl3%J9J!JL_qQ*NN|7L^`ztU@bYr8Q!7C*)!i%i9!rdS zt`h-`5KIdTlJvJ$HHrL>$>t6Mnew93;DPxsR{-tZMoWt+JDZFs=hUa5KBo>_`>*3E zSu~D~y}eHvUS5?7C&$OZM{9K7))M&Ti5*63jk0te(xpB(D%7U|fvY?9 z&hPov>z|9~Rlc`tp5s{s64vp1OOXsndtuZBJW%sK!A_1k-bAG^zipBVZmfB7W>lBg zm+QHnYn2?_A7YA>=peE?_96gN6x_u<68`NLXGBWfokh#lQ;+U+SJhnHFz!1CF zsu90Byy{9oRwY=?xgA$`TvE+htfjE55MG{IT2ZJe9Y#K|;IYrd$=S=ASf-~bAka57 zl=8LY4mU5aU=WKWt+FJo@!afecBXmmN~5An552C-O15jLUn9-7&c;lPxQ!ss3Y%W+ zeBX@U*+sD!wsPU4Is3D)SNPOkC}@y>U!VlA)pBmDhsO-9q^^Ezu~*&wUScGjD7~p` zO!jDHb>ErgO?OeBT3f5u(Wvamu{B&MhxHLxYrQ_Xvb$&!*3xOB;5#4BxW4No%v(Qb zJDT>1u)ZosE{UIbDqx9R38jZNnlIzex`(rN zNY&9C+eB(>?KD_255&DvmcpK-#r&Ecm}o~8R8}?z@L~Cv_Z6G3Pt-W*H=EZGc|!2i zPB@g#4+_&J$%YrW5Q~%)He)PjtME&VxO^%W|aG$osk( z$s&z4t{3Fz!RTr!8*M0{B3fcq~I1@FJiqxV8J>Qc!dh z3!Sh&j!AJOL2=)BaI6p;)FS~iRV769mME^!BF*UzNF2x)bl>m&CuD-w=po7 zsEJKT!F`=VwnJ+E^jt6rWkG9L3VIc@c4lx}%5&g4zP{~h%F3x_JC}T$j3pKALnPC& zg)IbI*<6$3?h)j0#1wgzOQ3>fl;}U5fSNMzuuW!SYUDN!3m6Su#%@IQ06m<`G|Fs;7T0y zxxfG_DcMqR z)5p_I$;)HCrM;C#FfYMM*3($#6cT#rQJ)Y)h*X;U!TyEa{e3}}%PoTI-+@#dwy&6g z=V{{bq_e^ti^g~E(oO|!?Kn9(IbqW4AszWBqRzG_mK5g_r2(?DyBnAF#$mfq)pgy4 zf97y|;MVThXrZP-Xa8=lCbzA|!YyXzf$~S1oxOvxfq*mYSg0FRO1vD>$eSdr7ZWe6 zzm0<(X;;VF+0}&=jC;X>9eUr{PEzq)WFU|P7kx;&|H6a#b18>_7yJM1w;^y!>R$A7 zo&u5K%G5^ z)pT#p*;aK*c70V%n4-J{5OMh$wk3{grADSgulcBS?5w_=6Ul? z5!8qLJS^&0dqTo#nYxOqJzZU&UzyS;Cu8_6p!-?Jl4a%k9a56G@CCNN>Xi?u8uyzG zgyem-i0o$_v>DNh0S!$RbIT_sxv?i{UDQrOf|rm|R`?wktpq>sJ4TMVsiA2WNV02{ z;$z&*Cp46!Gy_GggfAINIp1Ls%T=Mq+e`%rp~zo}utP&DE{Lf5zJ?5;&0W9^Tu70H zhQ5AyetzaA>#g%M1`)BdR8@+F(z39#e5f=t7*$q%V%O|!(?P?XKcpgX7k}?2pGQ`AJDWhb21`vv$3{yNXun)$CL{UX z#R()prYWaLB4X!YM#9C!!o)%*2v0&n!tY@Eo=;g+{IB7_Ujk$nPEH^An3-K&U71|j znd}_QnOS*xd6`+*nAzAEfgX&G?zT=wZj82$~!MHWCH^UED(Rwfqa|JyJ#H_QLWu$Mc3hW)1N&vN`P zlkthz*?e#?b94l@CCJMEdx3vk```Bd^j9`>w6k`8p+e2p(n*l*uOa`s{O?hUzyq@b zSngnHB4z7j<^U||Wb`u0e<%2V_xSf*3J#WL0E>T*{?Cs8e(i7lU+Csjv~)AG))ck0 z0p#XoMILrue&+w{oqvy1b2M`R_yY`d68xXw{WbXSH~&5G_mW!wTauNR`|nHs?ap5# z`I%ot;%_4INB(|a1q4tKo}c-@!Yl}XOh;o61||$9CHhv?4g4@2%1u=QI~X}85OV(e zad*3z%Jq`hm%2FCRUq$}gGzHUJ48%PCM$eZymAClz;s9Fc#Gp_TGZ?Cnx zUZhJobb7uY6FoiSyAMJU27~;^izE&`6mM`jdix_7tl!^Xeq>#NLf^EQl z{ns2|ke|VNQG>z%IRO|f3wS6w;wuy|@PE7vAy6c6p#L>3Nf#tZ6bs@%wg#-?2PaHm zg!7Mmz3dU)kEe&_U(X+lpdf&2dn8XoIs;grbzb*64--(DCw`Q zVeGh0P&XlYf#{~yYY5hrm3O1i8#Jn-piby-GJXq@1vy6NMuU zWN=38x@(T0PkuoEZ8hN=fZ;)zqz+)m8K7eue=I(1V7Oo9iD!w*j_36nKDRBrMu#Ngi$wLK{d&F#dw6)* zJ^d-+^z)AM?8@h@px{Y_T$CFY1q!*itn_CVaI=A!Ftl@-y4}g#c$GQ}tTO*ovAyWh zRPKuD^PM#ujsBnPd<9%oQ-$M2{ecM4aM;xR$M{GwNq)UuV6j zd^WKCRxJEclGkC|a7VCV))1>Gy;CZnCW`lothnmF`L|ykz=(B85!EpG75?s{D;ZFF?H86`ykA zV*I_+!5m`>JBTWs-#r44(^kAhPHutY~;7DEs#pxCZuzr3vs3_r+OPr-N zmQ$u2785AasZ{AUYAE$^iNZ^DmX;GS*$F)MXxgnVl(#o`fhklu{|52qhZ;f*v+kFF0$5HPtD+Qj-A$x=}^ zivYeg^!Y>Nv3RYy9QCcY1nd{Eg4v)E7}vL5#*kIrrG1LIBmf@;APVIP3U|gcl)go! zUsi0)WWcFDm-Ag7&!)?#^LHP3!fWNzQb!SgZK{F4{f>a?PiL|*QXH?2kKR2$a#hje zVCv}|fam|m)B!r|b@%Ya<#0hfGL@?q(BWPO$P_Nn+vNtE>~Zx4A%&rp4sZJY#I2U) zqPQ|%s)&-TNYMbEn%F2|&c8&yYYMVg?|N%Enbw=HjJ1a2K9q@B+M9%(SYvd17C5YFNz8 z%{}gB&XULhF}bR-G#mWvc6Hcn8AhgM9hxgS_rrqeuVEIDUJLb=^jCtDA9z+KM@JL$ zq8SNUDj4KeZqCX$sUj+~FHPPNXmuhh{@Z}#lYrTqzDoC_`@3}k+M1bQy|I_~V4VLl z%vIok!RMNQEBdX0{~P3Y3D}zduM4oe0bjp9*}F{T-hAGRUpyBNeqsOjBEa&mh~q@3F{em7}3Kh#-Z#G$G{s#YOplcYN_`_oOjzT!}oc zF5r;x`2yn#CFLXWIJ2!6`3Gi4BhzxkYBPhg3SJg5d8(bku2C$RPpaarU9%I$@NS^^qSSeRLU!m=IceT}3ngWTKD=>~yL0+XmcDnD| zAE6Nj0~~V%)rx2(dpnblY^ z!0ps|-X7xj27X0?^-gkQ@w{yA<&{eH{YPxN)oh84Dxr|~ldMQEV&1q{L_a73jZ#)} zkKHt{UoJ&?sctV~=vC|s3rK2E{Tf>a=6+f*{}xE$Jd$4*dn$bPho8~Jg5WU=x?7f_g=*JZp<1tvrf_E4n@GTTDm>?~o-*9t=NL2& z_`J5qSd;);LjFzM_ZY|uz4mvD{QTDMFf9F$pIt-WkA1H2j-1HV9N9D}`wUmkw#3II zB=j@QShv$Ba(gU8Y`#(tff3Z&`n;a~Y9`lH-J0xR>#-Z zSO41%vGtmPpl^2c6YZv3~hRbHh%9kHw1&( z$1CGlBSboxDR)Sx&5iVErTL?Nhg){R8E_)`yjEM-?hKlz>7HtHS4m+t;j&q7NK87_ zE^&SEW6Zc%YCORyGXr%8+}4?$i=UYlVo1eN<~P_ZsazfMTHIX}I~^@K$6>0UXw}K4 z@$?ybh9DD5Q!5Z?T=(f1>@exkzI}kK^SqYVu6@tB+U_VZo2{Hrr&?`Hi8(&)`1)wI zHg(>LSBZoA>q$Id9HjtjP3~?nmfQ(;xj!57w0%Yq@xxQE^)k!Sx^Lp9-f}(zjoO7f zEVIsae`e5WZ%WL;!J%Ne(KZgI^}RwYU1CXY!X;?dYQ^Aki{qWh@rt|U(54i}Zsild z_|)WyFGqpw7q)o(N^{2zXB#6VV_G=A0zko}AMda75pmfSPu9E#56ZCR2!x_OVbP1P zt*!At5fyiI`krKz$Rt;+(u$p7I32I>enKUuG8;mo|Y?3lNT4KCempXZ~|U8er)E0Wvg?9BA=*h z(((szWP9A99=+k1GPu;8p?*-TK=ud*eAVM%`>zcRrGZe&^*Yx#x5m9DUeQ3l%)C8v zZ9c=??SAy+NDYb`e?#2jxWjd{)EtjSA=`y~B~@e|UGc!;8sc#3Bdu`L{b6(c>!MLF zW!#?E`OawW4Z~+TjoU=q)wXW3ZDQ#ZhA$p&i7g3qm+0wy-k-#y?@|S8sRg1B7x#K@ z5oXFkJ0~S{5LVq@3Y4&P>Ls${8T|R3&=)FL*}x;O)_fzTag)ubkkPiDKg$$Bz?2D;X1OIu`cXB-!I3a8tsbPIlFE)HMeC(@z0TwFaahOkQQrLu zgSVuBot?qscFx*D0!<#IT-x|G)VgxK_I!PAnB(y;-{h zBwwVE$CYLnIqFPtzTQz{snM^6m7(UiwDGovwVYpezI0xg zH#|nRuVmvmBXBBDN+ukODa+(rlyY}KiiBVe;hTn7k6)RcinF9`WA(wAr5-Ld@pWD@dl+IWvmu-v&T33a9GknKCMlIF#0{HMkmk3l>d_b%XO)_x&vJawEVjdC@o6aw~GCi$t8 z#c>SDTL`Q&`HLT4VX#1gB?d6a4>AH1JHXC1H(VTqWI8Q?#~cuAl2_wAkfyVM3_ zcZA&&x6_Tu-N~Ob&b}G@F2B0Korx|&`UQ6vW|^sFlFC|4bsDUxQ2D+}MC9ogrs;|yy^KkFNH355hJ})oM0uG!;qpPn;NWtjKO^#ZhTwGj+vNMD zkDZOzj34)I>2Gz!c9u(ED`8O>61>7Ga8VV$3d;m+yIVyk8 zwD%ER0?Ft3W8p!%qB>PIhuP+y*{YUs$Ls!poYVMX4KxlPGB*mzWz0@|@Q2No8JSFj zp@iZ@mbe=-#9FgqszMC*_p;82Em%}|2sqr0IQtpAzHQ#;>dEOd?AjdEpGL9rH6P$) zCWuwo66FwjL|4W&Zf>!$++}Do9B*+gcnA>4I^+O*f)P~ z&Hu>3!<`NsM)yWyApQ!;3F3s|^{>g_uK5jrTtjWRvtDZQ-{*eN(~BNr>ZY_n?n>u# z%_m^U+w}MP8jPe`=4P+m?f|8OI;K=QXEU2ps8##E=#qyndo3F0p^M(Ae`ImLTO0E0 zkFOyKl%mXdQt1`7j;|P3ItUGm2h-`JQ;4uRwFl@SSKL;^LJ{O5qy(>zSJkGU>nK&zC30J?QGDzcMS`!rjeT!$x))QfhS}Y^VPr^f(e=Biz|x z1^>Cs7mfRrZB)S#of%Xwi$uzbIy~%>p-=mRSHUrU&$;tKSIIkhG_w9fxLE$L3CKDZ zy(qlG7q(~eIpbjoW=a;M4B=6sPLb8rp5f6jGc#kV6Wsr>p1jlR))+s%* zy6w-vrSMxCPcY2f@tbUTC!=7?QlXp~V8qvoK>Rt(0PmiZBr2PmqnDa4Vr|aLST1BN zK}0oXeWSCCit%ULqm@KljrM7&Z3U9OU3bo)8IBf>%2z}fVlxDMPBE_u`Lhc42S4Vv zg(Y(zNw}BGMx`~YX3%TO>bANhZhMTqCvBirX&O+r#?z)w|z!Mqv;^olCPl@P?jtSLj?uFm-6!WL;Ix&xqI!t5U|=ysuDvZEOD3(O165<5(* zG;ad}IUMgeJf3D9Xh7Y02KgtVx}bNxDD)y*u%3@3SSowDr$76rNKb19$kckGg_35( zhDX$UD2|V~+!fFbB3 ziS+wu3k2pO!Uct4(;r7uxz0B3*biChX#p>hz;O^!#u`ha(%kEqEbGWOuHU^$S|d6b zXXPFF#&eO+dmrQSXj(>K#cZo7nMq%9LwwG)O!HX!@$TyVX?^_IIUkx!F1P8DheI&o#+)3UBA9^Km}$ zCye||cdPV^k1VO|pri`gbN(GVCFY!Fb{%#~SZZo>>}}Lpi>nsITTvfuI@OymP={Ds z7VnPLe$+cV=*{I=rh(>56;u|c*xjcfXFc7F89#apcp=}9hikI)N5d_Xu9`;@c8J+RFu23BhBJyfA-4H>{-#!fle!@a7e2T@ zG5h9jUc@+uSYd3r(Jr@FS;$3_^uV4iZybf=Q};=K68WK-w3)})^2AL-TLQ>A`Ve|J z9-p+Y6^OVgjjxZpBi~9DCK4*Qp&&eau;=s#vYP3lN*aq^Bn*}i&XoJJHoq+6udjD+vd#DA2_NXiBb>y(+r^D|VwoD^z))!8w0e6MAB}Y}F7m0c?;EQZ03`H$LoWUwZVU!lCj5OTlm2Xm zmw!kbyj=u440UT_HN5{X{0Tsz{7le_G^^uGxDd;0=={Mn=+w(0;Qve8f8u}x00OS@ zAv!hOdA`|HwlbQf`pJFH+oLpH`Vam#vs&fe}-*--{6eBG0<@Lsvi8e+T2KYVHoAT{Rj|+<0_j#!@4z^ zn#`0YWTy+!TZ0c_KJKB^0Rctpv(kiBS<6*1mifl~v_>h*2x5)^Js4!>=?X|urGg^) z@27GB*+Uk=VSyrv2lq3|Y$&lX2w{8DvlF?WQkWr?`up&~K%N4<>P?@OGw_@s7`1sa z^O#`gs0|TJdhReHO2K<$?bA^HJov76kUiuPb#(7YEGgazZ4Z432Lfm5gZXlQVR*vi z(Qh#!-<^TL@ak}J@axyFR+poL)3>I&kF7q0ka4pfq*;bKxajKrhEAubR8fLAzIpR@ zYZJJY>~&U`(%Lnq)qF#)%}m#*e}QnqnSQ(gONk-l#K|s9rVD)IYU9y`R4a41?beOU zC3gp`nS#rxV>@)(ttiD@o%8!6pX)pb>+LCf@P7`Str~@!^qgVHGEWX(P-GYWI=#&Q z2y-=gwl%!F7f(&s4Ft%8eSKQ#)Gn@tOZ5jzlg47S!8#EW>_9r`x3-2*8_FByb))&K zwyGng@Yz69%Elv4&%JheGE(6=bohiFqtU}J!Pgd=;^ zgaHm&Cr~2j^&pIVyqen=j-}Wyt5xLl2#uEWeVW7eh?MgIXBL6_FL`V{ z+(&!o;}xx;^f$=;Y8DecZtME}qByDMv8L}aC47hKde2TSyDa)S(YbLp5H&|&~Zc>iCiGBRXv8F=!@64<;s#?<(P$p6^4w{V~32=v;$ zf-n{=6-X!97?RB5X1W;3~wk5k&N<;0)Umj!~Q<2p(OY%_D%vY2#_*Rcze#h z-`d$Jq}S;)TGSK@`MZbVI%FBZ7l#+c`zu*_@v=-9bxYN!+;RT=v2o-#GzYGTV=o@5 z3l(l5fMu%bMYTTsVeAAf(7U;%MXAnWvd&A6PdpMY(YQC%QW;Ma`ZwpRm?0V{{<%V+ z-(uUW*vHIoR-zc>i1X???-!; zzNHk$SS#UJ+MxIcMjCVC@dN;#Pk58ckjp-SR0LW&mJPGL+0CDu;wRx?1Y%99OUaBS zjm?sictKUC#W}xuyo{3%&Q1fFW~Q*}aY&}`Z4hv1-~RvsXEJ*CQ>GI-ihu568F#6f z5rMq?_`mERaX@ED%X7zgpOC1#PVGV}d=34W~ zZN_DhGA>BvP`3~0FMf**3cU7HE}0NUHYl-V!egy*_wrz#6rKWA-2P{OzI38*egG5# zFUMj2u)1Qdc&EzrRW7ajPy_|245dsa>s_MwOs5Xg{%k3X*~sVE9A3#x!!j*bT_}x~ z+^G8ROL!M4u(|4tDU)ZIr61R7cCQbK7K@kcW+Ukw_i7TYKCKE(n=AJ~!bM7dl>C!d ziW2E6+mNi{u)b;axLI#8V;Atg3+zQt4`enFJg=((QzF?Qu{U-t(QIa$E!NXmb-f_Y z$T%-{K2TT9eakGJ$_fJ+hvanQr&MZT`W!q{vz58|Blcrjde63EFLVl85j+ma|HDEr zwge%Fv^$;8nXQvXr&K=xAUYL*kb8Zv(WiUF5G<}M(l5^X5Tr9n$vb(hWEX39vS?MP zCIKHjFgO@yyWY~*8NcXW;v&7&YTihH1C54))C=E#NDC#$(Hz&C)# z(g0{|9ccbFkgY^oZS_okNp2+GnCbO>O8lKn-X6hCjMh941`uf=Qpw}7|EXA}S*;0q ziWm%yROGwfk-+55_vv`!-Q~etfy362z^HNOX~~SBTr%rE3yQ;Y z%kV|fpEBO`4zVI#fTpt2FP8Nup*ga zF@r|yT2u|oNWoxo_$5$+&HvmcScYUGcCJ3Tuc=-EDM6k)tdIf2b~aFmF$vf84^Un1e$yel64Doknx|mDMK^; zhLY*!+|RaipB`bR_!@-e%!a4*8epYR>^VIyc$|;NnNt8Hp=fM4xl$N_Dk&V(C zPmK1*r24~&hHgn_Cc|cD`aX%tRmPi`Vu|A=1F+Ea-q(ZOk!roo=hKfaxDxbe7OT!^ zHtW@yS5@bToybws)a2rbUo~qtn&2xe0R5R#w~35(3rMiE(Y9E+Q1GFh3F$4rb;e?d ze72BWZFSDZe>juH*2Kw|{h|%PVN}zV^^8Qu{fG-N3bjqt3bF5!8O%y%E`rO&$kK=e zy@Ul?UGt*|m*@^+n5O2@tI1ZIeB`tW>O705^Je8|Sw;F~_i5M(De5+~UYF}|Y1pV> z|9bWN=-rhKC)&j0VBzt@V#N>q)$U!Nx6|Qw=)0RqW&&Q);$~P58}7I$f;Te25tI~2 zr;a9;&z^@3RA23O%XxfII05G-D8KZ13bkEt60S6G4b2Kl#w3K z6qt7IkE0B?+z7O8Dh_tSHx(L~K^ z)}4b;pe~U}s7>xsoPo7rLHN|gR_om!`*xnwAw#~-aE)q}CRE96l};AV_S1%>i5w8X zkWoA+=HU!p)StH1i$J@yR9XD+q8lDV0_e`Y@|5XL%KQj}JmDMiIxaM}(Zg6GvzFA} z>0nzcw9wMf6GD0=ah%b zZa%eE9$qAR+tN0$PzNKEYL`W*UZwu^xXyM_RJ+3qYIdU@)z&tORQpYeWtq(~OEloh zgo~$4v-uM7<8&fEJ8kz0^3|CWBAw`DLPu$tI90BdzD`Lw<+j977m~3a-(|4EnQ(q;J_b!T6W-9IM zD6N4UVZS^M(?nj_Pb~^w+HV5d>3yK|T1P)hsoHHx0>)5u=)G17hG0I|nUuXYroj+boI2o5_$BokXT{z zO(j9bCcSP_$vo0$W9PF07wuXncux+`?4eTCcHE=Y>Nr8KU%8KO<`q|Nj-4*gq&|&- zOdPfu=Vj(~2bP(E`cs-;B+Jk?yH#ia?6#!HNJf-g3t)46dN9xRY-=3l?bOK1t zSuJ;S$K#W6Ap$b3$eo`(`StclxR4$F2*MuKZHOu<&M8z8rSTMfbVYWghGU}?qj%&sjTLT$Jb@hy_!~u;e#PHllr;{;EkN#7pOd{&5GY{c_89uwn@x)+^dT^m!> z)8J<8(e8FV5XL)gTIOt95(?g+JXNpO=q0XEkOgTKC!~X4B zu?EAE)_JLnU++V3Nqm`q3THKT^RJJdv2lre`$6$_7IP@h;|o`N^YnR+o&=!az*wI* zVu(=K97Yt^qD_I6p*NG*;@U)(7X%EF1h=~|_WR#?UAc}{8&lWrZ%Q6-QqJel3m{=B zR4hn;7bz`ZgY%eHWk1Ey>VBD~&UKdgHsQJ66&`3)loDG)mrpm7-8<+wF;hIfi&UBX zApvEs`s%DtC8%qZ@yeAZu5hTR;?{?-l})YUU1rLZ`C6R2tcGE-RJdg}qJ$4pIW#Q8C69=(bky8+3h|4tw$AjEN+Msji+Y@|l1?1L zAn%vg{=u7O^)MMf-suZl|E#{Gf$%BmE>gZ~%cv)icE(>1^4s>K4gu+arek>Z*SzW^ z3N5_ZivtaL-;nQTK_>g!IlY5>ygD90DM3j%dOV{5bB55Q_q_auW7@rIrKN%MHxbYI z$sqO+S{MZz^bq7dcJEqEYRQCz-T+Kpw_luyRHqff;R|e zaG!Dh(m_ou(tW>3&WXvRGWS3yoz?_5b5S{8sb6%`UpKC}7F+=`v01qZn<+MdNhJWy zVTkvX8~0T{TEXU^&X@jzt~-OX^Mj&ogNy}Au>k6$!5AbO=BZT+WO1J`a{Jitcrz;Juptq&D|?sBSid!+};tT)i|xd;LZ_!3THESeThK~ z^mUQQtJc|^u~DC#=c@{C+LH;g@HZw}rKlb^#<6#S`q5piPq@oL!I#CvH#xYSTO;@6 ziA57J(60$(nGFuY)!DI(yUTjL&wx^@y%1ub34Bm?W2trD9fO4+%YjFo?>rtF{rJV{ z>h4tY&jkAQ=me_vkje6*k=LkcdTEwx7o8T%{kKjLdJfUVb8#6$-ok97vIIO74hr%c ze7gOuWyELi5mcl2DFYbE30r?+ox1EjD7E-(ArcBSec>w4km9cNQ2If%lM1-wGeGq>EWze0|SvC5};zwEcVWb^$xjw&A->?SFp-M|Yxf2UEBkSCz zUF1b4tguM{XJG71^}Aa(AY|m0fYa7V0}Z87vUA?$5?vnXCSrGYDjkr*V^;Qh%6Vs{r(#bA zeEBFf=l=PO#j2?J?J6$*C+hjasJ#({lWQ>x?{{?T6Ycy^(q|oS*11ABFqrx0`{>XF zTJV8%T1fC~LTUS-y>JRH>T;fj&w_W?>&`PdV)R5Htr^W!)(-N=MWZq_oM5_Znfd~& zqp`V5M=c&irOBLAANHV6k39zg9X_rOKb-l;S{QmU9Y3tm#ZxIrL~6gz$^|Uk&Ddn51rN9kIGh1&nK=*4%ap3 zQ2OS7WEc{?klI~q+v@W}7CK^8i^%S6EzD5sf`rR#TBD*?gLT4pZ|5bYPcvGd{jt3_LMpt1 zhGP;y(lHO_3j0#I647~X_OA*YzQnCfJ4St4D^fyNiTlEN{8b{lNRA#C1k}wHQWwk2 zSSSINbiPG$(Zwy*SjyRrcDmmMf(1`gveHP!*!MGJu`z#S^6534Y~0`8PPOYK)4ts> z#SnTMcXA9=ZI@+DF8&DECesZkcW=M&dAD>&dvupN&zx`$ebnlbaG*f6R*-P$t`JzA zFXHwsCHqd$`z|4c8v)1Ha;hL-2#*_1okA>u1&`I3#yy=k_sQ05EG?+z`mkJdOc3iR z9GmewHlbhwzZM&x`&r&nYLVA1JVhtB=k?*XKzN#Fmw5gQyd7sopYW~Ka=Kuu`xQ(= zJu~}>{Y==kAlf8`vJX&tsN~5snsNkGtmU%>4&@!xgJ!der{60AAex-CxvZ%UkY<$? zK!y43ZesnaeMOFX94`Ec4TWIb9WBBqMwJSi>^GD<9&T?lAAI;+uNd@NoWj;MC4Q`K z4NrdVe9Dr4#ZN1r203KK9@-ZNiOQ8cM)Ws zSCfCqDOL1n`BCa8wJeDSmRCWbLMB83TRR(zfWrE|v2;g4Z{%=}JeP)`*NsrihbMld z-F1zyyrp^&G#-d_%kp(;ev+@`gIIz^6h_@mHsQ3l9zF&~#_s@*;M59RH|8Mt7z%rR zWj-K^lY8xBSfnPM*LE^-L5SbXSu5>56yZXr!R2$;(<6;dixW5*t+)6m!Z%!iR4vt* zu>&Yr>b!cA2`W)4qh@IWNy+fCKm-n#=%e7)U}|y{@tXqB8hC>fqkdgX9JRvR!8pon zKyykuBaI)w3|BO3RBB7{I&LW&gVxssHiy+W^}X-#M-=TBpdnC_h8cCW1 zzlSMpq@d-c*AOT3o^#}T^#VD4F#myfT}-```3O6Eh@D0e45UgqqWp3LVb7>O-?{gQ z)oQuzmb#MiWM@JIKFFn9ue;Ki(oOg&(a-2yX4v>{k20XE{N(zm)^^JdJ=IqmGTUrz ztrft?c&6xiJo2`ldGxVWqgXe8-SW0giQ+f{{cPF28ggqyN48nysn_|&Q= ziGD?e|Mlz1${2+8_)$&-?9zdZfr_baf#;gUoff7~{GVNRc@yP{7P~=eEh=9O_La8q zMlJgTGpud+D#EF^xQ!c!(V$)KGac3$Hi|5Y4H7X2`uY;DJKcp{Pjt7OX^@@wOjkoS zkxJ%l#kT0Vm2)hGlkr;a9mqg>B@dPPRy(`?z9mQkA|5=*2{#AHZg4Bb_gO(RS^8Qf zTBQvhiU>^lALhq0g-V%DmTMOkqeBa>W94ymD3aOi{j)zx8LKen4E^=DC_mD$TLZX0;Uzv-3PeKN*k(z$%UK3YzrjQjHqJ;zgK!fVd!c;MLDWm9b_ROU=eX1F!_4Q4*Jh6YFT+gMH>8d*JK zjqxoQE)Y8KOCM$gOnYV1^P9<+$=#7=35qSEVWeV@Kg&Er-q@i=1uRjx;sS&pF4ps}?0&fChi8rt;21f2#!H35ocSO45n9e!mjyGuN}s zy?&*FuL~uh_66;wUSMqCO0#2W z?gI}1lTgZJ=w9m;Y*=m#EHCRkj)D|Bqgt46r99;ccIu72D4QObe#taqGwUlBD+}$N zam1{Ff4fv816pMK2Z52Rxgkl26@& zo>vjigZ%~H6f&jd#H?_DbjOrC92ru#SFsp|JuKvnPeDtr!1aV02b((CjJCK75H zO)9z`TGpV`f1{K-kJ}fD8xQSto0%l*I^!C`5dMRbK&UN!qhCfA2=mpD2{#&=Jyx$; zIPNaBIvQ}jMn9()7N#4+Ahb%BvEyrun$io)x@aqkwCiCO zz9tsxC^S1SC^5C`u(hJK)9y>R@3R_BaD=oEVNfTuFKq)Lxw{|VhK5exW^Z8b4Ln71 zm^s1dYp5!f>rGXTpF!mg1{IDro%I(3G5L=R!S7%r71 z#6`9AjMIXgK4~>dA+sN0@8HlVH9&<8cA7;siEMQw2zPzTb&k`<&U+)LtI}W=*Izv` zN>}vs$Ll_b`)XIQWJUbiwv4>EK3)U9cE+S^Oa#JZGtYrW^4Y1k^l1}rk3M_gDpL?K ztHxy2+wv{%sKwZy*Xr*%NM4%({(C_J^`mE!05aa|&!m6KcNVCDN+|q;Mj32o#RhAl znKE~owV#xj>F0bBYn5J7fwA+0uiC^z+gYoVVl15^bzsV9r(8`KMvLe3kmU#_ljax@ zP=o;f8reC&qOp%lU?>7X4U!Boqgx^8i?$hOPp|t>BMVYpe`+l~y>YeAXf1UFxf%cF( zrIs&cci4JVIOW?R3_A_9JXzGBu{NK<&)>(fiVl($CWE(rTFjL#_ES_Z@?bOTeQ&%X znyE3vzuHYs`?jQo_r`_br1ny}d26gM1M+08O%leXGMiWBZern!Ct|}E0s?CF2hBHu z+wa%Z_bm~rZOUwPSy|upFKx+S(}EhxW=hm_rWnM6_swkmRTK5h$IsJbR$H8mQPIQM z-Vc?{wWjmwfQ0wC~fiBtG|(j83Di(kuoj7V=Bfa)YNARj7sqFcc4wLDeo<<($`@EMY%IW)Dd);F+9hu@d+In^OY@7;h%SY=C zen1&UzTWF;rBsvL(0(v5GTN8_1wdpnYU%v;+fvEw0d2OY)zUW=F3(yh$#c<0^JTU* z4rc-9J)!W9Uny0$hwq+-f_^g4=+Wqm5htX&o!YuZ7TQtC#L72oD)f3!2kMoC$0KMR za9wBmINkTVQ~LBR(WMS*c6C z@H$q9%u$|`Q5Y&z}2*++OJIUFfdZ-3JDkMQ)(1wk?U zUpUKmC ztK-KA)^Kc%@(dI}86cCR?@u4=yywBK)-Y(7g?B(pg1 zP;fCsjqmmTKjoe0I~`ry#&3cUQGyV?cY;Lkee3M#z4x+p(LxYJFFTSDC5Ya84G}~M ziQa9!chQ4)k-X1y{{`=t_lx5&W>%Y7Yt~%X`8zeb7GBL^zlA2BGziL)YX8vC8X)tI z7=orfW+CmFT#k74i>UaTRIzDG(Fg-4m6268)Ht?~o?u{>V^V$_4*E z9@_BJM<=@#t4V$!i;#4()F4IF9HSPYfrN7rIz8XZ&_Io<7|pXDpdKv1E?03ko*^om zBAIir|EOhiOkFJc%0qz9UG?Az-1w)k(xnr#sfAs$QZ$Fz*=%zo_rdOi;Sxh%p5QOT z!nk4<$TSzw=?%;;gh{cW%XS{!QVtxN>_+&#nrUvpGu>$c2BI-9Eo;j!O1A)pxviv9<5-S$-u~ulOUWGR_qf zQF0+o-`cFQqi(wd8Ay1OFY^t`X4k{|BREFsSR_zrFY}qFxf2a*>QBcsN;O*ApLJrEM@^FEQQH>psNb z7@F_lWR@@JYbOR{Gf=9o2bqqZ0{Z=E+uEeks{xA6F@8Cv+7G$Ok`Bfupeu=EG}2uXF>y+$bXP@V(tf)cUp4Q!zQa< zl^C?B22{Cz2-_0?#M0-dLQODo5ntz+^W$}UCA`LPe9lqc zLmgjSo1&Q}&!mb~W=Tty(M8)tQTKP`u&wE;aB^pd%xQXz*y`CCN%Mi`yq86teh*tlrlZ^w``rV$=BW{E`exaq}xzGKQrL?-_TSRk-`d-N3&0&6Hsbx-ZnbQ=$kA1+nJ z*_aBz?Ko{n>R(}beeb0>9WY3-piO1J{-9NABo&)}@hr8|-yvHozLA6m`*>GJ}E?|2PNG!sDtPQu+BL z_K|Oi$?VskESnLnd)dxbA=%;N4{f3Of6IBLfRb%h8cb3D!bCOfI{rDgLA4F?<+r3y z@3u&0)#ToC<@U4xK4P^V7R*#`Dp_Oj<>2PE=+)JE!$FFPFxNgL)#}S^p3?fMxXbvm zLN1Pz`E`nS`AT@NlP+eJOCOt_XaKx_+dl)v$N2Z`1&WIsGD+3^MDGS_e|&jaet2d;Bk!T;neW< zR82+UXjE0SVOft?2n=QRvoFML<-57q5Qg`>cOi_=*@_2wLDKJVW1T>Z7m7cNI|L1K zDiUDiPfk{i&Lq_tlr1>YDRrnkGAz0i2b${Lx-MSTZg4^HWe1r`hjQ~zW9gAE zuL?RB!elVo=lW_Q2~5MDZb_0Ofu*Sdg=YHANi-11ot9B^yRDGf%{DO~txZg2K&Uq} z%ly`1&%;jFpIWIWzy-=OErJ`V+xFx!1nq|c+tw}KnttvS4417UXxYVQsz zZXx7Y)+Sa z;oQ1USky9bZ?Jhoik3`a@55(^&vyvko{_;ela!Zw)#GHUs+?wGW)B7X^3`*GQbv|M$23>W+2B=b75kPEl^e%(*~-UM~X?Kd3JsQ14(8Ca0P9AC`DpVs7jIfm_syB5LZdW3DYReiB01oOe@BVBBt5^RL3;Wuk#8gbhf*>)?7ntA zHi>Z5`O&ugVy9}34%eO02k*-H8+$f*U=@5WgBCq&&>%#hc12->kewx>l2fnR1mz*i zyU5&$vQ_T87^cMb3xgscY!Qr-P$tgBv(`An%TwG=M4d#fNRgG0jHfO3HA7Mj(bxP! zct!w|#)ZHos93imeMoWkN8vyEgTJJlw?{kmz61%ZuO2-7l{K|pUxStpyGC+f?RPxt zh6~^~{#20!qOcsBCM35jWK{3#8rrSLKK%5GNgtY;o7Vwf+%o{_H>h80kqTQo_@oFI zfnXC>@7p3i`@1i7nfja^(6bwGs&3ys#E&ch(z>D6mh++EfE>5;3p>Gt@GSeWQs#Kg zS|k5tAunE_ysX&!(Qch z><0YlW`_*b>w}*v8#o6w2fs5?KIdp!Vt)2cK-+?d5Se)*^~86!7U%CAQ6lR;&$Lx% zR4<-9VzN!3D|q8tq+cs)oG!U7=z1zvJ>gk8>z{A(MH0*mz2qm} znF^)X=3R3yxBQj_j@Cv}8r(NBCUXK1(0*{^Hkp*mFFc!;C+I4)Nx}C)pa;-(@+Wx6 z=<~v80O1EXLWc~(q{oIy{b?eKDSVC@EG=4*?kQ=_3p3?m*yJi=nhw&sm3Qknu~E>C z*;M9crpCAf^k4n+&O9UNKkNl}UQR7Jd2d5}TpBH`!L8Hc+IJY>)qK zeg+cLcBxS3k=<6e6w6B}pl@kL&a>XKMWO9Y9~s+wX>8&JnFre^(eEAiDJF~LWAfMz zhPy6z*-o_4mT^Z_qlb`o7FCyzR>_<9bPsisiF{9e3VEHTBh0QGRCLn)rNw-KYQoi8`W1?BqB)VUaXU4qEF!rD^*n=omn*UQ%H2{P{2SN;V?wBmUU z`!Bd&&&=c-x3p+99Io~cHeZ}Dt&B0xn7vns7e<`{1}ASzJr}Om@jt1i8wotntG6o# zg4|n4^GiAL)~(r%ppvIg+SNuWFGb9l*_xOU2KHovc^B{;@+bq+;uhzIA`tlDTn|?{)(BoKoYd z{VJ~VR8oE7%`OMr(q;z$uVXi64jg#kQKEY*ndqbRbE_w54A}Ov!Dv{|BAbf!Ne|z| zlv{}=t@LyQMuS?kLbpW8*v->rUL;5hTAVy`gBRG&O!9DVwVAvrLMh!$WFD&si`#mc z`^x6SX!rV53w=E!LJ`i^F`9E!<+6x&B|3x27NQYWy4I%q@#iQpagWg`Wwp(Iu2iWQ*wHY} zIj%vZ9y!khp)3_2FXdn7U8=C@6v;$@Ckx#w3)2N)Cfq17dG%*$f{wE9Ud6+S2w7D^ zOR*_}Mzz`G4hDjX0p^l+%g_KzJ2s@sa9Z;*Vq&`65FR(~C1_*Ne75G-ET4wbtgem^NZaot7V5IK1*cOdhJK6Mrbv``#X6dBC# zqs+|;f@_34j|Z6)q*W*54AGcB-ZGSHdH`l5ZMIF_-9HG^Hxvg6Dv3uE`~OIe+(hT8 zkTF7Z$zFMZ7V&mpL1;s9UUOei5-$X9-V`|31&F7c8oVt&I^NbGifoDpS<$h!$eYv& zun50ja$~I#X#SuqR6K~&wTLTZ^d<}(Sda0JE#`ZDf0vrM#btbDJ*YZ@9Ikrn zW%9$3{*0dt@&oEPno7=u5NK=iJ zXa?t~q)1@k)7{q&rfuZJd(Nnun%#}hs1_PUydK-B%w!&_1WXj~M-Yx=du4q&{whjH z1~eK}lr3}XI2nIZ z(9ARIP~i61uxM~!&x{DT7*)RUePn+2DYMzvv(UXot5_B|k6o9%xLI&bC{dKoH*IW9 zv#G1J>h;e4&O)7-Kmj&6Tv?<`&5yr-5O5gzH9Vh3#@a%}f~~HMk@X!m`74JG4Es3S zW7<%xkBqUD>av7Jt3T4mEP1?k9P|KTdV?;u|DLGz!oebaqUL3+VnJf`gThev`SizW z?}GwA*(;F`e*XTtmOyG8a_H@;*h9c9KWNwj%rN@jE5E|L76)>I zj~e+y01AD-cD$|cZgTzNVE)|N5=tu^LoOnzTVeGnkL}T0c0(n_B(`VEv7|zwvrR9Q z-xU5z|qd5!E#^@@B=`r{LEqcmhq*QIzl? z#}m@SBsUrv@zz=N5D1k(0cQjuI}6~X)3W_@{0c6$3`MFUU#%iXK2oML1ZHI4zb051J?lj z5fj)p4i06qj~{0C+7VXB8!jAcHEHs{I7aK)Jv&@&*j?7a_1vCKjCjbZ0@%)Y0|Nsl z>;4b$rx2Bgj4}}fRW|!idUoPd`%^^}&j2f=RXjNuXZlrFt~2ZUxn6q6!7{7bMNDeG zT)Y`Px#JCbEpRa^^rfi_#kx+Bjit!_9NCJGJ@TUP%l2tDoB9UY3$VV7lixrl6NZ~= zrhibWMta;NX3rRwsBHYhl|I#AH0%sRA+LACxs;-QYGzW3bU0_g?(b7PZK(SD3e%LD z7a)7h)At!{S6QY&0^5Efkx$h=(bffPV+=ZG!JdFp-e5Bi_6ZvrMM6MYZu541dx{S( z4imJL+joC`nl1f00nP|B68xU2+#Vec_;39U$Uy39&qKAb6h2sM^Z;gpv9y$jSpt0?A0zxQcrB#7-97;7&G8$RvI0lQ;NGu@@5o?04$A}AA54izJ7mCb{D%xRDD(z_b6fl-wD-I`xyg3sf$iVaj2!hy@8>-oU2Hv!>OJPZHC zkc#D=%~AC2iAu#{<;l*`QebEWtv^`KS4lm0|6@>-8Nne2Jaar&U1^f&Zrl&QQ33zl zn1NUu)LT-B4aq!bT%dvTp8aYMcasbzQCV1>D!g3;14uvO(6ERV0qt#h zEKiKJ0ga&#fg|t>OJdj0>}6psMbv|&1V~K=s#LzR-T9!aP_{<1_nKb{SEAfw)2cds zUb18nnA>!vZ)d%zl1;24-$_XDn&fDBtVx!I0SYcvq*{I*Kk=`msuj z?t+Ea54WbzD(NI$fN2_?`bGQe;1D|bgh4|>PcUxUh;(7^%$S?$I9G>aBH%jxS*G`P zGMb`?SV#nMKAG+VqkPq{!a&4hHytvVgXQdL!vD&}3y)DnX}L!;gS^Re?RS0UQa?f_ zgkMMq+IUQ!uezJ2Kp^i#(O7^h@(>kceVkajSIZIp#=dVuQ$Yu;)#H1B7EPv0yMAIpXI;~3Dm= zt;a2FWa|gmWnWs{j^0g&Rtpo~LV*oTWKX5mp{FJ>{=g$;tL|Z*Ai7SPxEdbY z#O`Ly4AO~P!Z+xO6P0p)d&dxeKg&BF(>=HaB(Y1v2vo&VKRyY*$8pK+`Z%d*F6gsP zg5cU1bKPkj8d!g?fg#3L3(ml;&r-O?&8xaV*HGCw!R>$TJvoU${@X2HTJ&aX^A9p1 zEiPfHBVi%gy+6{HGJWJdE$dptu!w)#QG+$@T_SMM60oJ=__Lz@=Vv25SP}kToj+$$ z%LD27kEu(W3>BCeF(=vcK{h$W5~mP zUy2EcD{i%5a7~*%gWKRjjuDAAFPrgOH)(u#C5CTN4sd##TaS2HOanXQ*tZ85<-Jjb?HHVJ!EJs1+Ce29%v56wDl{jrV@m0-N&#hV1 z%GjWqdj#4-mPI*m@9&-Cd@l~zq%|O{{tDb?cr0 zo*C&3)30J{fTa6%aAco2NL}HLE^J;n064pRKy5AwsqAsrDF=+Xm9hCgJ}eA)}zPm9(C8%q;u6tgbGhpbuj{U(K8CR{(#wDgWjS z5;`g}9-DW1)PH`xIi6u8&R zw?TtL^B?}J_zhIQp2nLRN03=rd-sozDL6_wMtpVTZYu}H?LB=tutqz%9sM2$6Y})6 zu=v~ws;8Hb%dk*XZ{#5YF&*=twyGoF4QtMyv@p#_Q2MD5UNZi->OgT`q+K%0l)4wU z1NW!pO!v?sGK4b!YE1>`#sou&@xKm5fHud=#Q&?W4&X@5>&>_SI+PjctYHEDS68W0 z0-fQG3+MFzZ9)bU71={wf`8TD#X$9c+N4$@Ng|U{nyX2PoBxm^FQXz|B54xzKZVu0 AumAu6 literal 0 HcmV?d00001 diff --git a/docs/dev-guide/images/someip-collection-diagram.jpg b/docs/dev-guide/images/someip-collection-diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31896cf729a2e57f69f4676c65d02cad41ffa2ec GIT binary patch literal 118770 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 Pos4qG|2jhH{UZDq031&m literal 0 HcmV?d00001 diff --git a/docs/dev-guide/images/someip-commands-diagram.jpg b/docs/dev-guide/images/someip-commands-diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fb704fa5fd765eb46713c82f1ebd5dfec508b4ab GIT binary patch literal 109953 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/dev-guide/images/uds-dtc-architecture-uml.png b/docs/dev-guide/images/uds-dtc-architecture-uml.png new file mode 100644 index 0000000000000000000000000000000000000000..57eb1e765288500cf4889f1eced75a39b22b4de8 GIT binary patch literal 20282 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 +> [!NOTE] +> This is a "gated" feature of AWS IoT FleetWise for which you will need to request access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) +> for more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +Network agnostic data collection and actuator commands (NADC) is a feature of AWS IoT FleetWise that +enables the collection of data from sensors and the remote command of actuators that are accessible +via any arbitrary network interface. + +The sensors or actuators are modelled as signals in the signal catalog in the same way as CAN and +OBD signals, but the decoder manifest for these signals instead passes an arbitrary 'decoder' string +to the edge to map the signal to a input or output at the network layer. The decoder string may for +example be an identifier such as the fully-qualified-name (FQN) of the signal. The decoder string +can however also convey other parameters, e.g. CSV data, to perform decoding of the signal. + +This guide details examples of NADC in the Reference Implementation for AWS IoT FleetWise (FWE), and +details the steps needed to add your own sensors or actuators using the NADC feature. + +## Examples of NADC in FWE + +### AAOS VHAL + +The [`AaosVhalSource`](../../src/AaosVhalSource.h) **does not** use the FQN as the decoder string, +but rather passes a CSV of parameters for obtaining each Android Automotive (AAOS) VHAL vehicle +property. + +The corresponding JSON files for use with the cloud APIs are: + +- [`custom-nodes-aaos-vhal.json`](../../tools/android-app/cloud/custom-nodes-aaos-vhal.json) +- [`network-interface-custom-aaos-vhal.json`](../../tools/android-app/cloud/network-interface-custom-aaos-vhal.json) +- [`custom-decoders-aaos-vhal.json`](../../tools/android-app/cloud/custom-decoders-aaos-vhal.json) + +See [the Android Automotive guide](../../tools/android-app/README.md#android-automotive-user-guide) +for step-by-step instructions on running this example. + +### CAN actuators + +The [`CanCommandDispatcher`](../../src/CanCommandDispatcher.h) implements actuators controlled via +CAN bus. The `CanCommandDispatcher` is registered with the +[`ActuatorCommandManager`](../../src/ActuatorCommandManager.h) which identifies actuator signals by +FQN. + +The corresponding JSON files for use with the cloud APIs are: + +- [`custom-nodes-can-actuators.json`](../../tools/cloud/custom-nodes-can-actuators.json) +- [`network-interface-custom-can-actuators.json`](../../tools/cloud/network-interface-custom-can-actuators.json) +- [`custom-decoders-can-actuators.json`](../../tools/cloud/custom-decoders-can-actuators.json) + +See [the CAN actuators guide](./can-actuators-dev-guide.md) for step-by-step instructions on running +this example. + +### Location + +The [`IWaveGpsSource`](../../src/IWaveGpsSource.h) and the +[`ExternalGpsSource`](../../src/ExternalGpsSource.h) both use the +[`NamedSignalDataSource`](../../src/NamedSignalDataSource.h) to ingest location data. The +`ExternalGpsSource` is exposed via +[`IoTFleetWiseEngine::setExternalGpsLocation`](../../src/IoTFleetWiseEngine.h) allowing ingestion of +position information from outside FWE, for example from Android. + +The corresponding JSON files for use with the cloud APIs are: + +- [`custom-nodes-location.json`](../../tools/cloud/custom-nodes-location.json) +- [`network-interface-custom-location.json`](../../tools/cloud/network-interface-custom-location.json) +- [`custom-decoders-location.json`](../../tools/cloud/custom-decoders-location.json) + +See [the iWave guide](../iwave-g26-tutorial/iwave-g26-tutorial.md) or +[the Android guide](../../tools/android-app/README.md) for a step-by-step instructions on running +these examples. + +### `MULTI_RISING_EDGE_TRIGGER` + +The [CustomFunctionMultiRisingEdgeTrigger](../../src/CustomFunctionMultiRisingEdgeTrigger.h) module +implements a [custom function](./custom-function-dev-guide.md) that produces signal data for direct +collection via the custom function +[`conditionEndCallback`](./custom-function-dev-guide.md#interface-conditionendcallback) interface. +The signal data is modelled as a signal with FQN `Vehicle.MultiRisingEdgeTrigger`. The +[`NamedSignalDataSource`](../../src/NamedSignalDataSource.h) is used to obtain the signal ID of this +signal. + +The corresponding JSON files for use with the cloud APIs are: + +- [`custom-nodes-multi-rising-edge-trigger.json`](../../tools/cloud/custom-nodes-multi-rising-edge-trigger.json) +- [`network-interface-custom-named-signal.json`](../../tools/cloud/network-interface-custom-named-signal.json) +- [`custom-decoders-multi-rising-edge-trigger.json`](../../tools/cloud/custom-decoders-multi-rising-edge-trigger.json) + +See +[the custom function developer guide](./custom-function-dev-guide.md#custom-function-multi_rising_edge_trigger) +for more information. + +### SOME/IP + +FWE includes example FIDL and FDEPL files for SOME/IP sensor and actuator signals: +[`ExampleSomeipInterface.fidl`](../../interfaces/someip/fidl/ExampleSomeipInterface.fidl), +[`ExampleSomeipInterface.fdepl`](../../interfaces/someip/fidl/ExampleSomeipInterface.fdepl). The +[CommonAPI](https://covesa.github.io/capicxx-core-tools/) proxy for reading the sensor values and +the stubs for performing the actuator commands are implemented in +[`ExampleSomeipInterfaceWrapper`](../../src/ExampleSomeipInterfaceWrapper.h). Data collection from +SOME/IP is performed by [`SomeipDataSource`](../../src/SomeipDataSource.h), which uses the +[`NamedSignalDataSource`](../../src/NamedSignalDataSource.h) to identify the signals by FQN and +ingest the signal values. Remote command actuation for SOME/IP is performed by +[`SomeipCommandDispatcher`](../../src/SomeipCommandDispatcher.h). The `SomeipCommandDispatcher` is +registered with the [`ActuatorCommandManager`](../../src/ActuatorCommandManager.h) which identifies +actuator signals by FQN. + +The corresponding JSON files for use with the cloud APIs are: + +- [`custom-nodes-someip.json`](../../tools/cloud/custom-nodes-someip.json) +- [`network-interface-custom-someip.json`](../../tools/cloud/network-interface-custom-someip.json) +- [`custom-decoders-someip.json`](../../tools/cloud/custom-decoders-someip.json) + +See [the SOME/IP demo documentation](./edge-agent-dev-guide-someip.md) for a step-by-step guide to +running this example. + +### UDS DTC + +The [`RemoteDiagnosticDataSource`](../../src/RemoteDiagnosticDataSource.h) collects DTC data and +provides it for ingestion via [`NamedSignalDataSource`](../../src/NamedSignalDataSource.h) as a +signal with FQN `Vehicle.ECU1.DTC_INFO`. + +The corresponding JSON files for use with the cloud APIs are: + +- [`custom-nodes-uds-dtc.json`](../../tools/cloud/custom-nodes-uds-dtc.json) +- [`network-interface-custom-uds-dtc.json`](../../tools/cloud/network-interface-custom-uds-dtc.json) +- [`custom-decoders-uds-dtc.json`](../../tools/cloud/custom-decoders-uds-dtc.json) See + [the UDS DTC Example developer guide](./edge-agent-uds-dtc-dev-guide.md) for more information. + +## Implementing your own sensors and actuators + +### Sensor Data Collection + +> In this section data will be ingested for signals based on their FQN, hence the standard +> [`NamedSignalDataSource`](../../src/NamedSignalDataSource.h) will be used directly. For more +> complex interfaces, for example when additional configuration parameters such as IP address are +> needed, a custom network interface type will need to be added. Refer to the examples above to see +> how this is achieved. + +First, adjust the static configuration file of FWE to add the `namedSignalInterface` interface. The +interface ID will need to match the ID used with the cloud APIs, here it is `NAMED_SIGNAL`. + +Open your config file (typically named `config-0.json`) and add the network interface to it. (You +can also do this during the provisioning step, by passing option `--enable-named-signal-interface` +to [`tools/configure-fwe.sh`](../../tools/configure-fwe.sh).) + +```json +"networkInterfaces": [ + ... + { + "interfaceId": "NAMED_SIGNAL", + "type": "namedSignalInterface" + } + ... +] +``` + +Next, you need to feed the signal values into the FWE signal buffers. To do that, you use an +existing class called `NamedSignalDataSource`. This class exposes a function which will help us +insert the data we receive from our sensor into FWE's signal buffers. + +You can create you own data source that has, for example, a worker thread that captures sensor data, +and injects the data using the `NamedSignalDataSource` into FWE's signal buffers. Of course, any +other type of data acquisition could be implemented here. This example simply uses an existing +`NamedSignalDataSource` that is already initialized as part of the `IoTFleetWiseEngine` bootstrap +code, generates the data, and injects it into the signal buffers. + +```cpp +// Added the below to the main.cpp of FWE +// Generates random Lat/Long and inject those every second to the Signal Buffers +std::random_device rd; +std::mt19937 longGen( rd() ); +std::mt19937 latGen( rd() ); +std::uniform_real_distribution<> longDist( -180.0, 180.0 ); +std::uniform_real_distribution<> latDist( -90.0, 90.0 ); +for ( int n = 0; n < 100; ++n ) +{ + std::vector> values; + auto longitude = longDist( longGen ); + auto latitude = latDist( latGen ); + std::cout << longitude << '\t' << latitude << '\n'; + values.emplace_back( + "Vehicle.CurrentLocation.Longitude", + Aws::IoTFleetWise::DecodedSignalValue( longitude, Aws::IoTFleetWise::SignalType::DOUBLE ) ); + values.emplace_back( + "Vehicle.CurrentLocation.Latitude", + Aws::IoTFleetWise::DecodedSignalValue( latitude, Aws::IoTFleetWise::SignalType::DOUBLE ) ); + // This is the API used to inject the data into the Signal Buffers + // Passing zero as the timestamp will use the current system time + engine.ingestMultipleSignalValuesByName( 0, values ); + sleep( 1 ); +} +``` + +FWE will recognize these two signals and buffer the values. You can go ahead and create a data +collection campaign that acquires those two signals. To do that, use the `create-campaign` cloud API +to collect the signals `Vehicle.CurrentLocation.Longitude` and `Vehicle.CurrentLocation.Latitude`. + +### Remote Commands + +First, adjust the static configuration file of FWE to add a new `acCommandInterface` interface. The +interface ID will need to match the ID used with the cloud APIs, here it is `AC_ACTUATORS`. + +Open your config file (typically named `config-0.json`) and add the network interface to it. + +```json +"networkInterfaces": [ + ... + { + "interfaceId": "AC_ACTUATORS", + "type": "acCommandInterface" + } + ... +] +``` + +After you have created above an actuator for the AC Controls and declared it in the signal catalog, +model manifest and decoder manifest. Now, write some code that will run when a command to change the +actuator state is sent remotely. + +First, create a command dispatcher that is called whenever the system receives a command: + +```cpp +// Make sure you implement the ICommandDispatcher +class AcCommandDispatcher : public ICommandDispatcher +{ +public: + /** + * @brief Initializer command dispatcher with its associated underlying vehicle network / service + * @return True if successful. False otherwise. + */ + bool init() override; + +// This method will need to implement the actual command execution provided +// the actuator name and the value + /** + * @brief set actuator value + * @param actuatorName Actuator name + * @param signalValue Signal value + * @param commandId Command ID + * @param issuedTimestampMs Timestamp of when the command was issued in the cloud in ms since + * epoch. + * @param executionTimeoutMs Relative execution timeout in ms since `issuedTimestampMs`. A value + * of zero means no timeout. + * @param notifyStatusCallback Callback to notify command status + */ + void setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) override; +}; +``` + +An example implementation can be : + +```cpp +bool +AcCommandDispatcher::init() +{ + return true; +} + +void +AcCommandDispatcher::setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) +{ + // Here invoke your actuation + FWE_LOG_INFO( "Actuator " + actuatorName + " executed successfully for command ID " + commandId ); + notifyStatusCallback( CommandStatus::SUCCEEDED, REASON_CODE_UNSPECIFIED, "Success" ); +} +``` + +Finally, register the command dispatcher with `ActuatorCommandManager` For that, edit the +`IoTFleetWiseEngine.cpp` file which is where all the modules are initialized. + +```cpp +// IoTFleetWiseEngine.cpp +// .... +static const std::string AC_COMMAND_INTERFACE_TYPE = "acCommandInterface"; +// .... +else if ( interfaceType == AC_COMMAND_INTERFACE_TYPE ) +{ +// Here we create and register our command dispatcher + mAcCommandDispatcher =std::make_shared(); +// All commands on actuators defined in this interface will be routed to the AC command dispatcher + if ( !mActuatorCommandManager->registerDispatcher( interfaceId, mAcCommandDispatcher ) ) + { + return false; + } +``` + +Now, compile the software (make sure you add your headers and C++ files to the CMake file), and go +back to the Cloud APIs to test a command on this actuator. diff --git a/docs/dev-guide/store-and-forward-dev-guide.md b/docs/dev-guide/store-and-forward-dev-guide.md new file mode 100644 index 00000000..d57cfc3b --- /dev/null +++ b/docs/dev-guide/store-and-forward-dev-guide.md @@ -0,0 +1,367 @@ +# Store and Forward Dev Guide + + +> [!NOTE] +> This is a "gated" feature of AWS IoT FleetWise for which you will need to request access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) +> for more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + +**Topics** + +- [Prerequisites](#prerequisites) +- [Demo](#deploy-edge-agent) +- [Store and Forward High Level Implementation](#store-and-forward-high-level-implementation) +- [IoTJobsDataRequestHandler High Level Implementation](#iotjobsdatarequesthandler-high-level-implementation) + +The first half of developer guide illustrates step-by-step instructions on how to run the AWS IoT +FleetWise Store and Forward demo. The second half focus on the architecture of Store and Forward. It +also covers the IoT Jobs data request handler module. + +## Prerequisites + +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Linux or MacOS machine. + +## Deploy Edge Agent + +Use the following CloudFormation template to deploy pre-built FWE binary to a new AWS EC2 instance. + +1. Click here to + [**Launch CloudFormation Template**](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateUrl=https%3A%2F%2Faws-iot-fleetwise.s3.us-west-2.amazonaws.com%2Flatest%2Fcfn-templates%2Ffwdemo.yml&stackName=fwdemo). +1. (Optional) You can increase the number of simulated vehicles by updating the `FleetSize` + parameter. You can also specify the region IoT Things are created in by updating the + `IoTCoreRegion` parameter. +1. Select the checkbox next to _'I acknowledge that AWS CloudFormation might create IAM resources + with custom names.'_ +1. Choose **Create stack**. +1. Wait until the status of the Stack is 'CREATE_COMPLETE', this will take approximately 10 minutes. + +FWE has been deployed to an AWS EC2 Graviton (ARM64) Instance along with credentials that allow it +to connect to AWS IoT Core. CAN data is also being generated on the EC2 instance to simulate +periodic hard-braking events and 'NetworkType' switching. + +## Setup Signal Catalog, Vehicle Model, Decoder Manifest, Campaigns, and View Stored and Forwarded Data. + +The instructions below will register your AWS account for AWS IoT FleetWise, create a demonstration +signal catalog, vehicle model and decoder manifest, then register the virtual vehicle created in the +previous section. + +1. Open the AWS CloudShell: [Launch CloudShell](https://console.aws.amazon.com/cloudshell/home) + +1. Copy and paste the following command to clone the latest FWE source code from GitHub. + + ```bash + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge + ``` + +1. Copy and paste the following commands to install the dependencies of the demo script. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && sudo -H ./install-deps.sh + ``` + + The above command installs the following PIP packages: `wrapt plotly pandas cantools pyarrow` + +1. Run the demo script to setup the signal catalog, vehicle model, decoder manifest, and campaigns. + + We will deploy three campaigns: + + 1 - campaign-store-only-no-upload.json + + - Stores engine torque data into an 'engine' data partition on the vehicle. + - Does not forward any data. If only this campaign is used then there will be no data to download + from the cloud, because the data is only present on the vehicle itself. + + 2 - campaign-upload-critical-during-hard-braking.json + + - Stores brake pressure data into a data partition named 'critical'. + - When brake pressure is above a threshold (and only while that state is maintained), data in the + 'critical' partition will be forwarded. + + 3 - campaign-upload-during-wifi.json + + - Stores network type data into a data partition named 'basic'. + - When the vehicle network type is 1 (aka wifi), data in the 'basic' partition will be forwarded. + + If you increased the fleet size when deploying the edge agent, provide the updated fleet size in + the `--fleet-size` argument. + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && python3 dbc-to-nodes.py hscan.dbc can-nodes.json \ + && python3 dbc-to-decoders.py hscan.dbc can-decoders.json \ + && ./demo.sh \ + --region us-east-1 \ + --vehicle-name fwdemo-snf \ + --node-file can-nodes.json \ + --decoder-file can-decoders.json \ + --network-interface-file network-interface-can.json \ + --campaign-file campaign-store-only-no-upload.json \ + --campaign-file campaign-upload-critical-during-hard-braking.json \ + --campaign-file campaign-upload-during-wifi.json + ``` + + The demo script will save a set of varables about the run to a demo.env file, which can be used + by other scripts such as request-forward.sh or cleanup.sh. + + When the script completes, you can view the data forwarded by the second and third campaigns in + Timestream, as well as the demo output file(s). A path to an HTML file is given. _On your local + machine_, use `scp` to download it, then open it in your web browser: + + ```bash + scp -i ubuntu@: . + ``` + + Alternatively, if your AWS account is enrolled with Amazon QuickSight or Amazon Managed Grafana, + you may use them to browse the data from Amazon Timestream directly. + + As you explore the forwarded data, you can see the brake data from campaign 2 as well as the + network type signal from campaign 3. + + In order to view the data which was stored on the vehicles, including the first campaign's engine + data which was not forwarded to the cloud, access the shell on a vehicle and run + `ls -l /var/aws-iot-fleetwise/fwdemo*/*` to observe the presence and size of the locally stored + data- or continue on to the next section to manually request data to be forwarded to the cloud. + +## Manually Request Campaign Data to be Forwarded using an IoT Job. + +This section outlines how to submit a manual data request to forward stored data for a campaign. + +In the previous section we created three campaigns, the first of which +(campaign-store-only-no-upload.json) stores data, but does not forward any data. Here, we will +request the data from that campaign to be forwarded. + +The request-forward script reads most parameters from the demo.env file created by the demo script. +Run the following to have the script request all data to be forwareded from the first campaign +created by the demo script, and pull the collected data again: + +```bash +cd ~/aws-iot-fleetwise-edge/tools/cloud \ +&& ./request-forward.sh \ + --region us-east-1 +``` + +While the script runs, it will show you the AWS IoT Jobs CLI commands being used, e.g. `create-job` +and `describe-job`. + +There are a number of options you can supply to the request-forward.sh script in order to more +explicilty control its behavior. One of which is the script --end-time argument, which lets you +specify a point in time, where data collected after this point in time will not be forwarded. This +end time will be passed through to the IoT Job document by the script. The script --end-time +argument must be an ISO 8601 UTC formatted time string. e.g. `--end-time 2024-05-25T01:21:23Z` + +When the request-forward.sh script completes, you can view the data forwarded by the first campaign +which was requested through IoT Jobs in Timestream, as well as the demo output file(s). A path to an +HTML file is given. _On your local machine_, use `scp` to download it, then open it in your web +browser: + +```bash +scp -i ubuntu@: . +``` + +Alternatively, if your AWS account is enrolled with Amazon QuickSight or Amazon Managed Grafana, you +may use them to browse the data from Amazon Timestream directly. + +As you explore the forwarded data, you can now see the engine data from campaign 1, in addition to +the data from the other campaigns. + +## (Optional) Building FWE from source + +Alternatively if you would like to build the FWE binary from source, below is the instruction: + +Install the dependencies for FWE: + +```bash +cd ~/aws-iot-fleetwise-edge && +sudo -H ./tools/install-deps-native.sh --with-store-and-forward-support --prefix /usr/local && +sudo ldconfig +``` + +Compile FWE with enabling store-and-forward feature. Below is example of building FWE natively: + +```bash +./tools/build-fwe-native.sh --with-store-and-forward-support +``` + +Refer to [edge-agent-dev-guide.md](./edge-agent-dev-guide.md) for detailed Edge building and +provision instruction. + +## Clean up + +1. Run the following to clean up resources created by the `provision.sh` and `demo.sh` scripts: + + ```bash + cd ~/aws-iot-fleetwise-edge/tools/cloud \ + && ./clean-up.sh + ``` + +1. Delete the CloudFormation stack created earlier, which by default is called `fwdemo-snf`: + https://us-east-1.console.aws.amazon.com/cloudformation/home + +## Store and Forward High Level Implementation + +The goal of the Store and Forward feature is to persist signal data early, and then send that data +to the cloud when customer-configured conditions are met. ![](./images/snf-high-level-diagram.png) + +So the new classes introduced by this feature are StreamManager and StreamForwarder. StreamManager +is responsible for taking signals provided by CollectionInspectionEngine (CIE) and appending them to +a persistent stream. Separately, StreamForwarder is reading from the stream, sending data to the +cloud via MQTT. + +### Stream Management + +Before signal data can be written to disk, the streams themselves need to be set up. To accomplish +this, StreamManager reacts to changes in CollectionSchemes (campaigns) using a +`StreamManager::onChangeCollectionSchemeList` listener. If new campaigns are detected, streams are +created (and tracked within StreamManager). Deleted campaigns are removed from disk (and are +untracked by StreamManager). + +How do we know a campaign is store and forward? We check for the presence of store and forward +configuration, `newCampaignConfig->getStoreAndForwardConfiguration()`, within a CollectionScheme. + +Streams are created per campaign and per partition, and are stored on disk within the FWE +persistency directory. A campaign may have multiple partitions. Partitions are locations on disk +(e.g. /path/to/my/data) that hold signal data (signals can only belong to one partition at a time). +So as a basic example: say the customer has two campaigns, each with two partitions. This will +result in four streams being created. ![](./images/snf-stream-management-diagram.png) + +#### Expiring Old Stream Data + +S&F has a weak notion of TTL for stream data. It only promises that data will be removed at some +point after a given instant in time. This is currently accomplished by StreamManager expiring old +data on campaign changes. The rationale is that this will happen 1) on startup, 2) and otherwise +infrequently. + +### Storing Signal Data + +Every time Collection Inspection Engine (CIE) processes signals, it will have StreamManager write +them to disk (for S&F-enabled campaigns). StreamManager accomplishes this via the Store Library. +![](./images/snf-storing-data-diagram.png) + +StreamManager will: + +1. Split the list of signals by partition (remember that signals only belong to one partition at a + time) +2. Chunk each partition of signals +3. Serialize each chunk (EdgeToCloud payload) +4. (optional) Compress each serialized chunk +5. Finally store the blob in persistent stream + +The complexity happens at write time because we need data to be compressed on disk. And with +compression we need all the steps before it. Otherwise, it'd mean we'd need to decompress at read +time. + +### Forwarding Signal Data + +In its own thread, StreamForwarder will loop over persistent streams, taking each data entry and +sending it as-is to the cloud via MQTT. Remember that forwarding is guarded by a "forward condition" +(which is managed by CIE); StreamForwarder will only read from streams where the forward condition +is active. ![](./images/snf-forwarding-data-diagram.png) + +An important detail here is that persistent streams are FIFO queues, essentially. For each +campaign/partition, StreamForwarder has a stream "iterator", which keeps track of the "sequence +number" (aka, id of the next data to read from disk) (stream iterators themselves are persisted on +disk). If StreamForwarder successfully sent a point of data over MQTT, it will "checkpoint" the +iterator, meaning that it moves on to the next data point. So if there's an MQTT failure, for +example, the current iterator position will NOT be checkpointed---meaning, the next attempt by +StreamForwarder will read the same data as the last attempt. + +## IoTJobsDataRequestHandler High Level Implementation + +The goal of the IoTJobsDataRequestHandler is to handle manual data upload request through an IoT Job +that is created by the customer. To create a manual data upload request, the +IoTJobsDataRequestHandler expects the IoT Job Document to be formatted the following way: + +``` +{ + "version": "1.0", + "parameters": { + "campaignArn": ${aws:iot:parameter:campaignArn}, + "endTime": ${aws:iot:parameter:endTime} // Optional + } +} +``` + +To interact with IoT Jobs that target FWE, the IoTJobsDataRequestHandler class subscribes and +publishes to the reserved IoT Jobs MQTT topics through the existing IConnectivityModule class. When +publishing and receiving messages, each topic has its own unique request and response payload +([examples](https://docs.aws.amazon.com/iot/latest/developerguide/jobs-mqtt-api.html)) which is +sanity checked to ensure we only process valid IoT Jobs. + +### General IotJobsDataRequestHandler Workflow + +When IoTJobsDataRequestHandler is initiated, it sends a +[GetPendingExecutions](https://docs.aws.amazon.com/iot/latest/developerguide/jobs-mqtt-api.html#:~:text=GetPendingJobExecutions) +request to check and see if any jobs on FWE were in the QUEUED or IN_PROGRESS state when FWE last +shutdown. This returns the list of all jobs that are not in a terminal state, for a specified thing. + +GetPendingExecutions is not the only way we get notified of new jobs. The AWS IoT Jobs service will +publish a message to to the reserved topic `$aws/things/" + clientId + "/jobs/notify` when a job is +added to or removed from the list of pending job executions for a thing or the first job execution +in the list changes. + +After receiving the IoT Job request, we publish to +[DescribeJobExecution](https://docs.aws.amazon.com/iot/latest/developerguide/jobs-mqtt-api.html#mqtt-describejobexecution:~:text=DescribeJobExecution) +to get the job document that accompanies the request's Job ID. When FWE receives the job document, +IoTJobsDataRequestHandler will check to see that the job document contains a valid campaign arn that +is running on FWE, as well as if the IoT Job specifies an optional endTime parameter. Once we have +validated the job document, we will start uploading data from the specified store and forward +campaign until we have upload all of the data in the campaign, or we hit the optional endTime. + +Once we hit the end of the data stream or the optional specified end time, we update the IoT Job +execution status in the cloud to SUCCEEDED by sending an +[UpdateJobExecution](https://docs.aws.amazon.com/iot/latest/developerguide/jobs-mqtt-api.html#mqtt-describejobexecution:~:text=a%20JobExecution%20object.-,UpdateJobExecution,-Updates%20the%20status) +message to the reserved Iot Jobs topic. Similarly, we will update the job execution status in the +cloud to either be REJECTED or IN_PROGRESS, depending on if the Job document is valid. + +At any point during the IoT Job handling lifecycle, if an IoT Job is cancelled from the cloud, we +will stop forwarding data and update the internal status of the Job to cancelled. + +### Connecting IotJobsDataRequestHandler to StreamForwarder + +When an IoT Job specifies a valid campaignArn that is running on the FWE device, we signal to +StreamForwarder to start uploading data from the specified campaignArn. In StreamForwarder, we +created a notion of Source which represents what is the source that is calling StreamForwarder to +start uploading data for a campaign: `IOT_JOB` or `CONDITION`. This is important to keep the IoT Job +status in the cloud in sync with the internal IoT Job status on the device. `CONDITION` means that +the collection inspection engine evaluated a campaigns forward condition to be true -- this is how +FWE behaved before the Store and Forward project. `IOT_JOB` was added to discern that FWE is +forwarding data for a campaign that was initiated by a manual data pull request from a customer. +This is necessary since for a campaign that was signaled to forward data by a manual data pull +request, we do not want to stop uploading data until we reach the end of the campaign's data stream +or hit the optional IoT Job endTime, even if this campaign's forwarding condition evaluates to +false. + +### Utilizing the IoT Jobs EndTime Parameter + +In the IoT Job, the customer can specify an optional EndTime in iso8601 format. If set, this endTime +is passed to StreamForwarder and represent when we should stop forwarding data for a campaign that +was specified by the IoT Job. We compare the endTime to the triggerTime that represents when the +data was collected. If the triggerTime is past the endTime, then we will stop forwarding data and +signal the IoTJobsDataRequestHandler to mark the respective IoT Job as complete in the cloud. + +If an endTime is not specified, then we will internally set the endTime to 0 which we use to +represent that StreamForwarder should continue forwarding data for a campaign specified by an IoT +Job until we hit the end of the campaign's data stream or until the job is cancelled by the +customer. + +### Handling Multiple Jobs targeting the same Store and Forward campaign + +We do support handling multiple IoT Jobs targeting the same S&F campaign. In +IoTJobsDataRequestHandler, we map `mJobToCampaignId` . When StreamForwarder signals to +IoTJobsDataRequestHandler that a job upload is completed for a specific campaign, we will iterate +through `mJobToCampaignId` to get all IoT Job IDs that target the campaign that has completed +forwarding data, and then we will update all of these Jobs to SUCCEEDED in the cloud by publishing +to UpdateJobExecution. diff --git a/docs/dev-guide/vision-system-data/vision-system-data-demo.ipynb b/docs/dev-guide/vision-system-data/vision-system-data-demo.ipynb index aa8caab7..deb833fb 100644 --- a/docs/dev-guide/vision-system-data/vision-system-data-demo.ipynb +++ b/docs/dev-guide/vision-system-data/vision-system-data-demo.ipynb @@ -4,9 +4,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Amazon IoT Fleetwise for vision system data collection and transformation\n", + "# Amazon IoT FleetWise for vision system data collection and transformation\n", "\n", - "**Note:** AWS IoT FleetWise is currently available in `us-east-1` and `eu-central-1`.\n", + "**Note:** AWS IoT FleetWise is currently available in\n", + "[these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions.\n", "\n", "AWS IoT FleetWise is a managed service that you can use to collect vehicle data and organize it in\n", "the cloud. With AWS IoT FleetWise, you can standardize all of your vehicle data models, independent\n", @@ -135,8 +136,13 @@ "## Prerequisites\n", "\n", "- Access to an AWS account with administrator privileges.\n", - "- Signed in to the AWS IoT FleetWise console in the `us-east-1` Region using the account with\n", - " administrator privileges.\n", + "- Logged in to the AWS Console in the `us-east-1` region using the account with administrator\n", + " privileges.\n", + " - Note: if you would like to use a different region you will need to change `us-east-1` to your\n", + " desired region in each place that it is mentioned below.\n", + " - Note: AWS IoT FleetWise is currently available in\n", + " [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions.\n", + "- A local Windows, Mac, or Linux machine.\n", "- (Optional) IDL files and their corresponding ROS 2 bag (rosbag) files are available\n", "\n", "## Scope\n", @@ -324,7 +330,11 @@ "source": [ "cd ~/aws-iot-fleetwise-edge\n", "mkdir -p build/iotfleetwise\n", - "curl -L https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-ros2-arm64.tar.gz -o aws-iot-fleetwise-edge-ros2-arm64.tar.gz\n", + "if [ ! -z \"${FWE_BINARY_DOWNLOAD_COMMAND}\" ]; then\n", + " ${FWE_BINARY_DOWNLOAD_COMMAND}\n", + "else\n", + " curl -L https://github.com/aws/aws-iot-fleetwise-edge/releases/latest/download/aws-iot-fleetwise-edge-ros2-arm64.tar.gz -o aws-iot-fleetwise-edge-ros2-arm64.tar.gz\n", + "fi\n", "tar -zxf aws-iot-fleetwise-edge-ros2-arm64.tar.gz -C build/iotfleetwise/\n", "sudo -H ./tools/install-deps-native.sh --with-ros2-support --runtime-only > $LOG_DIR/deps.log 2>&1\n", "source /opt/ros/galactic/setup.bash" @@ -593,6 +603,7 @@ "cd ~/aws-iot-fleetwise-edge\n", "\n", "./tools/provision.sh \\\n", + " --region $REGION \\\n", " --vehicle-name $VEHICLE_NAME \\\n", " --creds-role-alias ${CRED_ROLE_NAME} \\\n", " --certificate-pem-outfile $BUILD_CONFIG_DIR/certificate.pem \\\n", @@ -600,8 +611,7 @@ " --endpoint-url-outfile $BUILD_CONFIG_DIR/endpoint.txt \\\n", " --vehicle-name-outfile $BUILD_CONFIG_DIR/vehicle-name.txt \\\n", " --creds-role-alias-outfile $BUILD_CONFIG_DIR/creds-role-alias.txt \\\n", - " --creds-endpoint-url-outfile $BUILD_CONFIG_DIR/creds-endpoint.txt \\\n", - " --region $REGION\n" + " --creds-endpoint-url-outfile $BUILD_CONFIG_DIR/creds-endpoint.txt\n" ] }, { diff --git a/docs/iwave-g26-tutorial/IWAVE-GPS-CAN.dbc b/docs/iwave-g26-tutorial/IWAVE-GPS-CAN.dbc deleted file mode 100644 index 8ca3bedc..00000000 --- a/docs/iwave-g26-tutorial/IWAVE-GPS-CAN.dbc +++ /dev/null @@ -1,47 +0,0 @@ -VERSION "" - -NS_ : - NS_DESC_ - CM_ - BA_DEF_ - BA_ - VAL_ - CAT_DEF_ - CAT_ - FILTER - BA_DEF_DEF_ - EV_DATA_ - ENVVAR_DATA_ - SGTYPE_ - SGTYPE_VAL_ - BA_DEF_SGTYPE_ - BA_SGTYPE_ - SIG_TYPE_REF_ - VAL_TABLE_ - SIG_GROUP_ - SIG_VALTYPE_ - SIGTYPE_VALTYPE_ - BO_TX_BU_ - BA_DEF_REL_ - BA_REL_ - BA_DEF_DEF_REL_ - BU_SG_REL_ - BU_EV_REL_ - BU_BO_REL_ - -BS_: - -BU_: - -BO_ 1 GPS: 8 Vector__XXX - SG_ Longitude : 0|32@1+ (0.0000001,-200) [-200|200] "Degrees" Vector__XXX - SG_ Latitude : 32|32@1+ (0.0000001,-200) [-200|200] "Degrees" Vector__XXX - - -BA_DEF_ SG_ "SignalType" STRING ; -BA_DEF_ SG_ "SignalLongName" STRING ; -BA_DEF_ BO_ "GenMsgCycleTime" INT 0 10000; -BA_DEF_DEF_ "SignalType" ""; -BA_DEF_DEF_ "SignalLongName" ""; -BA_DEF_DEF_ "GenMsgCycleTime" 0; -BA_ "GenMsgCycleTime" BO_ 1 1000; diff --git a/docs/iwave-g26-tutorial/iwave-g26-tutorial.md b/docs/iwave-g26-tutorial/iwave-g26-tutorial.md index 83fa15d0..227b0d3c 100644 --- a/docs/iwave-g26-tutorial/iwave-g26-tutorial.md +++ b/docs/iwave-g26-tutorial/iwave-g26-tutorial.md @@ -1,5 +1,13 @@ # iWave G26 TCU Tutorial + +> [!NOTE] +> This guide makes use of "gated" features of AWS IoT FleetWise for which you will need to request +> access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for +> more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + **Topics** - [Introduction](#introduction) @@ -11,7 +19,7 @@ - [Step 4: Provision AWS IoT credentials](#step-4-provision-aws-iot-credentials) - [Step 5: Deploy Edge Agent](#step-5-deploy-edge-agent) - [Step 6: Connect the iWave G26 TCU to the vehicle](#step-6-connect-the-tcu-to-the-vehicle) -- [Step 7: Collect OBD Data](#step-7-collect-obd-data) +- [Step 7: Collect OBD Data](#step-7-collect-gps-and-obd-data) - [Step 8: Clean up](#step-8-clean-up) **Copyright (C) Amazon Web Services, Inc. and/or its affiliates. All rights reserved.** @@ -81,21 +89,28 @@ to take action based on your use of AWS IoT FleetWise._** Follow the steps in this tutorial to set up and configure the iWave G26 TCU device hardware to work with your Edge Agent compiled from the FWE source code. You can then connect the device to a vehicle -so that it collects vehicle J1979 OBD-II data and transfers it to the AWS IoT FleetWise service. To -additionally also collected GPS data more steps described [here](./iwave-gps-setup.md) are required. -They require the steps below to be executed first. +so that it collects vehicle J1979 OBD-II data and GPS data, and transfers it to the AWS IoT +FleetWise service. **Estimated Time**: 60 minutes ## Prerequisites - A [G26 TCU from iWave Systems](https://www.iwavesystems.com/product/telematics-control-unit/) - device -- Access to an AWS account with administrator permissions -- To be signed in to the AWS Management Console with an account in your chosen Region - - **Note:** AWS IoT FleetWise is currently available in `us-east-1` and `eu-central-1`. -- A SIM card -- A local Linux or MacOS machine + device. +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Linux or MacOS machine. +- A SIM card. ## Step 1: Set up iWave Systems G26 TCU @@ -404,6 +419,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ && mkdir -p config \ && cd config \ && ../tools/provision.sh \ + --region us-east-1 \ --vehicle-name fwdemo-g26 \ --certificate-pem-outfile certificate.pem \ --private-key-outfile private-key.key \ @@ -415,7 +431,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ --log-color Yes \ --vehicle-name `cat vehicle-name.txt` \ --endpoint-url `cat endpoint.txt` \ - --can-bus0 can0 \ + --can-bus0 can0 && cd .. \ && zip -r aws-iot-fleetwise-deploy.zip . ``` @@ -478,7 +494,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ 2. After the OBD is connected, start the engine. -## Step 7: Collect OBD data +## Step 7: Collect GPS and OBD data 1. Run the following _on the development machine_ to install the dependencies of the demo script: @@ -495,11 +511,15 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ ```bash ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-g26 \ --node-file obd-nodes.json \ + --node-file custom-nodes-location.json \ --decoder-file obd-decoders.json \ + --decoder-file custom-decoders-location.json \ --network-interface-file network-interface-obd.json \ - --campaign-file campaign-obd-heartbeat.json + --network-interface-file network-interface-custom-location.json \ + --campaign-file campaign-obd-and-location-heartbeat.json ``` The demo script: @@ -507,18 +527,20 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ 1. Registers your AWS account with AWS IoT FleetWise, if not already registered. 1. Creates an Amazon Timestream database and table. 1. Creates IAM role and policy required for the service to write data to Amazon Timestream. - 1. Creates a signal catalog based on `obd-nodes.json`. - 1. Creates a model manifest that references the signal catalog with all of the OBD signals. + 1. Creates a signal catalog based on `obd-nodes.json` and `custom-nodes-location.json`. + 1. Creates a model manifest that references the signal catalog with all of the OBD and location + signals. 1. Activates the model manifest. - 1. Creates a decoder manifest linked to the model manifest using `obd-decoders.json` for decoding - signals from the network interfaces defined in `network-interfaces-obd.json`. + 1. Creates a decoder manifest linked to the model manifest using `obd-decoders.json` and + `custom-decoders-location.json` for decoding signals from the network interfaces defined in + `network-interface-obd.json` and `network-interface-custom-location.json`. 1. Updates the decoder manifest to set the status as `ACTIVE`. 1. Creates a vehicle with a name equal to `fwdemo-g26`, the same as the name passed to `provision.sh`. 1. Creates a fleet. 1. Associates the vehicle with the fleet. 1. Creates a campaign from `campaign-obd-heartbeat.json` that contains a time-based collection - scheme that collects OBD data and targets the campaign at the fleet. + scheme that collects OBD and location data and targets the campaign at the fleet. 1. Approves the campaign. 1. Waits until the campaign status is `HEALTHY`, which means the campaign has been deployed to the fleet. @@ -547,6 +569,25 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ extended period (like a week), unplug the iWave device from the J1962 DCL port to avoid depleting the battery. +### Troubleshooting + +- If you have problems collecting location data: + + - Firstly check that you can see NMEA formatted ASCII data when you run `cat < /dev/ttyUSB1`. + - If the GPS NMEA output it working but no gps fix is available you should move to an area with an + open sight to the sky. As long as no GPS fix is available you will see every 10 seconds + (assuming you configured `systemWideLogLevel` to `Trace`): + + ``` + [TRACE] [IWaveGpsSource.cpp:80] [pollData()]: [In the last 10000 millisecond found 10 lines with $GPGGA and extracted 0 valid coordinates from it] + ``` + + As soon as data is available you should see this: + + ``` + [TRACE] [IWaveGpsSource.cpp:80] [pollData()]: [In the last 10000 millisecond found 11 lines with $GPGGA and extracted 11 valid coordinates from it] + ``` + ## Step 8: Clean up 1. Run the following _on the development machine_ to clean up resources created by the diff --git a/docs/iwave-g26-tutorial/iwave-gps-setup.md b/docs/iwave-g26-tutorial/iwave-gps-setup.md deleted file mode 100644 index dc74e5f9..00000000 --- a/docs/iwave-g26-tutorial/iwave-gps-setup.md +++ /dev/null @@ -1,49 +0,0 @@ -# IWave GPS setup - -Setup the iWave device as described in the [iwave-g26-tutorial](iwave-g26-tutorial.md), then check -that you can see NMEA formatted ASCII data when you run `cat < /dev/ttyUSB1`. - -The required decoder manifest changes are explained in -[custom-data-source](../custom-data-source.md). If you are using the console to create the decoder -manifest, you can create an interface with Network Interface Id `IWAVE-GPS-CAN` and then import the -dbc file [IWAVE-GPS-CAN.dbc](IWAVE-GPS-CAN.dbc) - -# Configuration - -In the `staticConfig` section of the config.json the following section can be used to specify the -parameters without recompiling FWE: - -``` -"iWaveGpsExample": { - "nmeaFilePath": "/dev/ttyUSB1", - "canChannel" : "IWAVE-GPS-CAN", - "canFrameId" : "1", - "longitudeStartBit" : "0", - "latitudeStartBit" : "32" -} -``` - -When configure AWS IoT FleetWise use the `cmake` with the flag `-DFWE_FEATURE_IWAVE_GPS=On` and then -compile using `make` as usual. - -# Debug - -Like CAN if data is sent to cloud you should see this: - -``` -[INFO ] [IoTFleetWiseEngine.cpp:914] [doWork()]: [FWE data ready to send with eventID 1644139266 from arn:aws:iotfleetwise:us-east-1:xxxxxxxxxxxx:campaign/IWaveGpsCampaign Signals:70 [2514:13.393196,2514:13.393196,2514:13.393196,2514:13.393196,2514:13.393196,2514:13.393196, ...] first signal timestamp: 1666881551998 raw CAN frames:0 DTCs:0] -``` - -If the GPS NMEA output it working but gps fix is available you should move to an area with a open -sight to the sky. As long as no GPS fix is available you will see every 10 seconds (assuming you -configured `systemWideLogLevel` to `Trace`): - -``` -[TRACE] [IWaveGpsSource.cpp:112] [pollData()]: [In the last 10000 millisecond found 10 lines with $GPGGA and extracted 0 valid coordinates from it] -``` - -As soon as data is available you should see this: - -``` -[TRACE] [IWaveGpsSource.cpp:112] [pollData()]: [In the last 10000 millisecond found 11 lines with $GPGGA and extracted 11 valid coordinates from it] -``` diff --git a/docs/metrics.md b/docs/metrics.md index 1b7943d7..59ad31f6 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -1,6 +1,6 @@ # Application level metrics -The Reference Implementation for AWS IoT Fleetwise ("FWE") includes a +The Reference Implementation for AWS IoT FleetWise ("FWE") includes a [TraceModule](../src/TraceModule.cpp). The TraceModule provides a set of metrics that are used as an entry point to efficiently diagnose issues, saving you time since you no longer need to review the entire log of all FWE instances running. @@ -23,7 +23,7 @@ entire log of all FWE instances running. cloud, either no actual data was collected ( such as a time-based data collection campaign with no bus activity), or the data has been ingested to the cloud but there was an error processing it. To debug this, - [enable cloud logs in AWS IoT Fleetwise settings](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/logging-cw.html). + [enable cloud logs in AWS IoT FleetWise settings](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/logging-cw.html). - **`QueueConsumerToInspectionDataFrames`** monitors the current count of data frames in queue to the signal history buffer. If this value is close to the value defined in the static config `decodedSignalsBufferSize`, increase the static config, decrease `inspectionThreadIdleTimeMs`, @@ -33,7 +33,7 @@ entire log of all FWE instances running. - **`ConFail`** monitors the number of MQTT connection failures. This can have multiple root causes. If this is not zero please check the logs and search for `Connection failed with error` - **`FWE_STARTUP`** and **`FWE_SHUTDOWN`** provide the amount of time it takes to start and stop the - AWS IoT Edge Fleetwise process. If any value is more than 5 seconds, review the logs and make sure + AWS IoT Edge FleetWise process. If any value is more than 5 seconds, review the logs and make sure all required resources such as internet and buses are available before starting the process. - **`ObdE0`** to **`ObdE3`** monitors errors related to the OBD session. If you see non-zero values, make sure you're connected to a compatible OBD vehicle which is powered on. Otherwise turn off the diff --git a/docs/rpi-tutorial/raspberry-pi-tutorial.md b/docs/rpi-tutorial/raspberry-pi-tutorial.md index 25e7ebdb..37b7a725 100644 --- a/docs/rpi-tutorial/raspberry-pi-tutorial.md +++ b/docs/rpi-tutorial/raspberry-pi-tutorial.md @@ -6,7 +6,7 @@ - [Prerequisites](#prerequisites) - [Step 1: Set up the Raspberry Pi](#step-1-setup-the-raspberry-pi) - [Step 2: Launch your development machine](#step-2-launch-your-development-machine) -- [Step 3: Compile Edge Agent](#step-3-compile-egde-agent) +- [Step 3: Compile Edge Agent](#step-3-compile-edge-agent) - [Step 4: Provision AWS IoT credentials](#step-4-provision-aws-iot-credentials) - [Step 5: Deploy Edge Agent](#step-5-deploy-edge-agent) - [Step 6: Deploy a campaign to the Raspberry Pi](#step-6-deploy-a-campaign-to-the-raspberry-pi) @@ -84,10 +84,14 @@ to take action based on your use of AWS IoT FleetWise._** [Coolwell Waveshare 2-Channel Isolated CAN Bus Expansion Hat](https://www.amazon.de/-/en/Waveshare-CAN-HAT-SN65HVD230-Protection/dp/B087PWNMM8/?th=1), or the [2-Channel Isolated CAN Bus Expansion HAT](https://rarecomponents.com/store/2-ch-can-hat-waveshare). -- Access to an AWS account with administrator permissions -- To be signed in to the AWS Management Console with an account in your chosen Region - - **Note:** AWS IoT FleetWise is currently available in `us-east-1` and `eu-central-1`. -- A local Windows, Mac, or Linux machine +- Access to an AWS Account with administrator privileges. +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. +- A local Linux or MacOS machine. ## Step 1: Setup the Raspberry Pi @@ -180,12 +184,10 @@ launch an AWS EC2 Graviton (arm64) instance. For more information about Amazon E Next, compile FWE for the ARM 64-bit architecture of the processor present in the Raspberry Pi. -1. On your development machine, clone the latest FWE source code from GitHub by running the - following: +1. Run the following _on the development machine_ to clone the latest FWE source code from GitHub. ```bash - git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge \ - && cd ~/aws-iot-fleetwise-edge + git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge ``` 1. Review, modify and supplement [the FWE source code](../../src/) to ensure it meets your use case @@ -194,7 +196,8 @@ Next, compile FWE for the ARM 64-bit architecture of the processor present in th 1. Install the FWE dependencies: ```bash - sudo -H ./tools/install-deps-native.sh + cd ~/aws-iot-fleetwise-edge \ + && sudo -H ./tools/install-deps-native.sh ``` The command above installs the following Ubuntu packages for compiling FWE for ARM 64-bit: @@ -225,6 +228,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ && mkdir -p config \ && cd config \ && ../tools/provision.sh \ + --region us-east-1 \ --vehicle-name fwdemo-rpi \ --certificate-pem-outfile certificate.pem \ --private-key-outfile private-key.key \ @@ -316,6 +320,7 @@ mkdir -p ~/aws-iot-fleetwise-deploy \ ```bash ./demo.sh \ + --region us-east-1 \ --vehicle-name fwdemo-rpi \ --node-file obd-nodes.json \ --decoder-file obd-decoders.json \ diff --git a/interfaces/protobuf/schemas/cloudToCustomer/last_known_state_message.proto b/interfaces/protobuf/schemas/cloudToCustomer/last_known_state_message.proto new file mode 100644 index 00000000..8c18ca5a --- /dev/null +++ b/interfaces/protobuf/schemas/cloudToCustomer/last_known_state_message.proto @@ -0,0 +1,76 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +option java_package = "com.amazonaws.iot.autobahn.schemas.lastknownstate"; +package Aws.IoTFleetWise.Schemas.CustomerMessage; + +message LastKnownState { + + /* + * The absolute timestamp in milliseconds since Unix Epoch of when the event was triggered in vehicle. + */ + uint64 time_ms = 1; + + repeated Signal signals = 3; + + repeated ExtraDimension extra_dimensions = 4; +} + +message Signal { + + /* + * The Fully Qualified Name of the signal is the path to the signal plus the signal's name. + * For example, Vehicle.Chassis.SteeringWheel.HandsOff.HandsOffSteeringState + * The fully qualified name can have up to 150 characters. Valid characters: a-z, A-Z, 0-9, : (colon), and _ (underscore). + */ + string name = 1; + + /* + * The FWE reported signal value can be one of the following data types. + */ + oneof SignalValue { + double double_value = 2; + + bool boolean_value = 3; + + sint32 int8_value = 4; + + uint32 uint8_value = 5; + + sint32 int16_value = 6; + + uint32 uint16_value = 7; + + sint32 int32_value = 8; + + uint32 uint32_value = 9; + + sint64 int64_value = 10; + + uint64 uint64_value = 11; + + float float_value = 12; + /* + * An UTF-8 encoded or 7-bit ASCII string + */ + string string_value = 13; + } +} + +message ExtraDimension { + /* + * The Fully Qualified Name of the attribute is the path to the attribute plus the attribute's name. + * For example, Vehicle.Model.Color + * The fully qualified name can have up to 150 characters. Valid characters: a-z, A-Z, 0-9, : (colon), and _ (underscore). + */ + string name = 1; + + oneof ExtraDimensionValue { + /* + * An UTF-8 encoded or 7-bit ASCII string + */ + string string_value = 2; + } +} diff --git a/interfaces/protobuf/schemas/cloudToEdge/collection_schemes.proto b/interfaces/protobuf/schemas/cloudToEdge/collection_schemes.proto index 4b4fe469..9def1a0a 100644 --- a/interfaces/protobuf/schemas/cloudToEdge/collection_schemes.proto +++ b/interfaces/protobuf/schemas/cloudToEdge/collection_schemes.proto @@ -118,8 +118,69 @@ message CollectionScheme { * Additional data for S3 upload. */ S3UploadMetadata s3_upload_metadata = 16; + + /* + * Configuration of store and forward campaign. + */ + StoreAndForwardConfiguration store_and_forward_configuration = 17; + // Signals to fetch configurations. + repeated FetchInformation signal_fetch_information = 18; + + /* + * Amazon Resource Name of the campaign this collectionScheme is part of + */ + string campaign_arn = 19; +} +/* + * Condition based fetch configuration + */ +message ConditionBasedFetchConfig { + enum ConditionTriggerMode { + + /* + * Condition will evaluate to true regardless of previous state + */ + TRIGGER_ALWAYS = 0; + + /* + * Condition will evaluate to true only when it previously evaluated to false + */ + TRIGGER_ONLY_ON_RISING_EDGE = 1; + } + // Condition to be evaluated for fetch to trigger + CommonTypesMsg.ConditionNode condition_tree = 1; + // Trigger mode for condition + ConditionTriggerMode condition_trigger_mode = 2; +} + +/* + * Time based fetch configuration + */ +message TimeBasedFetchConfig { + // Max number of time action for fetch should be executed + uint64 max_execution_count = 1; + // Frequency for action execution + uint64 execution_frequency_ms = 2; + // Time after which max_execution_count should be reset to original value + uint64 reset_max_execution_count_interval_ms = 3; +} + +message FetchInformation { + // Signal id that will be fetched + uint32 signal_id = 1; + + // Fetch configuration for this signal + oneof fetchConfig { + TimeBasedFetchConfig time_based = 2; + ConditionBasedFetchConfig condition_based = 3; + }; + // IOT Event expression language version for this config + uint32 condition_language_version = 4; + // Expression for the function(s) to call for fetch + repeated CommonTypesMsg.ConditionNode actions = 5; } + message S3UploadMetadata { /* @@ -241,6 +302,12 @@ message SignalInformation { * its associated fixed_window_period_ms. Default is false. */ bool condition_only_signal = 5; + + /* + * The Id of the partition where this signal should be stored. This Id will be used to index into the partition + * configuration array. + */ + uint32 data_partition_id = 7; } /* @@ -271,3 +338,72 @@ message RawCanFrame { */ uint32 minimum_sample_period_ms = 4; } + +/* + * Store and Forward storage options, required. It defines where and how the data will be stored on the edge. It is only used when + * spoolerMode=TO_DISK. May be non-null only when spoolerMode=TO_DISK. + */ +message StorageOptions { + + /* + * The total amount of space allocated to this campaign including all overhead. Limited to a maximum of 1024TB. + */ + uint64 maximum_size_in_bytes = 1; + + /* + * Specifies where the data should be stored withing the device. Implementation is defined by the user who + * integrates FWE with their filesystem library. + */ + string storage_location = 2; + + /* + * The minimum amount of time to keep data on disk after it is collected. When this TTL expires, data may be + * deleted, but it is not guaranteed to be deleted immediately after expiry. Can hold TTL more than 132 years. + */ + uint32 minimum_time_to_live_in_seconds = 3; + +} + +/* + * Store and Forward upload options defines when the stored data may be uploaded. It is only used when + * spoolerMode=TO_DISK. May be non-null only when spoolerMode=TO_DISK. + */ +message UploadOptions { + + /* + * Root condition node for the Abstract Syntax Tree. + */ + CommonTypesMsg.ConditionNode condition_tree = 1; +} + +/* + * Store and Forward configuration for each partition. + */ +message PartitionConfiguration { + + /* + * Optional Store and Forward storage options. If not specified, data in this partition will be uploaded in + * realtime. + */ + StorageOptions storage_options = 1; + + /* + * Store and Forward upload options defines when the stored data may be uploaded. It is only used when + * spoolingMode=TO_DISK. May be non-null only when spoolingMode=TO_DISK. + */ + UploadOptions upload_options = 2; +} + +/* + * When spoolingMode is set to TO_DISK and PartitionConfiguration is not Null, then store and forward subsystem will be + * used to persistently store collected data. If non-null then spoolingMode must be TO_DISK. + * + * Optional. Required if any signalsToCollect specify a non-default partition. + */ +message StoreAndForwardConfiguration { + + /* + * Array of Store and Forward configurations for each data partition. + */ + repeated PartitionConfiguration partition_configuration = 1; +} diff --git a/interfaces/protobuf/schemas/cloudToEdge/command_request.proto b/interfaces/protobuf/schemas/cloudToEdge/command_request.proto new file mode 100644 index 00000000..fae22e1f --- /dev/null +++ b/interfaces/protobuf/schemas/cloudToEdge/command_request.proto @@ -0,0 +1,149 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +option java_package = "com.amazonaws.iot.autobahn.schemas"; +package Aws.IoTFleetWise.Schemas.Commands; + +/* + * A command is used to set actuator's state + */ +message CommandRequest { + + /* + * CommandId is the identifier for the command requested to be executed on edge. + */ + string command_id = 16; + + /* + * Command timeout in milliseconds + */ + uint64 timeout_ms = 17; + + oneof Command { + ActuatorCommand actuator_command = 18; + LastKnownStateCommand last_known_state_command = 19; + } + + /* + * Timestamp when the command was issued (in milliseconds since epoch) + */ + uint64 issued_timestamp_ms = 20; +} + +/* + * Command that sends a value to an actuator + * + * Actuators are identified by a signal ID and the decoder manifest will be used to find out how the + * value should be encoded and how to send it to the actuator. + */ +message ActuatorCommand { + /* + * Unique integer identifier of the signal generated by Cloud Designer + */ + uint32 signal_id = 1; + + /* + * Synchronization ID of the required decoder manifest for this command + */ + string decoder_manifest_sync_id = 2; + + /* + * Data types of physical signal values. + */ + oneof SignalValue { + + double double_value = 3; + + bool boolean_value = 4; + + sint32 int8_value = 5; + + uint32 uint8_value = 6; + + sint32 int16_value = 7; + + uint32 uint16_value = 8; + + sint32 int32_value = 9; + + uint32 uint32_value = 10; + + sint64 int64_value = 11; + + uint64 uint64_value = 12; + + float float_value = 13; + + /* + * An UTF-8 encoded or 7-bit ASCII string + */ + string string_value = 14; + } +} + +/* + * Command to operate on state templates + * + * This can change a state template's status or request data associated to a state template. + * Any state template referenced in this command should have been already deployed. + */ +message LastKnownStateCommand { + repeated StateTemplateInformation state_template_information = 1; + + message StateTemplateInformation { + /* + * This ID uniquely identifies a state template. Please note that it is not necessarily the + * same ID as provided by the Cloud APIs since it can contain additional prefix or suffix, or + * even be random. It just needs to match a state template that has been received by FWE. + */ + string state_template_sync_id = 1; + + oneof Operation { + ActivateOperation activate_operation = 2; + DeactivateOperation deactivate_operation = 3; + FetchSnapshotOperation fetch_snapshot_operation = 4; + } + } + + /* + * Activates the state template + * + * The associated state template will start collecting data following the rules defined in the + * state template definition until is it explicitly deactivated or when the auto-deactivation + * time passes. + * + * The first message will contain all signals (similar to a FetchSnapshotOperation). Then after this initial + * message, data will be collected following the update strategy defined in the state template. + */ + message ActivateOperation { + /* + * Automatically deactivates the state template after this time + * + * This time is relative to when FWE receives the command. + * + * If set to zero or omitted, the state template won't be auto-deactivated. + */ + uint32 deactivate_after_seconds = 1; + } + + /* + * Deactivates the state template + * + * Stops the collection of data associated to a state template. It will immediately be deactivated + * regardless whether auto-deactivation was enabled during activation. + */ + message DeactivateOperation {} + + /* + * Request a single snapshot + * + * This requests a single snapshot containing all data associated to a state template. + * The data will be sent just once, regardless of whether the state template is currently + * active or not. + * After the data is sent, if the state template is active it will continue collecting data + * normally. + */ + message FetchSnapshotOperation {} +} diff --git a/interfaces/protobuf/schemas/cloudToEdge/common_types.proto b/interfaces/protobuf/schemas/cloudToEdge/common_types.proto index 7bab1927..119b01bd 100644 --- a/interfaces/protobuf/schemas/cloudToEdge/common_types.proto +++ b/interfaces/protobuf/schemas/cloudToEdge/common_types.proto @@ -74,6 +74,11 @@ message ConditionNode { * The expression currently only supports primitive types so for complex signals this has to be used and not node_signal_id */ PrimitiveTypeInComplexSignal node_primitive_type_in_signal = 6; + + /* + * A node containing a string constant which can be used as a child node to an operator node. + */ + string node_string_value = 7; } /* @@ -134,7 +139,7 @@ message ConditionNode { * output based on specific logic */ message NodeFunction{ - /* +/* * This field was never supported, so it should not be used any more. */ reserved 2; @@ -150,6 +155,17 @@ message ConditionNode { * run an aggregation function over the samples and evaluate to a double. */ WindowFunction window_function = 1; + + /** + * A custom function node will take function_name as first params followed by input params + * required for the function to be executed defined by first params on the Edge. + */ + CustomFunction custom_function = 3; + + /** + * A function to check if passed signal value is null or not + */ + IsNullFunction is_null_function = 4; } /* @@ -190,5 +206,23 @@ message ConditionNode { PREV_LAST_WINDOW_AVG = 5; } } + + /** + * A custom function node will take function_name as first params followed by input params + * required for the function to be executed defined by first params on the Edge. + */ + message CustomFunction { + + string function_name = 1; + + repeated ConditionNode params = 2; + } + + /** + * An isNull function node will take single params and check for null condition of that params. + */ + message IsNullFunction { + ConditionNode expression = 1; + } } } diff --git a/interfaces/protobuf/schemas/cloudToEdge/decoder_manifest.proto b/interfaces/protobuf/schemas/cloudToEdge/decoder_manifest.proto index d4723150..52571b6c 100644 --- a/interfaces/protobuf/schemas/cloudToEdge/decoder_manifest.proto +++ b/interfaces/protobuf/schemas/cloudToEdge/decoder_manifest.proto @@ -33,6 +33,11 @@ message DecoderManifest { * List of complex signals, which refer to the complex types */ repeated ComplexSignal complex_signals = 5; + + /* + * List of custom decoding signals + */ + repeated CustomDecodingSignal custom_decoding_signals = 6; } message CANSignal { @@ -187,6 +192,7 @@ enum PrimitiveType { INT64 = 9; FLOAT32 = 10; FLOAT64 = 11; + STRING = 12; } /* @@ -280,3 +286,29 @@ message ComplexSignal { */ uint32 root_type_id = 4; } + +message CustomDecodingSignal { + /* + * Unique FleetWise internal identifier of the signal across all types ( i.e. CANSignal, OBDPIDSignal, CustomDecodingSignal) + * Value zero is reserved for internal usage. + * The most significant bit is reserved for internal usage. + */ + uint32 signal_id = 1; + + /* + * Interface ID for the network interface this signal is found on. The network interface details are provided as + * a part of the edge static configuration file. + */ + string interface_id = 2; + + /* + * Custom proprietary decoding identifier. + */ + string custom_decoding_id = 3; + + /* + * A CustomDecodingSignal has always a primitive type, so here the primitive type can be defined. + * If not present this value defaults to FLOAT64. + */ + PrimitiveType primitive_type = 4; +} diff --git a/interfaces/protobuf/schemas/cloudToEdge/state_templates.proto b/interfaces/protobuf/schemas/cloudToEdge/state_templates.proto new file mode 100644 index 00000000..41c410b5 --- /dev/null +++ b/interfaces/protobuf/schemas/cloudToEdge/state_templates.proto @@ -0,0 +1,90 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +option java_package = "com.amazonaws.iot.autobahn.schemas"; +package Aws.IoTFleetWise.Schemas.LastKnownState; + +/* + * Top level message sent by Cloud with signals to be collected to measure last known state for provided signals + * + * When a new message arrives containing a list of state templates, all current state templates in FWE + * will be replaced with the new ones. Therefore, this message should always contain a complete list + * of all state templates that exist in the Cloud, even if some of the templates are already present + * in FWE. + */ +message StateTemplates { + /* + * Synchronization ID of the required decoder manifest for this collection scheme + * + * The signal IDs present in this message are mapped to the actual signals based on this decoder + * manifest, which must be received by Edge before the collection scheme. + */ + string decoder_manifest_sync_id = 2; + + /* + * Old field for state_template_information that is not supported anymore + */ + reserved 4; + + /* + * List of state templates that should be added. + * + * Note: populating this field will override any values in state_template_information + * + * Each state template contains the signals that should be collected for it. + */ + repeated StateTemplateInformation state_templates_to_add = 5; + + /* + * List of state template synchronization IDs that must be removed from Edge. + * Note: populating this field will override any values in state_template_information + */ + repeated string state_template_sync_ids_to_remove = 6; + + /* + * The version denotes the latest set of changes requested by FW Cloud. + * All the messages with the same version are considered different parts of the same requested config. + */ + uint64 version = 7; +} + +message StateTemplateInformation { + /* + * Synchronization ID for the state template + * This ID is expected to uniquely identify the update strategy and the set of signals that are associated with a state template. + */ + string state_template_sync_id = 1; + + reserved 2; + reserved "decoder_manifest_sync_id"; + + /* + * Update strategy for state template + */ + oneof UpdateStrategy { + OnChangeUpdateStrategy on_change_update_strategy = 3; + PeriodicUpdateStrategy periodic_update_strategy = 4; + }; + + /* + * List of signals that should be collected. + */ + repeated uint32 signal_ids = 5; +} + +/* + * Signals with this strategy will be sent in a specific interval + */ +message PeriodicUpdateStrategy { + /* + * This can't be zero or omitted + */ + uint64 period_ms = 1; +} + +/* + * Signals with this strategy will be sent only when its value changes + */ +message OnChangeUpdateStrategy {} diff --git a/interfaces/protobuf/schemas/edgeConfiguration/staticConfiguration.json b/interfaces/protobuf/schemas/edgeConfiguration/staticConfiguration.json index 070ed979..ea0bd6ca 100644 --- a/interfaces/protobuf/schemas/edgeConfiguration/staticConfiguration.json +++ b/interfaces/protobuf/schemas/edgeConfiguration/staticConfiguration.json @@ -140,6 +140,266 @@ } }, "required": ["ros2Interface", "interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "someipToCanBridgeInterface": { + "type": "object", + "additionalProperties": false, + "properties": { + "someipServiceId": { + "type": "string", + "description": "The Service ID that provides the CAN over SOME/IP service." + }, + "someipInstanceId": { + "type": "string", + "description": "The Instance ID that provides the CAN over SOME/IP service." + }, + "someipEventId": { + "type": "string", + "description": "The Event ID of the CAN over SOME/IP data." + }, + "someipEventGroupId": { + "type": "string", + "description": "The Event Group ID that FWE should subscribe to for the CAN over SOME/IP data." + }, + "someipApplicationName": { + "type": "string", + "description": "Name of the SOME/IP application" + } + }, + "required": [ + "someipServiceId", + "someipInstanceId", + "someipEventId", + "someipEventGroupId", + "someipApplicationName" + ] + }, + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["someipToCanBridgeInterface"] + } + }, + "required": ["someipToCanBridgeInterface", "interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "someipCollectionInterface": { + "type": "object", + "additionalProperties": false, + "properties": { + "someipApplicationName": { + "type": "string", + "description": "Name of the SOME/IP application" + }, + "cyclicUpdatePeriodMs": { + "type": "integer", + "description": "Cyclic update period in milliseconds that FWE will periodically push the last available signal values. Set to zero to only collect values on change." + } + }, + "required": ["someipApplicationName", "cyclicUpdatePeriodMs"] + }, + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["someipCollectionInterface"] + } + }, + "required": ["someipCollectionInterface", "interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "someipCommandInterface": { + "type": "object", + "additionalProperties": false, + "properties": { + "someipApplicationName": { + "type": "string", + "description": "Name of the SOME/IP application" + } + }, + "required": ["someipApplicationName"] + }, + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["someipCommandInterface"] + } + }, + "required": ["someipCommandInterface", "interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "iWaveGpsInterface": { + "type": "object", + "additionalProperties": false, + "properties": { + "latitudeSignalName": { + "type": "string", + "description": "Name of latitude signal" + }, + "longitudeSignalName": { + "type": "string", + "description": "Name of longitude signal" + }, + "nmeaFilePath": { + "type": "string", + "description": "Path to NMEA device" + }, + "pollIntervalMs": { + "type": "string", + "description": "Poll interval in milliseconds" + } + }, + "required": [ + "latitudeSignalName", + "longitudeSignalName", + "nmeaFilePath", + "pollIntervalMs" + ] + }, + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["iWaveGpsInterface"] + } + }, + "required": ["iWaveGpsInterface", "interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "externalGpsInterface": { + "type": "object", + "additionalProperties": false, + "properties": { + "latitudeSignalName": { + "type": "string", + "description": "Name of latitude signal" + }, + "longitudeSignalName": { + "type": "string", + "description": "Name of longitude signal" + } + }, + "required": ["latitudeSignalName", "longitudeSignalName"] + }, + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["externalGpsInterface"] + } + }, + "required": ["externalGpsInterface", "interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["aaosVhalInterface"] + } + }, + "required": ["interfaceId", "type"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "interfaceId": { + "type": "string", + "description": "Every network interface is associated with a unique ID that must match the interface ID sent by the cloud in the decoder manifest" + }, + "type": { + "type": "string", + "enum": ["exampleUDSInterface"] + }, + "exampleUDSInterface": { + "type": "object", + "additionalProperties": false, + "properties": { + "configs": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "targetAddress": { + "type": "string", + "description": "ECU address" + }, + "name": { + "type": "string", + "description": "Name of of the target" + }, + "can": { + "type": "object", + "additionalProperties": false, + "properties": { + "interfaceName": { + "type": "string", + "description": "Can interface name" + }, + "functionalAddress": { + "type": "string", + "description": "Functional address for UDS request" + }, + "physicalRequestID": { + "type": "string", + "description": "Physical request ID for UDS request" + }, + "physicalResponseID": { + "type": "string", + "description": "Physical response ID for UDS response" + } + }, + "required": [ + "interfaceName", + "functionalAddress", + "physicalRequestID", + "physicalResponseID" + ] + } + }, + "required": ["targetAddress", "name", "can"] + } + } + }, + "required": ["configs"] + } + }, + "required": ["interfaceId", "type", "exampleUDSInterface"] } ] } @@ -183,6 +443,10 @@ "canDecoderThreadIdleTimeMs": { "type": "integer", "description": "Sleep time for CAN decoder thread if no new data is available (in milliseconds)" + }, + "lastKnownStateThreadIdleTimeMs": { + "type": "integer", + "description": "Sleep time for last known state inspection engine thread (in milliseconds)" } }, "required": [ @@ -234,6 +498,18 @@ "maximumAwsSdkHeapMemoryBytes": { "type": "integer", "description": "Set the maximum AWS SDK heap memory bytes. Default to 10000000" + }, + "readyToPublishCommandResponsesBufferSize": { + "type": "integer", + "description": "Max number of messages in the buffer used for storing ready to publish command responses" + }, + "maxConcurrentCommandRequests": { + "type": "integer", + "description": "Max number of Command Requests to process concurrently" + }, + "minFetchTriggerIntervalMs": { + "type": "integer", + "description": "Minimum interval between two fetch requests in milliseconds" } }, "required": ["readyToPublishDataBufferSize", "systemWideLogLevel"] @@ -246,6 +522,10 @@ "type": "integer", "description": "Maximum messages that can be published to the cloud as one payload" }, + "maxPublishLastKnownStateMessageCount": { + "type": "integer", + "description": "Maximum Last Known State messages that can be published to the cloud as one payload" + }, "collectionSchemeManagementCheckinIntervalMs": { "type": "integer", "description": "Time interval between collectionScheme checkins( in milliseconds )" @@ -266,21 +546,21 @@ "type": "string", "description": "The ID that uniquely identifies this device in the AWS Region" }, - "collectionSchemeListTopic": { + "iotFleetWiseTopicPrefix": { "type": "string", - "description": "Control Plane publishes to this Collection Scheme, vehicle subscribes" + "description": "The prefix for AWS IoT FleetWise topics. All topics from other services such as commands, jobs, device shadow will be unaffected by this option. If omitted, it defaults to $aws/iotfleetwise/" }, - "decoderManifestTopic": { + "commandsTopicPrefix": { "type": "string", - "description": "Control Plane publishes to this Decoder Manifest CollectionScheme, vehicle subscribes" + "description": "The prefix for AWS IoT Commands topics. If omitted, it defaults to $aws/commands/" }, - "canDataTopic": { + "deviceShadowTopicPrefix": { "type": "string", - "description": "Topic for sending collected data to cloud" + "description": "The prefix for AWS IoT Device Shadow topics. If omitted, it defaults to $aws/things/" }, - "checkinTopic": { + "jobsTopicPrefix": { "type": "string", - "description": "Topic for sending checkins to cloud" + "description": "The prefix for AWS IoT Jobs topics. If omitted, it defaults to $aws/things/" }, "certificateFilename": { "type": "string", @@ -324,12 +604,6 @@ "description": "Choose the connection module. Default to iotCore" } }, - "required": [ - "collectionSchemeListTopic", - "decoderManifestTopic", - "canDataTopic", - "checkinTopic" - ], "anyOf": [ { "properties": { diff --git a/interfaces/protobuf/schemas/edgeToCloud/command_response.proto b/interfaces/protobuf/schemas/edgeToCloud/command_response.proto new file mode 100644 index 00000000..82450353 --- /dev/null +++ b/interfaces/protobuf/schemas/edgeToCloud/command_response.proto @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +option java_package = "com.amazonaws.iot.autobahn.schemas"; +package Aws.IoTFleetWise.Schemas.Commands; + +message CommandResponse { + + Status status = 1; + + /** + * CommandId is the identifier passed in the command request and passed back here in the command + * response in order to uniquely identify each command run. + */ + string command_id = 2; + + /** + * Integer reason code. The 32-bit range is allocated as follows: + * + * 0x00000000 : Unspecified reason + * 0x00000001 - 0x0000FFFF : AWS IoT FleetWise reason codes + * 0x00010000 - 0x0001FFFF : OEM reason codes + * 0x00020000 - 0xFFFFFFFF : Reserved + */ + uint32 reason_code = 3; + + /** + * String reason description + */ + string reason_description = 4; +} + +enum Status { + COMMAND_STATUS_UNSPECIFIED = 0; + COMMAND_STATUS_SUCCEEDED = 1; + COMMAND_STATUS_EXECUTION_TIMEOUT = 2; + COMMAND_STATUS_EXECUTION_FAILED = 4; + COMMAND_STATUS_IN_PROGRESS = 10; +} diff --git a/interfaces/protobuf/schemas/edgeToCloud/last_known_state_data.proto b/interfaces/protobuf/schemas/edgeToCloud/last_known_state_data.proto new file mode 100644 index 00000000..04f4683c --- /dev/null +++ b/interfaces/protobuf/schemas/edgeToCloud/last_known_state_data.proto @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +option java_package = "com.amazonaws.iot.autobahn.schemas"; +package Aws.IoTFleetWise.Schemas.LastKnownState; + +/* + * Top level message sent to the Cloud with collected signals + */ +message LastKnownStateData { + /* + * The absolute timestamp in milliseconds since Unix Epoch of when the event was triggered. + */ + uint64 collection_event_time_ms_epoch = 2; + + /* + * List of state templates signal data that are captured. + * + * Each state template contains the signals that are collected for it. + */ + repeated CapturedStateTemplateSignals captured_state_template_signals = 6; +} + +message CapturedStateTemplateSignals { + /* + * Synchronization ID for the state template + * Any business logic or validation logic should not be built on basis of the structure of this ID + */ + string state_template_sync_id = 1; + + /* + * List of captured signals + * + * This list doesn't necessarily include all signals that were requested as data could be split in + * multiple messages. + * + */ + repeated CapturedSignal captured_signals = 2; +} + +message CapturedSignal { + uint32 signal_id = 1; + + /* + * Data types of physical signal values. + */ + oneof SignalValue { + + double double_value = 2; + + bool boolean_value = 3; + + sint32 int8_value = 4; + + uint32 uint8_value = 5; + + sint32 int16_value = 6; + + uint32 uint16_value = 7; + + sint32 int32_value = 8; + + uint32 uint32_value = 9; + + sint64 int64_value = 10; + + uint64 uint64_value = 11; + + float float_value = 12; + + /* + * An UTF-8 encoded or 7-bit ASCII string + */ + string string_value = 13; + } + + reserved 14; + reserved "update_strategy"; +} + +enum UpdateStrategy { + UPDATE_STRATEGY_UNSPECIFIED = 0; + UPDATE_STRATEGY_ON_CHANGE = 1; + UPDATE_STRATEGY_PERIODIC = 2; +} diff --git a/interfaces/protobuf/schemas/edgeToCloud/vehicle_data.proto b/interfaces/protobuf/schemas/edgeToCloud/vehicle_data.proto index 4facc90e..1deba846 100644 --- a/interfaces/protobuf/schemas/edgeToCloud/vehicle_data.proto +++ b/interfaces/protobuf/schemas/edgeToCloud/vehicle_data.proto @@ -106,6 +106,11 @@ message CapturedSignal { * a PID formula and directly cast to a double. */ double double_value = 3; + + /* + * An UTF-8 encoded or 7-bit ASCII string value of a decoded physical value. + */ + string string_value = 4; } } diff --git a/interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fdepl b/interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fdepl new file mode 100644 index 00000000..700e1612 --- /dev/null +++ b/interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fdepl @@ -0,0 +1,173 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import "platform:/plugin/org.genivi.commonapi.someip/deployment/CommonAPI-4-SOMEIP_deployment_spec.fdepl" +import "DeviceShadowOverSomeipInterface.fidl" + +define org.genivi.commonapi.someip.deployment for interface commonapi.DeviceShadowOverSomeipInterface { + SomeIpServiceID = 0x1235 + + method getShadow { + SomeIpMethodID = 0x753A + SomeIpReliable = false + + in { + shadowName { + SomeIpStringEncoding = utf8 + } + } + + out { + shadowDocument { + SomeIpStringEncoding = utf8 + } + } + } + + method updateShadow { + SomeIpMethodID = 0x753B + SomeIpReliable = false + + in { + shadowName { + SomeIpStringEncoding = utf8 + } + + updateDocument { + SomeIpStringEncoding = utf8 + } + } + + out { + shadowDocument { + SomeIpStringEncoding = utf8 + } + } + } + + method deleteShadow { + SomeIpMethodID = 0x753C + SomeIpReliable = false + + in { + shadowName { + SomeIpStringEncoding = utf8 + } + } + } + + broadcast shadowChanged { + SomeIpEventID = 0x753D + SomeIpEventGroups = { 0x753D } + + out { + shadowName { + SomeIpStringEncoding = utf8 + } + + shadowDocument { + SomeIpStringEncoding = utf8 + } + } + } +} + +define org.genivi.commonapi.someip.deployment for provider as Service { + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface" + SomeIpInstanceID = 0x5679 + } + + // Add several instances for DeviceShadowOverSomeipInterface. Each of these instances can be initialized + // after the routing manager and they can be associated to separate FWE processes so that the + // data seen by each FWE process is isolated. + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface0" + SomeIpInstanceID = 0x5700 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface1" + SomeIpInstanceID = 0x5701 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface2" + SomeIpInstanceID = 0x5702 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface3" + SomeIpInstanceID = 0x5703 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface4" + SomeIpInstanceID = 0x5704 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface5" + SomeIpInstanceID = 0x5705 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface6" + SomeIpInstanceID = 0x5706 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface7" + SomeIpInstanceID = 0x5707 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface8" + SomeIpInstanceID = 0x5708 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface9" + SomeIpInstanceID = 0x5709 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface10" + SomeIpInstanceID = 0x570a + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface11" + SomeIpInstanceID = 0x570b + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface12" + SomeIpInstanceID = 0x570c + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface13" + SomeIpInstanceID = 0x570d + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface14" + SomeIpInstanceID = 0x570e + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface15" + SomeIpInstanceID = 0x570f + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface16" + SomeIpInstanceID = 0x5710 + } + + instance commonapi.DeviceShadowOverSomeipInterface { + InstanceId = "commonapi.DeviceShadowOverSomeipInterface17" + SomeIpInstanceID = 0x5711 + } +} diff --git a/interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fidl b/interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fidl new file mode 100644 index 00000000..b35d6210 --- /dev/null +++ b/interfaces/someip/fidl/DeviceShadowOverSomeipInterface.fidl @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package commonapi + +interface DeviceShadowOverSomeipInterface { + version { major 1 minor 0 } + + method getShadow { + in { + String shadowName // empty ("") for classic shadow; non-empty for named shadow + } + + out { + String errorMessage // only valid in case of error + String shadowDocument // only valid in case of no error + } + + error ErrorCode // please check it to determine which output (errorMessage, shadowDocument) is valid + } + + method updateShadow { + in { + String shadowName // empty ("") for classic shadow; non-empty for named shadow + String updateDocument + } + + out { + String errorMessage // only valid in case of error + String shadowDocument // only valid in case of no error + } + + error ErrorCode // please check it to determine which output (errorMessage, shadowDocument) is valid + } + + method deleteShadow { + in { + String shadowName // empty ("") for classic shadow; non-empty for named shadow + } + + out { + String errorMessage // only valid in case of error + } + + error ErrorCode // please check it to determine which output (errorMessage) is valid + } + + broadcast shadowChanged { + out { + String shadowName // empty ("") for classic shadow; non-empty for named shadow + String shadowDocument + } + } + + enumeration ErrorCode { + NO_ERROR + INVALID_REQUEST + SHADOW_SERVICE_UNREACHABLE + REJECTED + UNKNOWN + } +} diff --git a/interfaces/someip/fidl/ExampleSomeipInterface.fdepl b/interfaces/someip/fidl/ExampleSomeipInterface.fdepl new file mode 100644 index 00000000..ec9caa56 --- /dev/null +++ b/interfaces/someip/fidl/ExampleSomeipInterface.fdepl @@ -0,0 +1,209 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import "platform:/plugin/org.genivi.commonapi.someip/deployment/CommonAPI-4-SOMEIP_deployment_spec.fdepl" +import "ExampleSomeipInterface.fidl" + +define org.genivi.commonapi.someip.deployment for interface commonapi.ExampleSomeipInterface { + SomeIpServiceID = 0x1234 + + attribute x { + SomeIpGetterID = 0x0BB8 + SomeIpSetterID = 0x0BB9 + SomeIpNotifierID = 0x80f2 + SomeIpNotifierEventGroups = { 0x80f2 } + + SomeIpAttributeReliable = false + } + + attribute a1 { + SomeIpGetterID = 0x0BBA + SomeIpSetterID = 0x0BBB + SomeIpNotifierID = 0x80f3 + SomeIpNotifierEventGroups = { 0x80f3 } + + SomeIpAttributeReliable = false + } + + method setInt32 { + SomeIpMethodID = 0x7530 + } + + method getInt32 { + SomeIpMethodID = 0x7531 + } + + method setInt64 { + SomeIpMethodID = 0x7532 + } + + method getInt64 { + SomeIpMethodID = 0x7533 + } + + method setBoolean { + SomeIpMethodID = 0x7534 + } + + method getBoolean { + SomeIpMethodID = 0x7535 + } + + method setFloat { + SomeIpMethodID = 0x7536 + } + + method getFloat { + SomeIpMethodID = 0x7537 + } + + method setDouble { + SomeIpMethodID = 0x7538 + } + + method getDouble { + SomeIpMethodID = 0x7539 + } + + method setString { + SomeIpMethodID = 0x753A + } + + method getString { + SomeIpMethodID = 0x753B + } + + method setInt32LongRunning { + SomeIpMethodID = 0x753C + } + + method getInt32LongRunning { + SomeIpMethodID = 0x753D + } + + broadcast notifyLRCStatus { + SomeIpEventID = 0x753E + SomeIpEventGroups = { 0x753E } + + out { + } + } +} + +define org.genivi.commonapi.someip.deployment for typeCollection commonapi.CommonTypes { + struct a1Struct { + } + + struct a2Struct { + } + +} + +define org.genivi.commonapi.someip.deployment for provider as Service { + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface" + SomeIpInstanceID = 0x5678 + } + + // Add an instance to serve as the routing manager when we want to test multiple SOME/IP + // applications running at the same time. + // In such situation, the routing manager should be initialized first and kept alive until the + // last application is disconnected. + instance commonapi.ExampleSomeipInterface { + InstanceId = "RoutingManager" + SomeIpInstanceID = 0x5500 + } + + // Add several instances for ExampleSomeipInterface. Each of these instances can be initialized + // after the routing manager and they can be associated to separate FWE processes so that the + // data seen by each FWE process is isolated. + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface0" + SomeIpInstanceID = 0x5600 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface1" + SomeIpInstanceID = 0x5601 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface2" + SomeIpInstanceID = 0x5602 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface3" + SomeIpInstanceID = 0x5603 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface4" + SomeIpInstanceID = 0x5604 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface5" + SomeIpInstanceID = 0x5605 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface6" + SomeIpInstanceID = 0x5606 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface7" + SomeIpInstanceID = 0x5607 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface8" + SomeIpInstanceID = 0x5608 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface9" + SomeIpInstanceID = 0x5609 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface10" + SomeIpInstanceID = 0x560a + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface11" + SomeIpInstanceID = 0x560b + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface12" + SomeIpInstanceID = 0x560c + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface13" + SomeIpInstanceID = 0x560d + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface14" + SomeIpInstanceID = 0x560e + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface15" + SomeIpInstanceID = 0x560f + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface16" + SomeIpInstanceID = 0x5610 + } + + instance commonapi.ExampleSomeipInterface { + InstanceId = "commonapi.ExampleSomeipInterface17" + SomeIpInstanceID = 0x5611 + } +} diff --git a/interfaces/someip/fidl/ExampleSomeipInterface.fidl b/interfaces/someip/fidl/ExampleSomeipInterface.fidl new file mode 100644 index 00000000..df9f3846 --- /dev/null +++ b/interfaces/someip/fidl/ExampleSomeipInterface.fidl @@ -0,0 +1,120 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package commonapi + +interface ExampleSomeipInterface { + version { major 1 minor 0 } + + attribute Int32 x + attribute CommonTypes.a1Struct a1 + + method setInt32 { + in { + Int32 value + } + } + + method getInt32 { + out { + Int32 value + } + } + + method setInt64 { + in { + Int64 value + } + } + + method getInt64 { + out { + Int64 value + } + } + + method setBoolean { + in { + Boolean value + } + } + + method getBoolean { + out { + Boolean value + } + } + + method setFloat { + in { + Float value + } + } + + method getFloat { + out { + Float value + } + } + + method setDouble { + in { + Double value + } + } + + method getDouble { + out { + Double value + } + } + + method setString { + in { + String value + } + } + + method getString { + out { + String value + } + } + + method setInt32LongRunning { + in { + String commandId + Int32 value + } + } + + method getInt32LongRunning { + out { + Int32 value + } + } + + broadcast notifyLRCStatus { + out { + String commandID + Int32 commandStatus + Int32 commandReasonCode + String commandReasonDescription + } + } +} + +typeCollection CommonTypes { + version { major 1 minor 0 } + + struct a1Struct { + String s + a2Struct a2 + } + + struct a2Struct { + Int32 a + Boolean b + Double d + } +} diff --git a/interfaces/uds-dtc/udsDtcSchema.json b/interfaces/uds-dtc/udsDtcSchema.json new file mode 100644 index 00000000..43b58c3c --- /dev/null +++ b/interfaces/uds-dtc/udsDtcSchema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://aws-iot-automotive.com/udsDtcSchema", + "type": "object", + "additionalProperties": false, + "title": "IoTFleetWise UDS DTC Schema", + "description": "The root schema for FWE UDS DTC JSON", + "properties": { + "DetectedDTCs": { + "description": "Detected DTCs", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "DTCAndSnapshot": { + "type": "object", + "additionalProperties": false, + "properties": { + "DTCStatusAvailabilityMask": { + "description": "DTC status availability mask in hex format", + "type": "string" + }, + "dtcCodes": { + "description": "List of DTC codes, snapshot and extended data", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "DTC": { + "description": "DTC code in hex format", + "type": "string" + }, + "DTCExtendedData": { + "description": "DTC extended data in hex reportDTCExtDataRecordByDTCNumber format", + "type": "string" + }, + "DTCSnapshotRecord": { + "description": "DTC snapshot data in hex reportDTCSnapshotRecordByDTCNumber format", + "type": "string" + } + } + } + } + } + }, + "ECUID": { + "description": "ECU identifier in hex format", + "type": "string" + } + } + } + } + } +} diff --git a/pyproject.toml b/pyproject.toml index 71ccd6db..3a18ef94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,6 @@ line_length = 100 [tool.black] line_length = 100 + +[tool.md_dead_link_check] +check_web_links = false diff --git a/src/AaosVhalSource.cpp b/src/AaosVhalSource.cpp index 0b4942cd..9ad42a1b 100644 --- a/src/AaosVhalSource.cpp +++ b/src/AaosVhalSource.cpp @@ -2,8 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 #include "AaosVhalSource.h" -#include "CollectionInspectionAPITypes.h" +#include "IDecoderManifest.h" +#include "LoggingModule.h" #include "QueueTypes.h" +#include +#include +#include #include namespace Aws @@ -11,53 +15,91 @@ namespace Aws namespace IoTFleetWise { -// NOLINT below due to C++17 warning of redundant declarations that are required to maintain C++14 compatibility -constexpr const char *AaosVhalSource::CAN_CHANNEL_NUMBER; // NOLINT -constexpr const char *AaosVhalSource::CAN_RAW_FRAME_ID; // NOLINT -AaosVhalSource::AaosVhalSource( SignalBufferDistributorPtr signalBufferDistributor ) - : mSignalBufferDistributor{ std::move( signalBufferDistributor ) } +AaosVhalSource::AaosVhalSource( InterfaceID interfaceId, SignalBufferDistributorPtr signalBufferDistributor ) + : mInterfaceId( std::move( interfaceId ) ) + , mSignalBufferDistributor{ std::move( signalBufferDistributor ) } { } -bool -AaosVhalSource::init( CANChannelNumericID canChannel, CANRawFrameID canRawFrameId ) +static bool +popU32FromString( std::string &decoder, uint32_t &val ) { - if ( canChannel == INVALID_CAN_SOURCE_NUMERIC_ID ) + try + { + size_t pos{}; + val = static_cast( std::stoul( decoder, &pos, 0 ) ); + pos++; // Skip over delimiter + decoder = ( pos < decoder.size() ) ? decoder.substr( pos ) : ""; + } + catch ( ... ) { return false; } - mCanChannel = canChannel; - mCanRawFrameId = canRawFrameId; - setFilter( mCanChannel, mCanRawFrameId ); return true; } -const char * -AaosVhalSource::getThreadName() + +void +AaosVhalSource::onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, + VehicleDataSourceProtocol networkProtocol ) { - return "AaosVhalSource"; + if ( networkProtocol != VehicleDataSourceProtocol::CUSTOM_DECODING ) + { + return; + } + std::lock_guard lock( mDecoderDictionaryUpdateMutex ); + mVehiclePropertyInfo.clear(); + mSignalIdToSignalType.clear(); + auto decoderDictionary = std::dynamic_pointer_cast( dictionary ); + if ( decoderDictionary == nullptr ) + { + FWE_LOG_TRACE( "Decoder dictionary removed" ); + return; + } + auto decodersForInterface = decoderDictionary->customDecoderMethod.find( mInterfaceId ); + if ( decodersForInterface == decoderDictionary->customDecoderMethod.end() ) + { + FWE_LOG_TRACE( "Decoder dictionary does not contain interface ID " + mInterfaceId ); + return; + } + for ( const auto &decoder : decodersForInterface->second ) + { + auto decoderString = decoder.first; + auto signalId = decoder.second.mSignalID; + uint32_t vehiclePropertyId{}; + uint32_t areaIndex{}; + uint32_t resultIndex{}; + if ( ( !popU32FromString( decoderString, vehiclePropertyId ) ) || + ( !popU32FromString( decoderString, areaIndex ) ) || ( !popU32FromString( decoderString, resultIndex ) ) ) + { + FWE_LOG_ERROR( "Invalid decoder for signal ID " + std::to_string( signalId ) + ": " + decoder.first ); + continue; + } + mVehiclePropertyInfo.push_back( + std::array{ vehiclePropertyId, areaIndex, resultIndex, signalId } ); + mSignalIdToSignalType[signalId] = decoder.second.mSignalType; + } + FWE_LOG_TRACE( "Decoder dictionary updated" ); } std::vector> AaosVhalSource::getVehiclePropertyInfo() { - std::vector> propertyInfo; - auto signalInfo = getSignalInfo(); - for ( const auto &signal : signalInfo ) - { - propertyInfo.push_back( std::array{ - static_cast( signal.mOffset ), // Vehicle property ID - signal.mFirstBitPosition, // Area index - signal.mSizeInBits, // Result index - signal.mSignalID // Signal ID - } ); - } - return propertyInfo; + std::lock_guard lock( mDecoderDictionaryUpdateMutex ); + return mVehiclePropertyInfo; } void AaosVhalSource::setVehicleProperty( SignalID signalId, const DecodedSignalValue &value ) { auto signalType = SignalType::DOUBLE; + { + std::lock_guard lock( mDecoderDictionaryUpdateMutex ); + auto it = mSignalIdToSignalType.find( signalId ); + if ( it != mSignalIdToSignalType.end() ) + { + signalType = it->second; + } + } auto timestamp = mClock->systemTimeSinceEpochMs(); CollectedSignalsGroup collectedSignalsGroup; collectedSignalsGroup.push_back( CollectedSignal::fromDecodedSignal( signalId, timestamp, value, signalType ) ); @@ -65,10 +107,5 @@ AaosVhalSource::setVehicleProperty( SignalID signalId, const DecodedSignalValue mSignalBufferDistributor->push( CollectedDataFrame( collectedSignalsGroup ) ); } -void -AaosVhalSource::pollData() -{ -} - } // namespace IoTFleetWise } // namespace Aws diff --git a/src/AaosVhalSource.h b/src/AaosVhalSource.h index 85669672..d1d755c0 100644 --- a/src/AaosVhalSource.h +++ b/src/AaosVhalSource.h @@ -5,11 +5,13 @@ #include "Clock.h" #include "ClockHandler.h" #include "CollectionInspectionAPITypes.h" -#include "CustomDataSource.h" +#include "IDecoderDictionary.h" #include "SignalTypes.h" +#include "VehicleDataSourceTypes.h" #include #include #include +#include #include #include @@ -18,26 +20,23 @@ namespace Aws namespace IoTFleetWise { -/** - * To implement a custom data source create a new class and inherit from CustomDataSource - * then call setFilter() then start() and provide an implementation for pollData - */ -class AaosVhalSource : public CustomDataSource +class AaosVhalSource { public: /** + * @param interfaceId Interface identifier * @param signalBufferDistributor Signal buffer distributor */ - AaosVhalSource( SignalBufferDistributorPtr signalBufferDistributor ); - /** - * Initialize AaosVhalSource and set filter for CustomDataSource - * - * @param canChannel the CAN channel used in the decoder manifest - * @param canRawFrameId the CAN message Id used in the decoder manifest - * - * @return on success true otherwise false - */ - bool init( CANChannelNumericID canChannel, CANRawFrameID canRawFrameId ); + AaosVhalSource( InterfaceID interfaceId, SignalBufferDistributorPtr signalBufferDistributor ); + ~AaosVhalSource() = default; + + AaosVhalSource( const AaosVhalSource & ) = delete; + AaosVhalSource &operator=( const AaosVhalSource & ) = delete; + AaosVhalSource( AaosVhalSource && ) = delete; + AaosVhalSource &operator=( AaosVhalSource && ) = delete; + + void onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, + VehicleDataSourceProtocol networkProtocol ); /** * Returns a vector of vehicle property info @@ -58,19 +57,13 @@ class AaosVhalSource : public CustomDataSource */ void setVehicleProperty( SignalID signalId, const DecodedSignalValue &value ); - static constexpr const char *CAN_CHANNEL_NUMBER = "canChannel"; - static constexpr const char *CAN_RAW_FRAME_ID = "canFrameId"; - -protected: - void pollData() override; - const char *getThreadName() override; - private: + InterfaceID mInterfaceId; SignalBufferDistributorPtr mSignalBufferDistributor; std::unordered_map mSignalIdToSignalType; std::shared_ptr mClock = ClockHandler::getClock(); - CANChannelNumericID mCanChannel{ INVALID_CAN_SOURCE_NUMERIC_ID }; - CANRawFrameID mCanRawFrameId{ 0 }; + std::mutex mDecoderDictionaryUpdateMutex; + std::vector> mVehiclePropertyInfo; }; } // namespace IoTFleetWise diff --git a/src/ActuatorCommandManager.cpp b/src/ActuatorCommandManager.cpp new file mode 100644 index 00000000..514ca4a6 --- /dev/null +++ b/src/ActuatorCommandManager.cpp @@ -0,0 +1,295 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "ActuatorCommandManager.h" +#include "Assert.h" +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include "LoggingModule.h" +#include "QueueTypes.h" +#include "TraceModule.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +ActuatorCommandManager::ActuatorCommandManager( std::shared_ptr commandResponses, + size_t maxConcurrentCommandRequests, + std::shared_ptr rawBufferManager ) + : mMaxConcurrentCommandRequests( maxConcurrentCommandRequests ) + , mCommandResponses( std::move( commandResponses ) ) + , mRawBufferManager( std::move( rawBufferManager ) ) +{ +} + +void +ActuatorCommandManager::onReceivingCommandRequest( const ActuatorCommandRequest &commandRequest ) +{ + std::lock_guard lock( mCommandRequestMutex ); + if ( mCommandRequestQueue.size() >= mMaxConcurrentCommandRequests ) + { + FWE_LOG_ERROR( "Command Requests processing queue is full, could not ingest command request" ); + return; + } + mCommandRequestQueue.push( commandRequest ); + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::QUEUE_PENDING_COMMAND_REQUESTS ); + FWE_LOG_TRACE( "New command request was handed over" ); + mWait.notify(); +} + +bool +ActuatorCommandManager::registerDispatcher( const std::string &interfaceId, + std::shared_ptr dispatcher ) +{ + auto registered = mInterfaceIDToCommandDispatcherMap.emplace( interfaceId, std::move( dispatcher ) ).second; + if ( !registered ) + { + FWE_LOG_ERROR( "Dispatcher for interface ID " + interfaceId + " already registered" ); + } + return registered; +} + +std::unordered_map> +ActuatorCommandManager::getActuatorNames() +{ + std::unordered_map> actuatorNames; + for ( const auto &interface : mInterfaceIDToCommandDispatcherMap ) + { + actuatorNames.emplace( interface.first, interface.second->getActuatorNames() ); + } + return actuatorNames; +} + +bool +ActuatorCommandManager::start() +{ + if ( mCommandResponses == nullptr ) + { + FWE_LOG_ERROR( "No queue provided for the Command Responses" ); + return false; + } + + // Prevent concurrent stop/init + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( false ); + if ( !mThread.create( doWork, this ) ) + { + FWE_LOG_TRACE( "Command Manager Thread failed to start" ); + } + else + { + FWE_LOG_TRACE( "Command Manager Thread started" ); + mThread.setThreadName( "fwRCCommandMng" ); + } + + return mThread.isActive() && mThread.isValid(); +} + +void +ActuatorCommandManager::doWork( void *data ) +{ + ActuatorCommandManager *commandManager = static_cast( data ); + + // Initialize dispatchers on this thread to avoid delaying bootstrap. Commands may be received + // directly after connection to the cloud, which will be queued until this initialization loop + // has completed. + for ( auto interfaceDispatcherIt = commandManager->mInterfaceIDToCommandDispatcherMap.begin(); + ( interfaceDispatcherIt != commandManager->mInterfaceIDToCommandDispatcherMap.end() ) && + ( !commandManager->shouldStop() ); + interfaceDispatcherIt++ ) + { + FWE_GRACEFUL_FATAL_ASSERT( interfaceDispatcherIt->second->init(), "Fatal error initializing dispatcher", ); + } + + while ( !commandManager->shouldStop() ) + { + commandManager->mWait.wait( Signal::WaitWithPredicate ); + + while ( !commandManager->mCommandRequestQueue.empty() ) + { + auto &commandRequest = commandManager->mCommandRequestQueue.front(); + commandManager->processCommandRequest( commandRequest ); + commandManager->mCommandRequestQueue.pop(); + TraceModule::get().decrementAtomicVariable( TraceAtomicVariable::QUEUE_PENDING_COMMAND_REQUESTS ); + } + } +} + +void +ActuatorCommandManager::processCommandRequest( const ActuatorCommandRequest &commandRequest ) +{ + FWE_LOG_TRACE( "Processing Command Request with ID: " + commandRequest.commandID ); + + if ( commandRequest.decoderID != mCurrentDecoderManifestID ) + { + FWE_LOG_ERROR( "Decoder manifest sync id does not match with the decoder manifest used by the agent, cannot " + "process Command with ID " + + commandRequest.commandID ); + queueCommandResponse( + commandRequest, + CommandStatus::EXECUTION_FAILED, + REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC, + // TODO: ALLOW_ALL_CHARS_FOR_REASON_DESCRIPTION: Add the detailed message back when the commands backend + // team remove the restriction on allowed chars for description + // commandRequest.decoderID + " vs " + mCurrentDecoderManifestID ); + "The decoder manifest associated to the command is different from the one received by the device" ); + return; + } + + if ( mCustomSignalDecoderFormatMap == nullptr ) + { + FWE_LOG_ERROR( "No Custom Signal Decoder Format map was provided, cannot process Command with ID " + + commandRequest.commandID ); + queueCommandResponse( commandRequest, + CommandStatus::EXECUTION_FAILED, + REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC, + "No decoder manifest" ); + return; + } + + // Retrieve Custom Decoder ID from the decoder manifest + auto customSignalDecoderFormatIt = mCustomSignalDecoderFormatMap->find( commandRequest.signalID ); + if ( customSignalDecoderFormatIt == mCustomSignalDecoderFormatMap->end() ) + { + FWE_LOG_ERROR( "Command Signal Decoder Format not found for signal ID " + + std::to_string( commandRequest.signalID ) + ". Command with ID " + commandRequest.commandID + + " can not be processed." ); + queueCommandResponse( + commandRequest, CommandStatus::EXECUTION_FAILED, REASON_CODE_NO_DECODING_RULES_FOUND, "" ); + return; + } + + const auto &interfaceId = customSignalDecoderFormatIt->second.mInterfaceId; + auto commandDispatcherIt = mInterfaceIDToCommandDispatcherMap.find( interfaceId ); + if ( commandDispatcherIt == mInterfaceIDToCommandDispatcherMap.end() ) + { + FWE_LOG_ERROR( "No command dispatcher found for signal ID " + std::to_string( commandRequest.signalID ) + + ", interface ID " + interfaceId + ". Command with ID " + commandRequest.commandID + + " can not be processed." ); + queueCommandResponse( + commandRequest, CommandStatus::EXECUTION_FAILED, REASON_CODE_NO_COMMAND_DISPATCHER_FOUND, "" ); + return; + } + const auto &actuatorName = customSignalDecoderFormatIt->second.mDecoder; + + // Timeout is already checked during command reception from the cloud, but check again here in case + // command dispatching is being run synchronously and a large delay has occurred since the command + // was received. + if ( ( commandRequest.executionTimeoutMs > 0 ) && + ( ( commandRequest.issuedTimestampMs + commandRequest.executionTimeoutMs ) <= + mClock->systemTimeSinceEpochMs() ) ) + { + FWE_LOG_ERROR( "Command Request with ID " + commandRequest.commandID + " timed out" ); + queueCommandResponse( + commandRequest, CommandStatus::EXECUTION_TIMEOUT, REASON_CODE_TIMED_OUT_BEFORE_DISPATCH, "" ); + return; + } + + // Send command request to the command dispatcher + commandDispatcherIt->second->setActuatorValue( + actuatorName, + commandRequest.signalValueWrapper, + commandRequest.commandID, + commandRequest.issuedTimestampMs, + commandRequest.executionTimeoutMs, + [this, commandRequest]( + CommandStatus status, CommandReasonCode reasonCode, const CommandReasonDescription &reasonDescription ) { + queueCommandResponse( commandRequest, status, reasonCode, reasonDescription ); + } ); +} + +void +ActuatorCommandManager::queueCommandResponse( const ActuatorCommandRequest &commandRequest, + CommandStatus commandStatus, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) +{ + if ( commandRequest.signalValueWrapper.type == SignalType::STRING ) + { + mRawBufferManager->decreaseHandleUsageHint( commandRequest.signalValueWrapper.value.rawDataVal.signalId, + commandRequest.signalValueWrapper.value.rawDataVal.handle, + RawData::BufferHandleUsageStage::UPLOADING ); + } + + // Emit metrics for command execution + if ( reasonCode == REASON_CODE_PRECONDITION_FAILED ) + { + TraceModule::get().incrementVariable( TraceVariable::COMMAND_PRECONDITION_CHECK_FAILURE ); + } + else if ( reasonCode == REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC ) + { + TraceModule::get().incrementVariable( TraceVariable::COMMAND_DECODER_MANIFEST_FAILURE ); + } + else if ( commandStatus == CommandStatus::EXECUTION_TIMEOUT ) + { + TraceModule::get().incrementVariable( TraceVariable::COMMAND_EXECUTION_TIMEOUT ); + } + else if ( commandStatus == CommandStatus::EXECUTION_FAILED ) + { + TraceModule::get().incrementVariable( TraceVariable::COMMAND_EXECUTION_FAILURE ); + } + else + { + // Do nothing + } + + // coverity[check_return] + mCommandResponses->push( + std::make_shared( commandRequest.commandID, commandStatus, reasonCode, reasonDescription ) ); +} + +void +ActuatorCommandManager::onChangeOfCustomSignalDecoderFormatMap( + const SyncID ¤tDecoderManifestID, + const SignalIDToCustomSignalDecoderFormatMapPtr &customSignalDecoderFormatMap ) +{ + std::lock_guard lock( mCustomSignalDecoderFormatMapUpdateMutex ); + mCustomSignalDecoderFormatMap = customSignalDecoderFormatMap; + mCurrentDecoderManifestID = currentDecoderManifestID; + FWE_LOG_TRACE( "Custom Signal Decoder Format Map was handed over to the Command Manager" ); +} + +bool +ActuatorCommandManager::shouldStop() const +{ + return mShouldStop.load( std::memory_order_relaxed ); +} + +bool +ActuatorCommandManager::stop() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return true; + } + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( true, std::memory_order_relaxed ); + FWE_LOG_TRACE( "Request stop" ); + mWait.notify(); + mThread.release(); + FWE_LOG_TRACE( "Stop finished" ); + mShouldStop.store( false, std::memory_order_relaxed ); + return !mThread.isActive(); +} + +bool +ActuatorCommandManager::isAlive() +{ + return mThread.isValid() && mThread.isActive(); +} + +ActuatorCommandManager::~ActuatorCommandManager() +{ + // To make sure the thread stops during teardown of tests. + if ( isAlive() ) + { + stop(); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/ActuatorCommandManager.h b/src/ActuatorCommandManager.h new file mode 100644 index 00000000..37746fa7 --- /dev/null +++ b/src/ActuatorCommandManager.h @@ -0,0 +1,162 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "ICommandDispatcher.h" +#include "IDecoderManifest.h" +#include "RawDataManager.h" +#include "Signal.h" +#include "SignalTypes.h" +#include "Thread.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Class that implements logic of remote command request processing + * + * This class is notified on the arrival of the new Decoder Manifest and Command Request. When new Command Request is + * received, Command Manager will queue the command for the execution (FIFO). Command execution timeout and + * preconditions are checked in this thread. After Command Dispatcher is finished with the command execution, Command + * Manager will queue Command Response for the upload. + */ +class ActuatorCommandManager +{ +public: + ActuatorCommandManager( std::shared_ptr commandResponses, + size_t maxConcurrentCommandRequests, + std::shared_ptr rawBufferManager ); + + ~ActuatorCommandManager(); + + ActuatorCommandManager( const ActuatorCommandManager & ) = delete; + ActuatorCommandManager &operator=( const ActuatorCommandManager & ) = delete; + ActuatorCommandManager( ActuatorCommandManager && ) = delete; + ActuatorCommandManager &operator=( ActuatorCommandManager && ) = delete; + + /** + * @brief Sets up connection and start main thread. + * @return True if successful. False otherwise. + */ + bool start(); + + /** + * @brief Disconnect and stops main thread. + * @return True if successful. False otherwise. + */ + bool stop(); + + /** + * @brief Checks that the worker thread is healthy and consuming data. + */ + bool isAlive(); + + void onChangeOfCustomSignalDecoderFormatMap( + const SyncID ¤tDecoderManifestID, + const SignalIDToCustomSignalDecoderFormatMapPtr &customSignalDecoderFormatMap ); + + /** + * @brief callback to be invoked to receive command request + */ + void onReceivingCommandRequest( const ActuatorCommandRequest &commandRequest ); + + /** + * Register a command dispatcher for a given interface ID + * @param interfaceId Network interface ID + * @param dispatcher Command dispatcher + * @return True if registered, false if another dispatcher was already registered for the given + * interface ID + */ + bool registerDispatcher( const std::string &interfaceId, std::shared_ptr dispatcher ); + + /** + * @brief Gets the actuator names supported by the command dispatchers + * @todo The decoder manifest doesn't yet have an indication of whether a signal is + * READ/WRITE/READ_WRITE. Until it does this interface is needed to get the names of the + * actuators supported by the command dispatcher, so that for string signals, buffers can be + * pre-allocated in the RawDataManager by the CollectionSchemeManager when a new decoder + * manifest arrives. When the READ/WRITE/READ_WRITE usage of a signal is available this + * interface can be removed. + * @return Actuator names per interface ID + */ + std::unordered_map> getActuatorNames(); + +private: + Thread mThread; + std::atomic mShouldStop{ false }; + std::mutex mThreadMutex; + Signal mWait; + + std::mutex mCommandRequestMutex; + + std::mutex mCustomSignalDecoderFormatMapUpdateMutex; + std::shared_ptr mCustomSignalDecoderFormatMap; + + /** + * @brief Internal command manager queue to process multiple commands + */ + std::queue mCommandRequestQueue; + + /** + * @brief Maximum amount of elements mCommandRequestQueue can contain + */ + size_t mMaxConcurrentCommandRequests{ 0 }; + + /** + * @brief Queue shared with the Data Sender containing Command Response to upload to the cloud + */ + std::shared_ptr mCommandResponses; + + /** + * @brief Clock member variable used to generate the time a command request was received + */ + std::shared_ptr mClock = ClockHandler::getClock(); + + /** + * @brief Map of suported Command dispatchers to call to dispatch command request to the underlying vehicle service. + */ + std::unordered_map> mInterfaceIDToCommandDispatcherMap; + + /** + * @brief Sync ID of the used decoder manifest to compare with the sync ID from the command request + */ + SyncID mCurrentDecoderManifestID; + + std::shared_ptr mRawBufferManager; + + // Stop the thread + bool shouldStop() const; + + static void doWork( void *data ); + + /** + * @brief Function with the main logic for command processing, including timeout ad precondition checks + */ + void processCommandRequest( const ActuatorCommandRequest &commandRequest ); + + /** + * @brief Adds command response object to the queue for the Data Sender to upload + */ + void queueCommandResponse( const ActuatorCommandRequest &commandRequest, + CommandStatus commandStatus, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ); +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/AwsGreengrassV2ConnectivityModule.cpp b/src/AwsGreengrassV2ConnectivityModule.cpp index 42483ff3..8f5507f7 100644 --- a/src/AwsGreengrassV2ConnectivityModule.cpp +++ b/src/AwsGreengrassV2ConnectivityModule.cpp @@ -12,10 +12,12 @@ namespace Aws namespace IoTFleetWise { -AwsGreengrassV2ConnectivityModule::AwsGreengrassV2ConnectivityModule( Aws::Crt::Io::ClientBootstrap *clientBootstrap ) +AwsGreengrassV2ConnectivityModule::AwsGreengrassV2ConnectivityModule( Aws::Crt::Io::ClientBootstrap *clientBootstrap, + const TopicConfig &topicConfig ) : mConnected( false ) , mConnectionEstablished( false ) , mClientBootstrap( clientBootstrap ) + , mTopicConfig( topicConfig ) { } @@ -51,20 +53,9 @@ AwsGreengrassV2ConnectivityModule::connect() } std::shared_ptr -AwsGreengrassV2ConnectivityModule::createSender( const std::string &topicName, QoS publishQoS ) +AwsGreengrassV2ConnectivityModule::createSender() { - auto greengrassPublishQoS = Aws::Greengrass::QOS_AT_MOST_ONCE; - switch ( publishQoS ) - { - case QoS::AT_MOST_ONCE: - greengrassPublishQoS = Aws::Greengrass::QOS_AT_MOST_ONCE; - break; - case QoS::AT_LEAST_ONCE: - greengrassPublishQoS = Aws::Greengrass::QOS_AT_LEAST_ONCE; - break; - } - - auto sender = std::make_shared( this, mGreengrassClient, topicName, greengrassPublishQoS ); + auto sender = std::make_shared( this, mGreengrassClient, mTopicConfig ); mSenders.emplace_back( sender ); return sender; } diff --git a/src/AwsGreengrassV2ConnectivityModule.h b/src/AwsGreengrassV2ConnectivityModule.h index 3e9e11ad..5992e0d1 100644 --- a/src/AwsGreengrassV2ConnectivityModule.h +++ b/src/AwsGreengrassV2ConnectivityModule.h @@ -10,6 +10,7 @@ #include "ISender.h" #include "Listener.h" #include "LoggingModule.h" +#include "TopicConfig.h" #include #include #include @@ -61,7 +62,7 @@ class IpcLifecycleHandler : public ConnectionLifecycleHandler class AwsGreengrassV2ConnectivityModule : public IConnectivityModule { public: - AwsGreengrassV2ConnectivityModule( Aws::Crt::Io::ClientBootstrap *clientBootstrap ); + AwsGreengrassV2ConnectivityModule( Aws::Crt::Io::ClientBootstrap *clientBootstrap, const TopicConfig &topicConfig ); ~AwsGreengrassV2ConnectivityModule() override; AwsGreengrassV2ConnectivityModule( const AwsGreengrassV2ConnectivityModule & ) = delete; @@ -79,7 +80,7 @@ class AwsGreengrassV2ConnectivityModule : public IConnectivityModule return mConnected; }; - std::shared_ptr createSender( const std::string &topicName, QoS publishQoS = QoS::AT_MOST_ONCE ) override; + std::shared_ptr createSender() override; std::shared_ptr createReceiver( const std::string &topicName ) override; @@ -94,6 +95,7 @@ class AwsGreengrassV2ConnectivityModule : public IConnectivityModule std::unique_ptr mLifecycleHandler; std::shared_ptr mGreengrassClient; Aws::Crt::Io::ClientBootstrap *mClientBootstrap; + const TopicConfig &mTopicConfig; }; } // namespace IoTFleetWise diff --git a/src/AwsGreengrassV2Sender.cpp b/src/AwsGreengrassV2Sender.cpp index 97e5a1da..32cc0b6b 100644 --- a/src/AwsGreengrassV2Sender.cpp +++ b/src/AwsGreengrassV2Sender.cpp @@ -11,7 +11,6 @@ #include #include #include -#include namespace Aws { @@ -21,12 +20,10 @@ namespace IoTFleetWise AwsGreengrassV2Sender::AwsGreengrassV2Sender( IConnectivityModule *connectivityModule, std::shared_ptr &greengrassClient, - std::string topicName, - Aws::Greengrass::QOS publishQoS ) + const TopicConfig &topicConfig ) : mConnectivityModule( connectivityModule ) , mGreengrassClient( greengrassClient ) - , mPublishQoS( publishQoS ) - , mTopicName( std::move( topicName ) ) + , mTopicConfig( topicConfig ) { } @@ -54,16 +51,8 @@ AwsGreengrassV2Sender::getMaxSendSize() const } void -AwsGreengrassV2Sender::sendBuffer( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) -{ - sendBufferToTopic( mTopicName, buf, size, callback ); -} - -void -AwsGreengrassV2Sender::sendBufferToTopic( const std::string &topic, - const uint8_t *buf, - size_t size, - OnDataSentCallback callback ) +AwsGreengrassV2Sender::sendBuffer( + const std::string &topic, const uint8_t *buf, size_t size, OnDataSentCallback callback, QoS qos ) { std::lock_guard connectivityLock( mConnectivityMutex ); if ( topic.empty() ) @@ -109,12 +98,24 @@ AwsGreengrassV2Sender::sendBufferToTopic( const std::string &topic, callback( ConnectivityError::NoConnection ); return; } + + auto greengrassPublishQoS = Aws::Greengrass::QOS_AT_MOST_ONCE; + switch ( qos ) + { + case QoS::AT_MOST_ONCE: + greengrassPublishQoS = Aws::Greengrass::QOS_AT_MOST_ONCE; + break; + case QoS::AT_LEAST_ONCE: + greengrassPublishQoS = Aws::Greengrass::QOS_AT_LEAST_ONCE; + break; + } + auto publishOperation = mGreengrassClient->NewPublishToIoTCore(); Aws::Greengrass::PublishToIoTCoreRequest publishRequest; publishRequest.SetTopicName( topic.c_str() != nullptr ? topic.c_str() : "" ); Aws::Crt::Vector payload( buf, buf + size ); publishRequest.SetPayload( payload ); - publishRequest.SetQos( mPublishQoS ); + publishRequest.SetQos( greengrassPublishQoS ); FWE_LOG_TRACE( "Attempting to publish to " + topic + " topic" ); auto onMessageFlushCallback = [callback, topicName = topic]( int errorCode ) { diff --git a/src/AwsGreengrassV2Sender.h b/src/AwsGreengrassV2Sender.h index 6b12069a..e52e1733 100644 --- a/src/AwsGreengrassV2Sender.h +++ b/src/AwsGreengrassV2Sender.h @@ -5,6 +5,7 @@ #include "IConnectivityModule.h" #include "ISender.h" +#include "TopicConfig.h" #include #include #include @@ -31,8 +32,7 @@ class AwsGreengrassV2Sender : public ISender public: AwsGreengrassV2Sender( IConnectivityModule *connectivityModule, std::shared_ptr &greengrassClient, - std::string topicName, - Aws::Greengrass::QOS publishQoS ); + const TopicConfig &topicConfig ); ~AwsGreengrassV2Sender() override = default; AwsGreengrassV2Sender( const AwsGreengrassV2Sender & ) = delete; @@ -44,12 +44,11 @@ class AwsGreengrassV2Sender : public ISender size_t getMaxSendSize() const override; - void sendBuffer( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) override; - - void sendBufferToTopic( const std::string &topic, - const uint8_t *buf, - size_t size, - OnDataSentCallback callback ) override; + void sendBuffer( const std::string &topic, + const uint8_t *buf, + size_t size, + OnDataSentCallback callback, + QoS qos = QoS::AT_LEAST_ONCE ) override; void invalidateConnection() @@ -68,13 +67,15 @@ class AwsGreengrassV2Sender : public ISender return mPayloadCountSent; } + const TopicConfig & + getTopicConfig() const override + { + return mTopicConfig; + } + private: bool isAliveNotThreadSafe(); - bool - isTopicValid() - { - return !mTopicName.empty(); - }; + /** See "Message size" : "The payload for every publish request can be no larger * than 128 KB. AWS IoT Core rejects publish and connect requests larger than this size." * https://docs.aws.amazon.com/general/latest/gr/iot-core.html#limits_iot @@ -84,10 +85,9 @@ class AwsGreengrassV2Sender : public ISender std::mutex mConnectivityMutex; std::shared_ptr &mGreengrassClient; - Aws::Greengrass::QOS mPublishQoS; std::atomic mPayloadCountSent{}; - std::string mTopicName; + const TopicConfig &mTopicConfig; }; } // namespace IoTFleetWise diff --git a/src/AwsIotConnectivityModule.cpp b/src/AwsIotConnectivityModule.cpp index c59b09b9..416c5702 100644 --- a/src/AwsIotConnectivityModule.cpp +++ b/src/AwsIotConnectivityModule.cpp @@ -24,10 +24,12 @@ namespace IoTFleetWise AwsIotConnectivityModule::AwsIotConnectivityModule( std::string rootCA, std::string clientId, std::shared_ptr mqttClientBuilder, + const TopicConfig &topicConfig, AwsIotConnectivityConfig connectionConfig ) : mRootCA( std::move( rootCA ) ) , mClientId( std::move( clientId ) ) , mMqttClientBuilder( std::move( mqttClientBuilder ) ) + , mTopicConfig( topicConfig ) , mConnectionConfig( std::move( connectionConfig ) ) , mInitialConnectionThread( [this]() -> RetryStatus { @@ -65,20 +67,9 @@ AwsIotConnectivityModule::connect() } std::shared_ptr -AwsIotConnectivityModule::createSender( const std::string &topicName, QoS publishQoS ) +AwsIotConnectivityModule::createSender() { - auto sdkPublishQoS = Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE; - switch ( publishQoS ) - { - case QoS::AT_MOST_ONCE: - sdkPublishQoS = Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE; - break; - case QoS::AT_LEAST_ONCE: - sdkPublishQoS = Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_LEAST_ONCE; - break; - } - - auto sender = std::make_shared( this, mMqttClient, topicName, sdkPublishQoS ); + auto sender = std::make_shared( this, mMqttClient, mTopicConfig ); mSenders.emplace_back( sender ); return sender; } diff --git a/src/AwsIotConnectivityModule.h b/src/AwsIotConnectivityModule.h index c2b97e83..cf4c3460 100644 --- a/src/AwsIotConnectivityModule.h +++ b/src/AwsIotConnectivityModule.h @@ -12,6 +12,7 @@ #include "LoggingModule.h" #include "MqttClientWrapper.h" #include "RetryThread.h" +#include "TopicConfig.h" #include #include #include @@ -175,11 +176,13 @@ class AwsIotConnectivityModule : public IConnectivityModule * @param rootCA The Root CA for the certificate * @param clientId the id that is used to identify this connection instance * @param mqttClientBuilder a builder that can create MQTT client instances + * @param topicConfig responsible for building topic names. It will be available to senders. * @param connectionConfig allows some connection config to be overriden */ AwsIotConnectivityModule( std::string rootCA, std::string clientId, std::shared_ptr mqttClientBuilder, + const TopicConfig &topicConfig, AwsIotConnectivityConfig connectionConfig = AwsIotConnectivityConfig() ); ~AwsIotConnectivityModule() override; @@ -207,7 +210,7 @@ class AwsIotConnectivityModule : public IConnectivityModule return mConnected; }; - std::shared_ptr createSender( const std::string &topicName, QoS publishQoS = QoS::AT_MOST_ONCE ) override; + std::shared_ptr createSender() override; std::shared_ptr createReceiver( const std::string &topicName ) override; @@ -224,6 +227,7 @@ class AwsIotConnectivityModule : public IConnectivityModule std::string mClientId; std::shared_ptr mMqttClient; std::shared_ptr mMqttClientBuilder; + const TopicConfig &mTopicConfig; AwsIotConnectivityConfig mConnectionConfig; RetryThread mInitialConnectionThread; RetryThread mSubscriptionsThread; diff --git a/src/AwsIotReceiver.cpp b/src/AwsIotReceiver.cpp index 9decc899..a0db929b 100644 --- a/src/AwsIotReceiver.cpp +++ b/src/AwsIotReceiver.cpp @@ -80,10 +80,18 @@ AwsIotReceiver::subscribe() mSubscribed = false; if ( errorCode != 0 ) { - TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::SUBSCRIBE_ERROR ); auto errorString = Aws::Crt::ErrorDebugString( errorCode ); - FWE_LOG_ERROR( "Subscribe failed with error code " + std::to_string( errorCode ) + ": " + - std::string( errorString != nullptr ? errorString : "Unknown error" ) ); + auto logMessage = "Subscribe failed with error code " + std::to_string( errorCode ) + ": " + + std::string( errorString != nullptr ? errorString : "Unknown error" ); + if ( errorCode == AWS_ERROR_MQTT5_USER_REQUESTED_STOP ) + { + FWE_LOG_TRACE( logMessage ); + } + else + { + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::SUBSCRIBE_ERROR ); + FWE_LOG_ERROR( logMessage ); + } subscribeFinishedPromise.set_value( false ); return; } diff --git a/src/AwsIotSender.cpp b/src/AwsIotSender.cpp index 349e3d10..32445541 100644 --- a/src/AwsIotSender.cpp +++ b/src/AwsIotSender.cpp @@ -10,7 +10,6 @@ #include #include #include -#include namespace Aws { @@ -19,12 +18,10 @@ namespace IoTFleetWise AwsIotSender::AwsIotSender( IConnectivityModule *connectivityModule, std::shared_ptr &mqttClient, - std::string topicName, - Aws::Crt::Mqtt5::QOS publishQoS ) + const TopicConfig &topicConfig ) : mConnectivityModule( connectivityModule ) , mMqttClient( mqttClient ) - , mTopicName( std::move( topicName ) ) - , mPublishQoS( publishQoS ) + , mTopicConfig( topicConfig ) { } @@ -52,16 +49,8 @@ AwsIotSender::getMaxSendSize() const } void -AwsIotSender::sendBuffer( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) -{ - sendBufferToTopic( mTopicName, buf, size, callback ); -} - -void -AwsIotSender::sendBufferToTopic( const std::string &topic, - const uint8_t *buf, - size_t size, - OnDataSentCallback callback ) +AwsIotSender::sendBuffer( + const std::string &topic, const uint8_t *buf, size_t size, OnDataSentCallback callback, QoS qos ) { std::lock_guard connectivityLock( mConnectivityMutex ); if ( topic.empty() ) @@ -100,14 +89,23 @@ AwsIotSender::sendBufferToTopic( const std::string &topic, return; } - publishMessageToTopic( topic, buf, size, callback ); + auto sdkQos = Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE; + switch ( qos ) + { + case QoS::AT_MOST_ONCE: + sdkQos = Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE; + break; + case QoS::AT_LEAST_ONCE: + sdkQos = Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_LEAST_ONCE; + break; + } + + publishMessageToTopic( topic, buf, size, callback, sdkQos ); } void -AwsIotSender::publishMessageToTopic( const std::string &topic, - const uint8_t *buf, - size_t size, - OnDataSentCallback callback ) +AwsIotSender::publishMessageToTopic( + const std::string &topic, const uint8_t *buf, size_t size, OnDataSentCallback callback, Aws::Crt::Mqtt5::QOS qos ) { auto payload = Aws::Crt::ByteBufFromArray( buf, size ); @@ -126,14 +124,22 @@ AwsIotSender::publishMessageToTopic( const std::string &topic, else { auto errorString = Aws::Crt::ErrorDebugString( errorCode ); - FWE_LOG_ERROR( std::string( "Operation failed with error" ) + - ( errorString != nullptr ? std::string( errorString ) : std::string( "Unknown error" ) ) ); + auto logMessage = "Publish failed with error: " + + ( errorString != nullptr ? std::string( errorString ) : std::string( "Unknown error" ) ); + if ( errorCode == AWS_ERROR_MQTT5_USER_REQUESTED_STOP ) + { + FWE_LOG_TRACE( logMessage ); + } + else + { + FWE_LOG_ERROR( logMessage ); + } callback( ConnectivityError::TransmissionError ); } }; std::shared_ptr publishPacket = std::make_shared( - topic.c_str(), Aws::Crt::ByteCursorFromByteBuf( payload ), mPublishQoS ); + topic.c_str(), Aws::Crt::ByteCursorFromByteBuf( payload ), qos ); if ( !mMqttClient->Publish( publishPacket, onPublishComplete ) ) { callback( ConnectivityError::TransmissionError ); diff --git a/src/AwsIotSender.h b/src/AwsIotSender.h index c0423431..027abbc4 100644 --- a/src/AwsIotSender.h +++ b/src/AwsIotSender.h @@ -8,6 +8,7 @@ #include "IConnectivityModule.h" #include "ISender.h" #include "MqttClientWrapper.h" +#include "TopicConfig.h" #include #include #include @@ -34,8 +35,7 @@ class AwsIotSender : public ISender public: AwsIotSender( IConnectivityModule *connectivityModule, std::shared_ptr &mqttClient, - std::string topicName, - Aws::Crt::Mqtt5::QOS publishQoS ); + const TopicConfig &topicConfig ); ~AwsIotSender() override = default; AwsIotSender( const AwsIotSender & ) = delete; @@ -47,12 +47,11 @@ class AwsIotSender : public ISender size_t getMaxSendSize() const override; - void sendBuffer( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) override; - - void sendBufferToTopic( const std::string &topic, - const uint8_t *buf, - size_t size, - OnDataSentCallback callback ) override; + void sendBuffer( const std::string &topic, + const uint8_t *buf, + size_t size, + OnDataSentCallback callback, + QoS qos = QoS::AT_LEAST_ONCE ) override; void invalidateConnection() @@ -71,20 +70,20 @@ class AwsIotSender : public ISender return mPayloadCountSent; } + const TopicConfig & + getTopicConfig() const override + { + return mTopicConfig; + } + private: bool isAliveNotThreadSafe(); - // coverity[autosar_cpp14_a0_1_3_violation] false positive - function is used - bool - isTopicValid() - { - return !mTopicName.empty(); - }; - void publishMessageToTopic( const std::string &topic, const uint8_t *buf, size_t size, - OnDataSentCallback callback ); + OnDataSentCallback callback, + Aws::Crt::Mqtt5::QOS qos ); /** See "Message size" : "The payload for every publish request can be no larger * than 128 KB. AWS IoT Core rejects publish and connect requests larger than this size." @@ -93,16 +92,14 @@ class AwsIotSender : public ISender static const size_t AWS_IOT_MAX_MESSAGE_SIZE = 131072; // = 128 KiB IConnectivityModule *mConnectivityModule; std::shared_ptr &mMqttClient; + const TopicConfig &mTopicConfig; std::mutex mConnectivityMutex; - std::string mTopicName; std::atomic mPayloadCountSent{}; /** * @brief Clock member variable used to generate the time an MQTT message was received */ std::shared_ptr mClock = ClockHandler::getClock(); - - Aws::Crt::Mqtt5::QOS mPublishQoS; }; } // namespace IoTFleetWise diff --git a/src/CANDecoder.cpp b/src/CANDecoder.cpp index 3e93aff6..d150a6be 100644 --- a/src/CANDecoder.cpp +++ b/src/CANDecoder.cpp @@ -160,7 +160,7 @@ CANDecoder::extractSignalFromFrame( const uint8_t *frameData, const CANSignalFor // Write first bits to result // NOTE: The start bit here is different from how it appears in a DBC file. In a DBC file, the // start bit indicates the LSB for little endian and MSB for big endian signals. - // But AWS IoT Fleetwise considers start bit to always be the LSB regardless of endianess. + // But AWS IoT FleetWise considers start bit to always be the LSB regardless of endianess. uint64_t result = frameData[startByte] >> startBitInByte; // Write residual bytes diff --git a/src/CacheAndPersist.cpp b/src/CacheAndPersist.cpp index b0eed10a..b9cabae8 100644 --- a/src/CacheAndPersist.cpp +++ b/src/CacheAndPersist.cpp @@ -24,6 +24,10 @@ CacheAndPersist::CacheAndPersist( const std::string &partitionPath, size_t maxPa PERSISTENCY_WORKSPACE } , mDecoderManifestFile( mPersistencyWorkspace + DECODER_MANIFEST_FILE ) , mCollectionSchemeListFile( mPersistencyWorkspace + COLLECTION_SCHEME_LIST_FILE ) +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + , mStateTemplateListFile( mPersistencyWorkspace + STATE_TEMPLATE_LIST_FILE ) + , mStateTemplateListMetadataFile( mPersistencyWorkspace + STATE_TEMPLATE_LIST_METADATA_FILE ) +#endif , mPayloadMetadataFile( mPersistencyWorkspace + PAYLOAD_METADATA_FILE ) , mCollectedDataPath( mPersistencyWorkspace + COLLECTED_DATA_FOLDER ) , mMaxPersistencePartitionSize( maxPartitionSize ) @@ -447,6 +451,12 @@ CacheAndPersist::getFileName( DataType dataType ) case DataType::EDGE_TO_CLOUD_PAYLOAD: return mCollectedDataPath; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + case DataType::STATE_TEMPLATE_LIST: + return mStateTemplateListFile; + case DataType::STATE_TEMPLATE_LIST_METADATA: + return mStateTemplateListMetadataFile; +#endif default: FWE_LOG_ERROR( "Invalid data type specified" ); @@ -532,6 +542,9 @@ CacheAndPersist::cleanupPersistedData() std::string filename = it->path().string(); if ( filename != mDecoderManifestFile && filename != mCollectionSchemeListFile && filename != mPayloadMetadataFile && +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + filename != mStateTemplateListFile && filename != mStateTemplateListMetadataFile && +#endif ( std::find( filenames.begin(), filenames.end(), filename ) == filenames.end() ) ) { // Delete files after iterating over directory diff --git a/src/CacheAndPersist.h b/src/CacheAndPersist.h index 5e6c2b79..8b0c7839 100644 --- a/src/CacheAndPersist.h +++ b/src/CacheAndPersist.h @@ -38,6 +38,10 @@ enum class DataType COLLECTION_SCHEME_LIST, DECODER_MANIFEST, DEFAULT_DATA_TYPE, +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + STATE_TEMPLATE_LIST, + STATE_TEMPLATE_LIST_METADATA, +#endif }; /** @@ -206,6 +210,10 @@ class CacheAndPersist // Define File names for the components using the lib static constexpr const char *DECODER_MANIFEST_FILE = "DecoderManifest.bin"; static constexpr const char *COLLECTION_SCHEME_LIST_FILE = "CollectionSchemeList.bin"; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + static constexpr const char *STATE_TEMPLATE_LIST_FILE = "StateTemplateList.bin"; + static constexpr const char *STATE_TEMPLATE_LIST_METADATA_FILE = "StateTemplateListMetadata.json"; +#endif static constexpr const char *PAYLOAD_METADATA_FILE = "PayloadMetadata.json"; // Folder to isolate persistency workspace static constexpr const char *PERSISTENCY_WORKSPACE = "FWE_Persistency/"; @@ -222,6 +230,10 @@ class CacheAndPersist std::string mPersistencyWorkspace; std::string mDecoderManifestFile; std::string mCollectionSchemeListFile; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + std::string mStateTemplateListFile; + std::string mStateTemplateListMetadataFile; +#endif std::string mPayloadMetadataFile; std::string mCollectedDataPath; std::uintmax_t mMaxPersistencePartitionSize; diff --git a/src/CanCommandDispatcher.cpp b/src/CanCommandDispatcher.cpp new file mode 100644 index 00000000..dd3320a0 --- /dev/null +++ b/src/CanCommandDispatcher.cpp @@ -0,0 +1,452 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CanCommandDispatcher.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "LoggingModule.h" +#include "Thread.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +CanCommandDispatcher::CanCommandDispatcher( const std::unordered_map &config, + std::string canInterfaceName, + std::shared_ptr rawBufferManager ) + : mConfig{ config } + , mCanInterfaceName{ std::move( canInterfaceName ) } + , mIoStream( mIoContext ) + , mRawBufferManager( std::move( rawBufferManager ) ) +{ +} + +CanCommandDispatcher::~CanCommandDispatcher() +{ + mIoContext.stop(); + if ( mThread.joinable() ) + { + mThread.join(); + } + close( mCanSocket ); +} + +std::vector +CanCommandDispatcher::getActuatorNames() +{ + std::vector actuatorNames; + for ( const auto &config : mConfig ) + { + actuatorNames.push_back( config.first ); + } + return actuatorNames; +} + +bool +CanCommandDispatcher::popString( const struct canfd_frame &canFrame, size_t &index, std::string &str ) +{ + // coverity[INFINITE_LOOP] False positive, loop can exit + while ( index < canFrame.len ) + { + if ( canFrame.data[index] == 0x00 ) + { + index++; + return true; + } + str += static_cast( canFrame.data[index] ); + index++; + } + return false; +} + +bool +CanCommandDispatcher::pushString( struct canfd_frame &canFrame, const std::string &str ) +{ + if ( ( canFrame.len + str.size() + 1 ) > CANFD_MAX_DLEN ) + { + return false; + } + memcpy( &canFrame.data[canFrame.len], str.data(), str.size() ); + canFrame.len = static_cast( canFrame.len + static_cast( str.size() ) ); + canFrame.data[canFrame.len] = 0x00; + canFrame.len++; + return true; +} + +bool +CanCommandDispatcher::pushArgument( struct canfd_frame &canFrame, const SignalValueWrapper &signalValue ) +{ + switch ( signalValue.type ) + { + case SignalType::BOOLEAN: { + uint8_t boolValU8 = static_cast( signalValue.value.boolVal ); + return pushNetworkByteOrder( canFrame, boolValU8 ); + } + case SignalType::UINT8: + return pushNetworkByteOrder( canFrame, signalValue.value.uint8Val ); + case SignalType::INT8: + return pushNetworkByteOrder( canFrame, signalValue.value.int8Val ); + case SignalType::UINT16: + return pushNetworkByteOrder( canFrame, signalValue.value.uint16Val ); + case SignalType::INT16: + return pushNetworkByteOrder( canFrame, signalValue.value.int16Val ); + case SignalType::UINT32: + return pushNetworkByteOrder( canFrame, signalValue.value.uint32Val ); + case SignalType::INT32: + return pushNetworkByteOrder( canFrame, signalValue.value.int32Val ); + case SignalType::UINT64: + return pushNetworkByteOrder( canFrame, signalValue.value.uint64Val ); + case SignalType::INT64: + return pushNetworkByteOrder( canFrame, signalValue.value.int64Val ); + case SignalType::FLOAT: + return pushNetworkByteOrder( canFrame, signalValue.value.floatVal ); + case SignalType::DOUBLE: + return pushNetworkByteOrder( canFrame, signalValue.value.doubleVal ); + case SignalType::STRING: { + auto loanedFrame = mRawBufferManager->borrowFrame( signalValue.value.rawDataVal.signalId, + signalValue.value.rawDataVal.handle ); + if ( loanedFrame.isNull() ) + { + return false; + } + std::string stringVal; + stringVal.assign( reinterpret_cast( loanedFrame.getData() ), loanedFrame.getSize() ); + return pushString( canFrame, stringVal ); + } + default: + FWE_LOG_ERROR( "Unsupported datatype " + std::to_string( static_cast( signalValue.type ) ) ); + return false; + } +} + +void +CanCommandDispatcher::handleCanFrameReception( const boost::system::error_code &error, size_t len ) +{ + static_cast( len ); + if ( error != boost::system::errc::success ) + { + FWE_LOG_ERROR( "Error reading from socket: " + error.message() ); + return; + } + struct canfd_frame receivedCanFrame; + size_t bytesTransferred = 0; + try + { + bytesTransferred = mIoStream.read_some( boost::asio::buffer( &receivedCanFrame, sizeof( receivedCanFrame ) ) ); + } + catch ( const std::exception &e ) + { + FWE_LOG_ERROR( "Error during read: " + std::string( e.what() ) ); + // Continue: setup reception, then return due to bytesTransferred != CANFD_MTU below + } + static_cast( setupReception() ); + if ( bytesTransferred != CANFD_MTU ) + { + return; + } + auto actuatorNameIt = mResponseIdToActuatorName.find( receivedCanFrame.can_id ); + if ( actuatorNameIt == mResponseIdToActuatorName.end() ) + { + return; + } + size_t index = 0; + CommandID receivedCommandId; + if ( !popString( receivedCanFrame, index, receivedCommandId ) ) + { + FWE_LOG_ERROR( "Could not pop null-terminated string for command id" ); + return; + } + uint8_t statusU8{}; + if ( !popNetworkByteOrder( receivedCanFrame, index, statusU8 ) ) + { + FWE_LOG_ERROR( "Could not pop status code" ); + return; + } + // coverity[autosar_cpp14_a7_2_1_violation] To reduce maintenance effort, cast directly to enum + CommandStatus status = static_cast( statusU8 ); + uint32_t reasonCode{}; + if ( !popNetworkByteOrder( receivedCanFrame, index, reasonCode ) ) + { + FWE_LOG_ERROR( "Could not pop reason code" ); + return; + } + std::string reasonDescription; + if ( !popString( receivedCanFrame, index, reasonDescription ) ) + { + FWE_LOG_ERROR( "Could not pop null-terminated string for reason description" ); + return; + } + std::lock_guard lock( mExecutionStateMutex ); + auto executionStateIt = mExecutionState.find( receivedCommandId ); + if ( executionStateIt == mExecutionState.end() ) + { + FWE_LOG_WARN( "Received response for actuator " + actuatorNameIt->second + " with command id " + + receivedCommandId + ", status " + commandStatusToString( status ) + ", reason code " + + std::to_string( reasonCode ) + ", reason description " + reasonDescription + + ", but command id is not active - possibly timed out" ); + return; + } + if ( receivedCanFrame.can_id != executionStateIt->second.canResponseId ) + { + FWE_LOG_WARN( "Received response for actuator " + executionStateIt->second.actuatorName + " with command id " + + receivedCommandId + ", status " + commandStatusToString( status ) + ", reason code " + + std::to_string( reasonCode ) + ", reason description " + reasonDescription + + ", but with wrong CAN id: " + std::to_string( receivedCanFrame.can_id ) + " vs " + + std::to_string( executionStateIt->second.canResponseId ) ); + return; + } + FWE_LOG_INFO( "Received response for actuator " + actuatorNameIt->second + " with command id " + receivedCommandId + + ", status " + commandStatusToString( status ) + ", reason code " + std::to_string( reasonCode ) + + ", reason description " + reasonDescription ); + executionStateIt->second.notifyStatusCallback( status, reasonCode, reasonDescription ); + if ( status != CommandStatus::IN_PROGRESS ) + { + mExecutionState.erase( receivedCommandId ); + } +} + +void +CanCommandDispatcher::handleTimeout( const boost::system::error_code &error, std::string commandId ) +{ + if ( error == boost::asio::error::operation_aborted ) + { + return; + } + std::lock_guard lock( mExecutionStateMutex ); + auto executionStateIt = mExecutionState.find( commandId ); + if ( executionStateIt == mExecutionState.end() ) + { + FWE_LOG_ERROR( "Could not find execution state for command id " + commandId ); + return; + } + FWE_LOG_WARN( "Execution timeout for actuator " + executionStateIt->second.actuatorName + " with command id " + + commandId ); + executionStateIt->second.notifyStatusCallback( CommandStatus::EXECUTION_TIMEOUT, REASON_CODE_NO_RESPONSE, "" ); + mExecutionState.erase( commandId ); +} + +bool +CanCommandDispatcher::setupReception() +{ + try + { + mIoStream.async_read_some( + boost::asio::null_buffers(), + std::bind( + &CanCommandDispatcher::handleCanFrameReception, this, std::placeholders::_1, std::placeholders::_2 ) ); + } + catch ( const std::exception &e ) + { + FWE_LOG_ERROR( "Error starting async read: " + std::string( e.what() ) ); + return false; + } + return true; +} + +void +CanCommandDispatcher::setupTimeout( boost::asio::steady_timer &timer, std::string commandId, Timestamp timeoutMs ) +{ + try + { + timer.expires_after( std::chrono::milliseconds( timeoutMs ) ); + timer.async_wait( std::bind( &CanCommandDispatcher::handleTimeout, this, std::placeholders::_1, commandId ) ); + } + catch ( const std::exception &e ) + { + FWE_LOG_ERROR( "Error starting async timer: " + std::string( e.what() ) ); + return; + } +} + +bool +CanCommandDispatcher::init() +{ + for ( const auto &commandConfig : mConfig ) + { + mResponseIdToActuatorName.emplace( commandConfig.second.canResponseId, commandConfig.first ); + } + if ( !setupCan() ) + { + return false; + } + try + { + mIoStream.assign( mCanSocket ); + } + catch ( const std::exception &e ) + { + FWE_LOG_ERROR( "Error setting socket fd: " + std::string( e.what() ) ); + return false; + } + if ( !setupReception() ) + { + return false; + } + + // Start event loop thread: + mThread = std::thread( [this]() { + Thread::setCurrentThreadName( "CanCmdDsp" ); + // Prevent the io context loop from exiting when it runs out of work + auto workGuard = boost::asio::make_work_guard( mIoContext ); + mIoContext.run(); + } ); + FWE_LOG_INFO( "Successfully initialized CAN command interface on " + mCanInterfaceName ); + return true; +} + +void +CanCommandDispatcher::setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) +{ + auto configIt = mConfig.find( actuatorName ); + if ( configIt == mConfig.end() ) + { + FWE_LOG_WARN( "Actuator not supported: " + actuatorName + ", command id " + commandId ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_NOT_SUPPORTED, "" ); + return; + } + if ( configIt->second.signalType != signalValue.type ) + { + FWE_LOG_WARN( "Argument type mismatch for " + actuatorName + " and command id " + commandId ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_ARGUMENT_TYPE_MISMATCH, "" ); + return; + } + Timestamp remainingTimeoutMs = 0; + if ( executionTimeoutMs > 0 ) + { + auto currentTimeMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + if ( ( issuedTimestampMs + executionTimeoutMs ) <= currentTimeMs ) + { + FWE_LOG_ERROR( "Command Request with ID " + commandId + " timed out" ); + notifyStatusCallback( CommandStatus::EXECUTION_TIMEOUT, REASON_CODE_TIMED_OUT_BEFORE_DISPATCH, "" ); + return; + } + remainingTimeoutMs = issuedTimestampMs + executionTimeoutMs - currentTimeMs; + } + std::lock_guard lock( mExecutionStateMutex ); + auto emplaceResult = mExecutionState.emplace( commandId, + ExecutionState{ configIt->first, + configIt->second.canResponseId, + notifyStatusCallback, + boost::asio::steady_timer( mIoContext ) } ); + if ( !emplaceResult.second ) + { + FWE_LOG_WARN( "Ignoring duplicate command id " + commandId + " for actuator " + configIt->first ); + return; + } + auto &executionState = emplaceResult.first->second; + // Send CAN request message: + executionState.sendCanFrame.can_id = configIt->second.canRequestId; + if ( ( !pushString( executionState.sendCanFrame, commandId ) ) || + ( !pushNetworkByteOrder( executionState.sendCanFrame, issuedTimestampMs ) ) || + ( !pushNetworkByteOrder( executionState.sendCanFrame, executionTimeoutMs ) ) || + ( !pushArgument( executionState.sendCanFrame, signalValue ) ) ) + { + FWE_LOG_ERROR( "Error pushing data for " + actuatorName + " and command id " + commandId ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, "" ); + mExecutionState.erase( commandId ); + return; + } + FWE_LOG_INFO( "Sending request for actuator " + actuatorName + " and command id " + commandId ); + auto handleSendComplete = [this, commandId]( const boost::system::error_code &error, size_t bytesTransferred ) { + std::lock_guard writeLock( mExecutionStateMutex ); + auto executionStateIt = mExecutionState.find( commandId ); + if ( executionStateIt == mExecutionState.end() ) + { + FWE_LOG_WARN( "Write to socket completed after timeout for command id " + commandId ); + return; + } + if ( error != boost::system::errc::success ) + { + FWE_LOG_ERROR( "Error writing to socket: " + error.message() ); + executionStateIt->second.notifyStatusCallback( + CommandStatus::EXECUTION_FAILED, REASON_CODE_WRITE_FAILED, "Writing to CAN socket failed" ); + mExecutionState.erase( commandId ); + return; + } + if ( bytesTransferred != CANFD_MTU ) + { + FWE_LOG_ERROR( "Unexpected number of bytes transferred: " + std::to_string( bytesTransferred ) ); + executionStateIt->second.notifyStatusCallback( + CommandStatus::EXECUTION_FAILED, REASON_CODE_WRITE_FAILED, "Writing to CAN socket failed" ); + mExecutionState.erase( commandId ); + return; + } + FWE_LOG_INFO( "Request sent for actuator " + executionStateIt->second.actuatorName + " and command id " + + commandId ); + }; + try + { + mIoStream.async_write_some( + boost::asio::buffer( &executionState.sendCanFrame, sizeof( executionState.sendCanFrame ) ), + handleSendComplete ); + } + catch ( const std::exception &e ) + { + FWE_LOG_ERROR( "Error starting async write: " + std::string( e.what() ) ); + notifyStatusCallback( + CommandStatus::EXECUTION_FAILED, REASON_CODE_WRITE_FAILED, "Writing to CAN socket failed" ); + mExecutionState.erase( commandId ); + return; + } + + if ( remainingTimeoutMs > 0 ) + { + setupTimeout( executionState.timer, commandId, remainingTimeoutMs ); + } +} + +bool +CanCommandDispatcher::setupCan() +{ + // Setup CAN-FD socket: + mCanSocket = socket( PF_CAN, SOCK_RAW | SOCK_NONBLOCK, CAN_RAW ); + if ( mCanSocket < 0 ) + { + FWE_LOG_ERROR( "Failed to create socket: " + getErrnoString() ); + return false; + } + int canfdOn = 1; + if ( setsockopt( mCanSocket, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &canfdOn, sizeof( canfdOn ) ) != 0 ) + { + FWE_LOG_ERROR( "setsockopt CAN_RAW_FD_FRAMES FAILED" ); + close( mCanSocket ); + return false; + } + auto interfaceIndex = if_nametoindex( mCanInterfaceName.c_str() ); + if ( interfaceIndex == 0 ) + { + FWE_LOG_ERROR( "CAN Interface with name " + mCanInterfaceName + " is not accessible" ); + close( mCanSocket ); + return false; + } + struct sockaddr_can interfaceAddress = {}; + interfaceAddress.can_family = AF_CAN; + interfaceAddress.can_ifindex = static_cast( interfaceIndex ); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) + if ( bind( mCanSocket, (struct sockaddr *)&interfaceAddress, sizeof( interfaceAddress ) ) < 0 ) + { + FWE_LOG_ERROR( "Failed to bind socket: " + getErrnoString() ); + close( mCanSocket ); + return false; + } + return true; +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CanCommandDispatcher.h b/src/CanCommandDispatcher.h new file mode 100644 index 00000000..578837d7 --- /dev/null +++ b/src/CanCommandDispatcher.h @@ -0,0 +1,213 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +// clang-format off +/** + * This class implements interface `ICommandDispatcher`. It's a class for dispatching commands + * onto CAN, with one CAN request message ID and one CAN response message ID. + * + * The command CAN request payload is formed from the null-terminated command ID string, a uint64_t + * issued timestamp in ms since epoch, a uint64_t relative execution timeout in ms since the issued + * timestamp, and one actuator argument serialized in network byte order. A relative timeout value + * of zero means no timeout. + * Example with command ID "01J3N9DAVV10AA83PZJX561HPS", issued timestamp of 1723134069000, relative + * timeout of 1000, and actuator datatype int32_t with value 1234567890: + * |----------------------------------------|----------------------------------------|---------------------------------|---------------------------------|---------------------------| + * | Payload byte: | 0 | 1 | ... | 24 | 25 | 26 | 27 | 28 | ... | 33 | 34 | 35 | 36 | ... | 41 | 42 | 43 | 44 | 45 | 46 | + * |----------------------------------------|----------------------------------------|---------------------------------|---------------------------------|---------------------------| + * | Value: | 0x30 | 0x31 | ... | 0x50 | 0x53 | 0x00 | 0x00 | 0x00 | ... | 0x49 | 0x08 | 0x00 | 0x00 | ... | 0x03 | 0xE8 | 0x49 | 0x96 | 0x02 | 0xD2 | + * |----------------------------------------|----------------------------------------|---------------------------------|---------------------------------|---------------------------| + * Command ID (null terminated string)-------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * Issued timestamp (uint64_t network byte order)-------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * Execution timeout (uint64_t network byte order)----------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * Request argument (int32_t network byte order)----------------------------------------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * + * + * The command CAN response payload is formed from the null-terminated command ID string, a 1-byte + * command status code, a 4-byte uint32_t reason code, and a null-terminated reason description + * string. The values of the status code correspond with the enum `CommandStatus` + * Example with command ID "01J3N9DAVV10AA83PZJX561HPS", response status + * `CommandStatus::EXECUTION_FAILED`, reason code 0x0001ABCD, and reason description "hello": + * |----------------------------------------|----------------------------------------|------|---------------------------|-----------------------------------------| + * | Payload byte: | 0 | 1 | ... | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | + * |----------------------------------------|----------------------------------------|------|---------------------------|-----------------------------------------| + * | Value: | 0x30 | 0x31 | ... | 0x50 | 0x53 | 0x00 | 0x03 | 0x00 | 0x01 | 0xAB | 0xCD | 0x68 | 0x65 | 0x6C | 0x6C | 0x6F | 0x00 | + * |----------------------------------------|----------------------------------------|------|---------------------------|-----------------------------------------| + * Command ID (null terminated string)-------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * Command status (enum CommandStatus)------------------------------------------------^^^^^^ + * Reason code (uint32_t network byte order)-------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * Reason description (null terminated string)---------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * + * When a command is dispatched via the `setActuatorValue` interface: + * 1. The module will store the `commandId` and `notifyStatusCallback`, and then send the CAN + * request message. If the execution timeout is greater than zero, a timeout timer is started + * that expires at `issuedTimestampMs + executionTimeoutMs`. + * 2. When a response is received with the matching `commandID` before the timeout timer expires, + * the `notifyStatusCallback` will be called with the status, reason code, and reason + * description string. If the command status code is `CommandStatus::IN_PROGRESS` the command + * will not be considered completed, otherwise it will be considered completed. + * 3. If no response is received before the timeout timer expires, the `notifyStatusCallback` will + * be called with `EXECUTION_TIMEOUT` and the command is considered completed. + * 4. If a response is received after a command has completed, it is ignored. + * + * Note: it is possible to dispatch multiple commands for the same or different actuators + * concurrently. + */ +// clang-format on +class CanCommandDispatcher : public ICommandDispatcher +{ +public: + struct CommandConfig + { + unsigned canRequestId; + unsigned canResponseId; + SignalType signalType; + }; + + CanCommandDispatcher( const std::unordered_map &config, + std::string canInterfaceName, + std::shared_ptr rawBufferManager ); + + ~CanCommandDispatcher() override; + + CanCommandDispatcher( const CanCommandDispatcher & ) = delete; + CanCommandDispatcher &operator=( const CanCommandDispatcher & ) = delete; + CanCommandDispatcher( CanCommandDispatcher && ) = delete; + CanCommandDispatcher &operator=( CanCommandDispatcher && ) = delete; + + /** + * @brief Initializer command dispatcher with its associated underlying vehicle network / service + * @return True if successful. False otherwise. + */ + bool init() override; + + /** + * @brief set actuator value + * @param actuatorName Actuator name + * @param signalValue Signal value + * @param commandId Command ID + * @param issuedTimestampMs Timestamp of when the command was issued in the cloud in ms since + * epoch. + * @param executionTimeoutMs Relative execution timeout in ms since `issuedTimestampMs`. A value + * of zero means no timeout. + * @param notifyStatusCallback Callback to notify command status + */ + void setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) override; + + /** + * @brief Gets the actuator names supported by the command dispatcher + * @todo The decoder manifest doesn't yet have an indication of whether a signal is + * READ/WRITE/READ_WRITE. Until it does this interface is needed to get the names of the + * actuators supported by the command dispatcher, so that for string signals, buffers can be + * pre-allocated in the RawDataManager by the CollectionSchemeManager when a new decoder + * manifest arrives. When the READ/WRITE/READ_WRITE usage of a signal is available this + * interface can be removed. + * @return List of actuator names + */ + std::vector getActuatorNames() override; + +private: + /** + * @brief map of supported actuators + */ + const std::unordered_map &mConfig; + std::string mCanInterfaceName; + std::unordered_map mResponseIdToActuatorName; + + struct ExecutionState + { + ExecutionState( const std::string &actuatorNameIn, + unsigned canResponseIdIn, + NotifyCommandStatusCallback notifyStatusCallbackIn, + boost::asio::steady_timer timerIn ) + : actuatorName( actuatorNameIn ) + , canResponseId( canResponseIdIn ) + , notifyStatusCallback( std::move( notifyStatusCallbackIn ) ) + , timer( std::move( timerIn ) ) + { + } + const std::string &actuatorName; + unsigned canResponseId; + NotifyCommandStatusCallback notifyStatusCallback; + boost::asio::steady_timer timer; + struct canfd_frame sendCanFrame = {}; + }; + std::unordered_map mExecutionState; + std::mutex mExecutionStateMutex; + + boost::asio::io_context mIoContext; + boost::asio::posix::basic_stream_descriptor<> mIoStream; + int mCanSocket{ -1 }; + std::thread mThread; + std::shared_ptr mRawBufferManager; + + bool setupCan(); + bool setupReception(); + void setupTimeout( boost::asio::steady_timer &timer, std::string commandId, Timestamp timeoutMs ); + void handleCanFrameReception( const boost::system::error_code &error, size_t len ); + void handleTimeout( const boost::system::error_code &error, std::string commandId ); + static bool popString( const struct canfd_frame &canFrame, size_t &index, std::string &str ); + static bool pushString( struct canfd_frame &canFrame, const std::string &str ); + + template + static bool + popNetworkByteOrder( const struct canfd_frame &canFrame, size_t &index, T &value ) + { + if ( ( index + sizeof( T ) ) > canFrame.len ) + { + return false; + } + value = boost::endian::endian_load( &canFrame.data[index] ); + index += sizeof( T ); + return true; + } + + template + static bool + pushNetworkByteOrder( struct canfd_frame &canFrame, const T &value ) + { + if ( ( canFrame.len + sizeof( T ) ) > CANFD_MAX_DLEN ) + { + return false; + } + boost::endian::endian_store( &canFrame.data[canFrame.len], value ); + canFrame.len = static_cast( canFrame.len + static_cast( sizeof( T ) ) ); + return true; + } + + bool pushArgument( struct canfd_frame &canFrame, const SignalValueWrapper &signalValue ); +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CollectionInspectionAPITypes.h b/src/CollectionInspectionAPITypes.h index 314f529e..bd15881f 100644 --- a/src/CollectionInspectionAPITypes.h +++ b/src/CollectionInspectionAPITypes.h @@ -24,6 +24,7 @@ namespace IoTFleetWise struct ExpressionNode; static constexpr uint32_t MAX_NUMBER_OF_ACTIVE_CONDITION = 256; /**< More active conditions will be ignored */ +static constexpr uint32_t MAX_NUMBER_OF_ACTIVE_FETCH_CONDITION = 256; static constexpr uint32_t ALL_CONDITIONS = 0xFFFFFFFF; static constexpr uint32_t MAX_EQUATION_DEPTH = 10; /**< If the AST of the expression is deeper than this value the equation is not accepted */ @@ -43,6 +44,9 @@ struct PassThroughMetadata uint32_t priority{ 0 }; SyncID decoderID; SyncID collectionSchemeID; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + std::string campaignArn; +#endif }; // As a start these structs are mainly a copy of the data defined in ICollectionScheme but as plain old data structures @@ -56,6 +60,9 @@ struct InspectionMatrixSignalCollectionInfo * of samples in the buffer only necessary for condition evaluation */ SignalType signalType{ SignalType::UNKNOWN }; + std::vector fetchRequestIDs; /**< contains all fetch request IDs associated with the signal + * for the given campaign + */ }; struct InspectionMatrixCanFrameCollectionInfo @@ -66,6 +73,22 @@ struct InspectionMatrixCanFrameCollectionInfo uint32_t minimumSampleIntervalMs; /**< 0 which all frames are recording as seen on the bus */ }; +struct ConditionForFetch +{ + const ExpressionNode *condition; /**< points into InspectionMatrix.expressionNodes; + * Raw pointer is used as needed for efficient AST and ConditionWithCollectedData + * never exists without the relevant InspectionMatrix */ + bool triggerOnlyOnRisingEdge; + FetchRequestID fetchRequestID; +}; + +struct ConditionForForward +{ + const ExpressionNode *condition; /**< points into InspectionMatrix.expressionNodes; + * Raw pointer is used as needed for efficient AST and ConditionWithCollectedData + * never exists without the relevant InspectionMatrix */ +}; + struct ConditionWithCollectedData { const ExpressionNode *condition = @@ -79,8 +102,14 @@ struct ConditionWithCollectedData bool includeActiveDtcs{}; bool triggerOnlyOnRisingEdge{}; PassThroughMetadata metadata; + // Conditions to check for custom fetch configuration + std::vector fetchConditions; bool isStaticCondition{ true }; bool alwaysEvaluateCondition{ false }; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // Conditions to check for forwarding the stored data + std::vector forwardConditions; +#endif }; struct InspectionMatrix @@ -95,20 +124,53 @@ struct InspectionMatrix struct InspectionValue { InspectionValue() = default; + InspectionValue( bool val ) + { + boolVal = val; + type = DataType::BOOL; + } + InspectionValue( double val ) + { + doubleVal = val; + type = DataType::DOUBLE; + } + InspectionValue( int val ) + : InspectionValue( static_cast( val ) ) + { + } + InspectionValue( std::string val ) + { + if ( stringVal == nullptr ) + { + stringVal = std::make_unique( std::move( val ) ); + } + else + { + *stringVal = std::move( val ); + } + type = DataType::STRING; + } + InspectionValue( const char *val ) + : InspectionValue( std::string( val ) ) + { + } InspectionValue( const InspectionValue & ) = delete; InspectionValue &operator=( const InspectionValue & ) = delete; InspectionValue( InspectionValue && ) = default; - InspectionValue &operator=( InspectionValue && ) = delete; + InspectionValue &operator=( InspectionValue && ) = default; ~InspectionValue() = default; enum class DataType { UNDEFINED, BOOL, DOUBLE, + STRING }; DataType type = DataType::UNDEFINED; bool boolVal{}; double doubleVal{}; + std::unique_ptr stringVal; + SignalID signalID{ INVALID_SIGNAL_ID }; InspectionValue & operator=( bool val ) { @@ -129,11 +191,41 @@ struct InspectionValue *this = static_cast( val ); return *this; } + InspectionValue & + operator=( std::string val ) + { + if ( stringVal == nullptr ) + { + stringVal = std::make_unique( std::move( val ) ); + } + else + { + *stringVal = std::move( val ); + } + type = DataType::STRING; + return *this; + } + InspectionValue & + operator=( const char *val ) + { + *this = std::string( val ); + return *this; + } + bool + isUndefined() const + { + return type == DataType::UNDEFINED; + } bool isBoolOrDouble() const { return ( type == DataType::BOOL ) || ( type == DataType::DOUBLE ); } + bool + isString() const + { + return type == DataType::STRING; + } double asDouble() const { @@ -347,13 +439,19 @@ struct CollectedSignal SignalID signalID{ INVALID_SIGNAL_ID }; Timestamp receiveTime{ 0 }; SignalValueWrapper value; + FetchRequestID fetchRequestID{ DEFAULT_FETCH_REQUEST_ID }; CollectedSignal() = default; template - CollectedSignal( SignalID signalIDIn, Timestamp receiveTimeIn, T sigValue, SignalType sigType ) + CollectedSignal( SignalID signalIDIn, + Timestamp receiveTimeIn, + T sigValue, + SignalType sigType, + FetchRequestID fetchRequestIDIn = DEFAULT_FETCH_REQUEST_ID ) : signalID( signalIDIn ) , receiveTime( receiveTimeIn ) + , fetchRequestID( fetchRequestIDIn ) { switch ( sigType ) { @@ -390,6 +488,10 @@ struct CollectedSignal case SignalType::BOOLEAN: value.setVal( static_cast( sigValue ), sigType ); break; + case SignalType::STRING: + // Handles raw buffer handle in the background + value.setVal( static_cast( sigValue ), sigType ); + break; case SignalType::UNKNOWN: // Signal of type UNKNOWN will not be collected break; @@ -405,7 +507,8 @@ struct CollectedSignal fromDecodedSignal( SignalID signalIDIn, Timestamp receiveTimeIn, const DecodedSignalValue &decodedSignalValue, - SignalType signalType ) + SignalType signalType, + FetchRequestID fetchRequestIDIn = DEFAULT_FETCH_REQUEST_ID ) { switch ( decodedSignalValue.signalType ) { @@ -414,7 +517,8 @@ struct CollectedSignal case SignalType::INT64: return CollectedSignal{ signalIDIn, receiveTimeIn, decodedSignalValue.signalValue.int64Val, signalType }; default: - return CollectedSignal{ signalIDIn, receiveTimeIn, decodedSignalValue.signalValue.doubleVal, signalType }; + return CollectedSignal{ + signalIDIn, receiveTimeIn, decodedSignalValue.signalValue.doubleVal, signalType, fetchRequestIDIn }; } } @@ -532,13 +636,41 @@ struct CollectionInspectionEngineOutput enum class ExpressionErrorCode { SUCCESSFUL, - SIGNAL_NOT_FOUND, - FUNCTION_DATA_NOT_AVAILABLE, STACK_DEPTH_REACHED, NOT_IMPLEMENTED_TYPE, NOT_IMPLEMENTED_FUNCTION, TYPE_MISMATCH }; +using CustomFunctionInvocationID = uint32_t; +struct CustomFunctionInvokeResult +{ + CustomFunctionInvokeResult( ExpressionErrorCode e ) + : error( e ) + { + } + CustomFunctionInvokeResult( ExpressionErrorCode e, InspectionValue v ) + : error( e ) + , value( std::move( v ) ) + { + } + ExpressionErrorCode error; + InspectionValue value; +}; +using CustomFunctionInvokeCallback = + std::function &args )>; +using CustomFunctionConditionEndCallback = std::function &collectedSignalIds, + Timestamp timestamp, + CollectionInspectionEngineOutput &output )>; +using CustomFunctionCleanupCallback = std::function; + +struct CustomFunctionCallbacks +{ + CustomFunctionInvokeCallback invokeCallback; + CustomFunctionConditionEndCallback conditionEndCallback; + CustomFunctionCleanupCallback cleanupCallback; +}; + } // namespace IoTFleetWise } // namespace Aws diff --git a/src/CollectionInspectionEngine.cpp b/src/CollectionInspectionEngine.cpp index 16c6ce66..545c78f3 100644 --- a/src/CollectionInspectionEngine.cpp +++ b/src/CollectionInspectionEngine.cpp @@ -6,23 +6,29 @@ #include #include +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include +#endif + namespace Aws { namespace IoTFleetWise { -CollectionInspectionEngine::CollectionInspectionEngine( bool sendDataOnlyOncePerCondition ) - : mSendDataOnlyOncePerCondition( sendDataOnlyOncePerCondition ) +CollectionInspectionEngine::CollectionInspectionEngine( uint32_t minFetchTriggerIntervalMs, + bool sendDataOnlyOncePerCondition ) + : mMinFetchTriggerIntervalMs( minFetchTriggerIntervalMs ) + , mSendDataOnlyOncePerCondition( sendDataOnlyOncePerCondition ) { setActiveDTCsConsumed( ALL_CONDITIONS, false ); } template void -CollectionInspectionEngine::addSignalBuffer( const InspectionMatrixSignalCollectionInfo &signal ) +CollectionInspectionEngine::addSignalBuffer( const InspectionMatrixSignalCollectionInfo &signal, + SignalBufferConditionID signalBufferConditionIndex ) { - auto signalHistoryBufferVectorPtr = getSignalHistoryBuffersPtr( signal.signalID ); - + auto signalHistoryBufferVectorPtr = getSignalHistoryBuffersPtr( signal.signalID, signalBufferConditionIndex ); if ( signalHistoryBufferVectorPtr == nullptr ) { return; @@ -39,15 +45,41 @@ CollectionInspectionEngine::addSignalBuffer( const InspectionMatrixSignalCollect } } signalHistoryBufferVector.emplace_back( signal.sampleBufferSize, - signal.minimumSampleIntervalMs + signal.minimumSampleIntervalMs, + ( signal.signalType == SignalType::STRING ) #ifdef FWE_FEATURE_VISION_SYSTEM_DATA - , - signal.signalType == SignalType::COMPLEX_SIGNAL + || ( signal.signalType == SignalType::COMPLEX_SIGNAL ) #endif + ); signalHistoryBufferVector.back().addFixedWindow( signal.fixedWindowPeriod ); } +void +CollectionInspectionEngine::cleanupCustomFunctions( const ExpressionNode *expression ) +{ + if ( expression == nullptr ) + { + return; + } + cleanupCustomFunctions( expression->left ); + cleanupCustomFunctions( expression->right ); + for ( const auto ¶m : expression->function.customFunctionParams ) + { + cleanupCustomFunctions( param ); + } + if ( expression->function.customFunctionName.empty() ) + { + return; + } + auto customFunction = mCustomFunctionCallbacks.find( expression->function.customFunctionName ); + if ( ( customFunction == mCustomFunctionCallbacks.end() ) || ( !customFunction->second.cleanupCallback ) ) + { + return; + } + customFunction->second.cleanupCallback( expression->function.customFunctionInvocationId ); +} + void CollectionInspectionEngine::onChangeInspectionMatrix( const std::shared_ptr &inspectionMatrix, const TimePoint ¤tTime ) @@ -55,11 +87,12 @@ CollectionInspectionEngine::onChangeInspectionMatrix( const std::shared_ptrconditions ) { // Check if we can add an additional condition to mConditions @@ -81,6 +114,10 @@ CollectionInspectionEngine::onChangeInspectionMatrix( const std::shared_ptr( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::INT8: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::UINT16: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::INT16: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::UINT32: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::INT32: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::UINT64: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::INT64: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::FLOAT: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::DOUBLE: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::BOOLEAN: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); + break; + case SignalType::STRING: + addSignalBuffer( signal, signalBufferConditionIndex ); break; case SignalType::UNKNOWN: // Signal of UNKNOWN type should not be processed break; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA case SignalType::COMPLEX_SIGNAL: - addSignalBuffer( signal ); + addSignalBuffer( signal, signalBufferConditionIndex ); break; #endif } @@ -169,6 +222,7 @@ CollectionInspectionEngine::onChangeInspectionMatrix( const std::shared_ptr( signal, activeCondition, conditionIndex ); break; + case SignalType::STRING: + updateConditionBuffer( signal, activeCondition, conditionIndex ); case SignalType::UNKNOWN: // Signal of type UNKNOWN should not be processed break; @@ -251,7 +307,15 @@ CollectionInspectionEngine::updateConditionBuffer( const long unsigned int conditionIndex ) { SignalID signalID = inspectionMatrixCollectionInfo.signalID; - auto buf = getSignalHistoryBufferPtr( signalID, inspectionMatrixCollectionInfo.minimumSampleIntervalMs ); + // Use a default index if fetch strategy wasn't specified or use the first associated id. + // All fetch ids in this struct are mapped to the same signalBufferConditionIndex + auto signalBufferConditionIndex = + inspectionMatrixCollectionInfo.fetchRequestIDs.empty() + ? mFetchRequestToConditionIndexMap[DEFAULT_FETCH_REQUEST_ID] + : mFetchRequestToConditionIndexMap[inspectionMatrixCollectionInfo.fetchRequestIDs[0]]; + + auto buf = getSignalHistoryBufferPtr( + signalID, signalBufferConditionIndex, inspectionMatrixCollectionInfo.minimumSampleIntervalMs ); if ( buf != nullptr ) { buf->mConditionsThatEvaluateOnThisSignal.set( conditionIndex ); @@ -268,9 +332,11 @@ CollectionInspectionEngine::updateConditionBuffer( template bool -CollectionInspectionEngine::allocateBufferVector( SignalID signalID, size_t &usedBytes ) +CollectionInspectionEngine::allocateBufferVector( SignalID signalID, + SignalBufferConditionID signalBufferConditionIndex, + size_t &usedBytes ) { - auto signalHistoryBufferVectorPtr = getSignalHistoryBuffersPtr( signalID ); + auto signalHistoryBufferVectorPtr = getSignalHistoryBuffersPtr( signalID, signalBufferConditionIndex ); if ( signalHistoryBufferVectorPtr != nullptr ) { auto &signalHistoryBufferVector = *signalHistoryBufferVectorPtr; @@ -303,91 +369,105 @@ CollectionInspectionEngine::preAllocateBuffers() size_t usedBytes = 0; // Allocate Signal Buffer - for ( auto &bufferVector : mSignalBuffers ) + for ( auto &bufferVectorOuter : mSignalBuffers ) { - auto signalID = bufferVector.first; - if ( mSignalToBufferTypeMap.find( signalID ) != mSignalToBufferTypeMap.end() ) + auto signalBufferConditionIndex = bufferVectorOuter.first; + for ( auto &bufferVector : bufferVectorOuter.second ) { - auto signalType = mSignalToBufferTypeMap[signalID]; - switch ( signalType ) + auto signalID = bufferVector.first; + if ( mSignalToBufferTypeMap.find( signalID ) != mSignalToBufferTypeMap.end() ) { - case SignalType::UINT8: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::INT8: - if ( !allocateBufferVector( signalID, usedBytes ) ) + auto signalType = mSignalToBufferTypeMap[signalID]; + // coverity[autosar_cpp14_m6_4_6_violation] + // coverity[misra_cpp_2008_rule_6_4_6_violation] compiler warning is preferred over a default-clause + switch ( signalType ) { - return false; - } - break; - case SignalType::UINT16: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::INT16: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::UINT32: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::INT32: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::UINT64: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::INT64: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::FLOAT: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::DOUBLE: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; - case SignalType::BOOLEAN: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; + case SignalType::UINT8: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::INT8: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::UINT16: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::INT16: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::UINT32: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::INT32: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::UINT64: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::INT64: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::FLOAT: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::DOUBLE: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::BOOLEAN: + if ( !allocateBufferVector( signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::STRING: + if ( !allocateBufferVector( + signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; + case SignalType::UNKNOWN: + // Signal of type UNKNOWN should not be processed; + break; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA - case SignalType::COMPLEX_SIGNAL: - if ( !allocateBufferVector( signalID, usedBytes ) ) - { - return false; - } - break; + case SignalType::COMPLEX_SIGNAL: + if ( !allocateBufferVector( + signalID, signalBufferConditionIndex, usedBytes ) ) + { + return false; + } + break; #endif - default: - FWE_LOG_WARN( "Unknown type :" + std::to_string( static_cast( signalType ) ) ); - break; + } } } } @@ -420,28 +500,44 @@ CollectionInspectionEngine::clear() mSignalToBufferTypeMap.clear(); mCanFrameBuffers.clear(); mConditions.clear(); + mFetchRequestToConditionIndexMap.clear(); mNextConditionToCollectedIndex = 0; mNextWindowFunctionTimesOut = 0; mConditionsWithInputSignalChanged.reset(); - mConditionsWithConditionCurrentlyTrue.reset(); + // Default all conditions to true to force rising edge logic + mConditionsWithConditionCurrentlyTrue.set(); mConditionsTriggeredWaitingPublished.reset(); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + mForwardConditionCurrentlyTrueForCampaignPartitions.clear(); +#endif if ( mRawBufferManager != nullptr ) { mRawBufferManager->resetUsageHintsForStage( RawData::BufferHandleUsageStage::COLLECTION_INSPECTION_ENGINE_HISTORY_BUFFER ); } + // Cleanup the custom functions for the last campaigns: + if ( mActiveInspectionMatrix ) + { + for ( const auto &condition : mActiveInspectionMatrix->conditions ) + { + cleanupCustomFunctions( condition.condition ); + } + } } template void -CollectionInspectionEngine::updateBufferFixedWindowFunction( SignalID signalID, Timestamp timestamp ) +CollectionInspectionEngine::updateBufferFixedWindowFunction( SignalID signalID, + SignalBufferConditionID signalBufferConditionIndex, + Timestamp timestamp ) { std::vector> *signalHistoryBufferPtr = nullptr; try { - if ( mSignalBuffers.find( signalID ) != mSignalBuffers.end() ) + auto outerMapIt = mSignalBuffers.find( signalBufferConditionIndex ); + if ( outerMapIt != mSignalBuffers.end() && outerMapIt->second.find( signalID ) != outerMapIt->second.end() ) { - auto &mapVal = mSignalBuffers.at( signalID ); + auto &mapVal = outerMapIt->second[signalID]; signalHistoryBufferPtr = boost::get>>( &mapVal ); } } @@ -471,58 +567,65 @@ void CollectionInspectionEngine::updateAllFixedWindowFunctions( Timestamp timestamp ) { mNextWindowFunctionTimesOut = std::numeric_limits::max(); - for ( auto &signalVector : mSignalBuffers ) + for ( auto &signalVectorOuter : mSignalBuffers ) { - auto signalID = signalVector.first; - if ( mSignalToBufferTypeMap.find( signalID ) != mSignalToBufferTypeMap.end() ) + auto signalBufferConditionIndex = signalVectorOuter.first; + for ( auto &signalVector : signalVectorOuter.second ) { - auto signalType = mSignalToBufferTypeMap[signalID]; - // coverity[autosar_cpp14_m6_4_6_violation] - // coverity[misra_cpp_2008_rule_6_4_6_violation] compiler warning is preferred over a default-clause - switch ( signalType ) + auto signalID = signalVector.first; + if ( mSignalToBufferTypeMap.find( signalID ) != mSignalToBufferTypeMap.end() ) { - case SignalType::UINT8: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::INT8: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::UINT16: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::INT16: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::UINT32: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::INT32: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::UINT64: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::INT64: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::FLOAT: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::DOUBLE: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::BOOLEAN: - updateBufferFixedWindowFunction( signalID, timestamp ); - break; - case SignalType::UNKNOWN: - FWE_LOG_WARN( "Window functions are not supported for signal ID: " + std::to_string( signalID ) + - " as it is of type UNKNOWN" ); - break; + auto signalType = mSignalToBufferTypeMap[signalID]; + // coverity[autosar_cpp14_m6_4_6_violation] + // coverity[misra_cpp_2008_rule_6_4_6_violation] compiler warning is preferred over a default-clause + switch ( signalType ) + { + case SignalType::UINT8: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::INT8: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::UINT16: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::INT16: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::UINT32: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::INT32: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::UINT64: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::INT64: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::FLOAT: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::DOUBLE: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::BOOLEAN: + updateBufferFixedWindowFunction( signalID, signalBufferConditionIndex, timestamp ); + break; + case SignalType::STRING: + // Window functions are not supported for string signals + break; + case SignalType::UNKNOWN: + FWE_LOG_WARN( "Window functions are not supported for signal ID: " + std::to_string( signalID ) + + " as it is of type UNKNOWN" ); + break; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA - case SignalType::COMPLEX_SIGNAL: - // Window functions are not supported for complex signals - break; + case SignalType::COMPLEX_SIGNAL: + // Window functions are not supported for complex signals + break; #endif + } } } } @@ -535,8 +638,7 @@ CollectionInspectionEngine::evaluateStaticCondition( uint32_t conditionIndex ) InspectionValue result; ExpressionErrorCode ret = eval( condition.mCondition.condition, condition, result, MAX_EQUATION_DEPTH, conditionIndex ); - if ( ( ret != ExpressionErrorCode::SUCCESSFUL ) || ( result.type != InspectionValue::DataType::BOOL ) || - ( !result.boolVal ) ) + if ( ( ret != ExpressionErrorCode::SUCCESSFUL ) || ( !result.isBoolOrDouble() ) || ( !result.asBool() ) ) { // Flip default true flag to false if static condition is evaluated to false mConditionsWithConditionCurrentlyTrue.reset( conditionIndex ); @@ -559,11 +661,13 @@ CollectionInspectionEngine::evaluateConditions( const TimePoint ¤tTime ) for ( uint32_t i = 0; i < mConditions.size(); i++ ) { ActiveCondition &condition = mConditions[i]; + bool conditionEvaluated = false; bool conditionEvaluatedToTrue = false; - // Only reevaluate non-static conditions with changed input + // Only reevaluate non-static conditions with changed input or conditions with isNull or custom functions if ( ( ( mConditionsWithInputSignalChanged.test( i ) ) && ( !condition.mCondition.isStaticCondition ) ) || ( condition.mCondition.alwaysEvaluateCondition ) ) { + conditionEvaluated = true; InspectionValue result; ExpressionErrorCode ret = eval( condition.mCondition.condition, condition, result, MAX_EQUATION_DEPTH, i ); if ( ( ret != ExpressionErrorCode::SUCCESSFUL ) || ( !result.isBoolOrDouble() ) || ( !result.asBool() ) ) @@ -574,6 +678,70 @@ CollectionInspectionEngine::evaluateConditions( const TimePoint ¤tTime ) { conditionEvaluatedToTrue = true; } + + // If fetch conditions are set and input signal has changed, evaluate fetch conditions + if ( !condition.mCondition.fetchConditions.empty() ) + { + InspectionValue fetchConditionResult; + for ( auto fetchCondition : condition.mCondition.fetchConditions ) + { + if ( ( mLastFetchTrigger.find( fetchCondition.fetchRequestID ) != mLastFetchTrigger.end() ) && + ( currentTime.monotonicTimeMs < + mLastFetchTrigger[fetchCondition.fetchRequestID] + mMinFetchTriggerIntervalMs ) ) + { + continue; + } + + ret = eval( fetchCondition.condition, condition, fetchConditionResult, MAX_EQUATION_DEPTH, i ); + + if ( ( ret == ExpressionErrorCode::SUCCESSFUL ) && ( fetchConditionResult.isBoolOrDouble() ) && + ( fetchConditionResult.asBool() ) ) + { + if ( ( !fetchCondition.triggerOnlyOnRisingEdge ) || + ( !mFetchConditionsWithConditionCurrentlyTrue.test( fetchCondition.fetchRequestID ) ) ) + { + // Notify fetch manager that condition for this fetch request evaluated to TRUE + mFetchConditionEvaluationListeners.notify( fetchCondition.fetchRequestID, + fetchConditionResult.boolVal ); + oneConditionEvaluatedToTrue = true; + mLastFetchTrigger[fetchCondition.fetchRequestID] = currentTime.monotonicTimeMs; + } + mFetchConditionsWithConditionCurrentlyTrue.set( fetchCondition.fetchRequestID ); + } + else + { + mFetchConditionsWithConditionCurrentlyTrue.reset( fetchCondition.fetchRequestID ); + } + } + } + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // If forward conditions are set and input signal has changed, evaluate forward conditions + if ( !condition.mCondition.forwardConditions.empty() ) + { + for ( uint32_t j = 0; j < condition.mCondition.forwardConditions.size(); j++ ) + { + InspectionValue resultForward; + auto forwardCondition = condition.mCondition.forwardConditions[j]; + ret = eval( forwardCondition.condition, condition, resultForward, MAX_EQUATION_DEPTH, i ); + if ( ( ret != ExpressionErrorCode::SUCCESSFUL ) || ( !resultForward.isBoolOrDouble() ) || + ( !resultForward.asBool() ) ) + { + + mForwardConditionCurrentlyTrueForCampaignPartitions[condition.mCondition.metadata.campaignArn] + [j] = false; + } + else + { + + mForwardConditionCurrentlyTrueForCampaignPartitions[condition.mCondition.metadata.campaignArn] + [j] = true; + oneConditionEvaluatedToTrue = true; + } + } + } +#endif + // Reset this flag only after fetch conditions are reevaluated mConditionsWithInputSignalChanged.reset( i ); } // If condition was reevaluated to true or if condition is still true and not waiting to be published @@ -603,7 +771,20 @@ CollectionInspectionEngine::evaluateConditions( const TimePoint ¤tTime ) oneConditionEvaluatedToTrue = true; } } + + if ( conditionEvaluated ) + { + for ( const auto &customFunction : mCustomFunctionCallbacks ) + { + if ( customFunction.second.conditionEndCallback ) + { + customFunction.second.conditionEndCallback( + condition.mCollectedSignalIds, currentTime.systemTimeMs, condition.mCollectedData ); + } + } + } } + return oneConditionEvaluatedToTrue; } @@ -639,8 +820,11 @@ CollectionInspectionEngine::collectLastSignals( SignalID id, { output.emplace_back( id, sample.mTimestamp, sample.mValue, signalType ); sample.setAlreadyConsumed( conditionId, true ); + if ( ( signalType == SignalType::STRING ) // NOLINT(clang-diagnostic-parentheses-equality) #ifdef FWE_FEATURE_VISION_SYSTEM_DATA - if ( signalType == SignalType::COMPLEX_SIGNAL ) + || ( signalType == SignalType::COMPLEX_SIGNAL ) +#endif + ) { NotifyRawBufferManager::increaseElementUsage( id, @@ -648,7 +832,6 @@ CollectionInspectionEngine::collectLastSignals( SignalID id, RawData::BufferHandleUsageStage::COLLECTION_INSPECTION_ENGINE_SELECTED_FOR_UPLOAD, sample.mValue ); } -#endif } newestSignalTimestamp = std::max( newestSignalTimestamp, sample.mTimestamp ); pos--; @@ -709,6 +892,7 @@ CollectionInspectionEngine::collectData( ActiveCondition &condition, output.triggeredVisionSystemData->triggerTime = condition.mLastTrigger.systemTimeMs; output.triggeredVisionSystemData->eventID = condition.mEventID; #endif + // Pack signals for ( auto &s : condition.mCondition.signals ) { @@ -804,6 +988,14 @@ CollectionInspectionEngine::collectData( ActiveCondition &condition, newestSignalTimestamp, output.triggeredCollectionSchemeData->signals ); break; + case SignalType::STRING: + collectLastSignals( s.signalID, + s.sampleBufferSize, + conditionId, + s.signalType, + newestSignalTimestamp, + output.triggeredCollectionSchemeData->signals ); + break; case SignalType::UNKNOWN: FWE_LOG_WARN( "Signal ID: " + std::to_string( s.signalID ) + " associated with Campaign SyncId: " + ( condition.mCondition.metadata.collectionSchemeID ) + @@ -868,6 +1060,7 @@ CollectionInspectionEngine::collectNextDataToSend( const TimePoint ¤tTime, if ( mConditionsTriggeredWaitingPublished.test( mNextConditionToCollectedIndex ) ) { auto &condition = mConditions[mNextConditionToCollectedIndex]; + { if ( ( ( condition.mLastTrigger.systemTimeMs == 0 ) && ( condition.mLastTrigger.monotonicTimeMs == 0 ) ) || @@ -907,6 +1100,15 @@ CollectionInspectionEngine::collectNextDataToSend( const TimePoint ¤tTime, return {}; } +#ifdef FWE_FEATURE_STORE_AND_FORWARD +// coverity[autosar_cpp14_a18_1_2_violation] std::vector specialization is acceptable in this usecase +std::unordered_map> +CollectionInspectionEngine::forwardConditionForCampaignPartitions() +{ + return mForwardConditionCurrentlyTrueForCampaignPartitions; +} +#endif + void CollectionInspectionEngine::addNewRawCanFrame( CANRawFrameID canID, CANChannelNumericID channelID, @@ -958,29 +1160,22 @@ CollectionInspectionEngine::getLatestBufferSignalValue( SignalID id, { auto *s = condition.getEvaluationSignalsBufferPtr( id ); - if ( s == nullptr ) + if ( ( s != nullptr ) && ( s->mCounter != 0 ) ) // Otherwise leave result as undefined { - FWE_LOG_WARN( "SIGNAL_NOT_FOUND" ); - // Signal not collected by any active condition - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + result = static_cast( s->mBuffer[s->mCurrentPosition].mValue ); } - if ( s->mCounter == 0 ) - { - // Not a single sample collected yet - return ExpressionErrorCode::SIGNAL_NOT_FOUND; - } - result = static_cast( s->mBuffer[s->mCurrentPosition].mValue ); return ExpressionErrorCode::SUCCESSFUL; } ExpressionErrorCode CollectionInspectionEngine::getLatestSignalValue( SignalID id, ActiveCondition &condition, InspectionValue &result ) { + // Set the signal ID, even if value is undefined. Can be used by custom functions to use signal by reference. + result.signalID = id; if ( mSignalToBufferTypeMap.find( id ) == mSignalToBufferTypeMap.end() ) { - FWE_LOG_WARN( "SIGNAL_NOT_FOUND" ); - // Signal not collected by any active condition - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + // Signal not collected by any active condition, leave result as undefined: + return ExpressionErrorCode::SUCCESSFUL; } auto signalType = mSignalToBufferTypeMap[id]; // coverity[autosar_cpp14_m6_4_6_violation] @@ -1009,17 +1204,46 @@ CollectionInspectionEngine::getLatestSignalValue( SignalID id, ActiveCondition & return getLatestBufferSignalValue( id, condition, result ); case SignalType::BOOLEAN: return getLatestBufferSignalValue( id, condition, result ); + case SignalType::STRING: { + auto res = getLatestBufferSignalValue( id, condition, result ); + if ( res != ExpressionErrorCode::SUCCESSFUL ) + { + return res; + } + if ( result.isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( result.type != InspectionValue::DataType::DOUBLE ) + { + FWE_LOG_WARN( "Expected a numeric value for raw buffer handle type" ); + return ExpressionErrorCode::TYPE_MISMATCH; + } + auto loanedRawDataFrame = + mRawBufferManager->borrowFrame( id, static_cast( result.doubleVal ) ); + + if ( loanedRawDataFrame.isNull() ) + { + FWE_LOG_ERROR( "Raw data with signal id: " + std::to_string( id ) + + " and buffer handle: " + std::to_string( result.doubleVal ) + + " could not be used for inspection because it was already deleted" ); + result.type = InspectionValue::DataType::UNDEFINED; + return ExpressionErrorCode::SUCCESSFUL; + } + auto data = loanedRawDataFrame.getData(); + auto size = loanedRawDataFrame.getSize(); + result = std::string( reinterpret_cast( data ), size ); + return ExpressionErrorCode::SUCCESSFUL; + } case SignalType::UNKNOWN: - FWE_LOG_WARN( "Signal ID: " + std::to_string( id ) + " associated with Campaign SyncId: " + - condition.mCondition.metadata.collectionSchemeID + " is of type UNKNOWN and used in evaluation" ); - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + return ExpressionErrorCode::SUCCESSFUL; // Leave result as undefined #ifdef FWE_FEATURE_VISION_SYSTEM_DATA case SignalType::COMPLEX_SIGNAL: FWE_LOG_WARN( "Complex signals are not supported in evaluation" ) - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; #endif } - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; } template @@ -1032,36 +1256,52 @@ CollectionInspectionEngine::getSampleWindowFunctionType( WindowFunction function auto w = condition.getFixedTimeWindowFunctionDataPtr( signalID ); if ( w == nullptr ) { - // Signal not collected by any active condition - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + // Signal not collected by any active condition, leave result as undefined: + return ExpressionErrorCode::SUCCESSFUL; } switch ( function ) { case WindowFunction::LAST_FIXED_WINDOW_AVG: - result = static_cast( w->mLastAvg ); - return w->mLastAvailable ? ExpressionErrorCode::SUCCESSFUL : ExpressionErrorCode::FUNCTION_DATA_NOT_AVAILABLE; + if ( w->mLastAvailable ) + { + result = static_cast( w->mLastAvg ); + } + return ExpressionErrorCode::SUCCESSFUL; case WindowFunction::LAST_FIXED_WINDOW_MIN: - result = static_cast( w->mLastMin ); - return w->mLastAvailable ? ExpressionErrorCode::SUCCESSFUL : ExpressionErrorCode::FUNCTION_DATA_NOT_AVAILABLE; + if ( w->mLastAvailable ) + { + result = static_cast( w->mLastMin ); + } + return ExpressionErrorCode::SUCCESSFUL; case WindowFunction::LAST_FIXED_WINDOW_MAX: - result = static_cast( w->mLastMax ); - return w->mLastAvailable ? ExpressionErrorCode::SUCCESSFUL : ExpressionErrorCode::FUNCTION_DATA_NOT_AVAILABLE; + if ( w->mLastAvailable ) + { + result = static_cast( w->mLastMax ); + } + return ExpressionErrorCode::SUCCESSFUL; case WindowFunction::PREV_LAST_FIXED_WINDOW_AVG: - result = static_cast( w->mPreviousLastAvg ); - return w->mPreviousLastAvailable ? ExpressionErrorCode::SUCCESSFUL - : ExpressionErrorCode::FUNCTION_DATA_NOT_AVAILABLE; + if ( w->mPreviousLastAvailable ) + { + result = static_cast( w->mPreviousLastAvg ); + } + return ExpressionErrorCode::SUCCESSFUL; case WindowFunction::PREV_LAST_FIXED_WINDOW_MIN: - result = static_cast( w->mPreviousLastMin ); - return w->mPreviousLastAvailable ? ExpressionErrorCode::SUCCESSFUL - : ExpressionErrorCode::FUNCTION_DATA_NOT_AVAILABLE; + if ( w->mPreviousLastAvailable ) + { + result = static_cast( w->mPreviousLastMin ); + } + return ExpressionErrorCode::SUCCESSFUL; case WindowFunction::PREV_LAST_FIXED_WINDOW_MAX: - result = static_cast( w->mPreviousLastMax ); - return w->mPreviousLastAvailable ? ExpressionErrorCode::SUCCESSFUL - : ExpressionErrorCode::FUNCTION_DATA_NOT_AVAILABLE; - default: + if ( w->mPreviousLastAvailable ) + { + result = static_cast( w->mPreviousLastMax ); + } + return ExpressionErrorCode::SUCCESSFUL; + case WindowFunction::NONE: return ExpressionErrorCode::NOT_IMPLEMENTED_FUNCTION; } + return ExpressionErrorCode::NOT_IMPLEMENTED_FUNCTION; } ExpressionErrorCode @@ -1072,9 +1312,8 @@ CollectionInspectionEngine::getSampleWindowFunction( WindowFunction function, { if ( mSignalToBufferTypeMap.find( signalID ) == mSignalToBufferTypeMap.end() ) { - FWE_LOG_WARN( "SIGNAL_NOT_FOUND" ); - // Signal not collected by any active condition - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + // Signal not collected by any active condition, leave result as undefined: + return ExpressionErrorCode::SUCCESSFUL; } auto signalType = mSignalToBufferTypeMap[signalID]; // coverity[autosar_cpp14_m6_4_6_violation] @@ -1103,18 +1342,18 @@ CollectionInspectionEngine::getSampleWindowFunction( WindowFunction function, return getSampleWindowFunctionType( function, signalID, condition, result ); case SignalType::BOOLEAN: return getSampleWindowFunctionType( function, signalID, condition, result ); + case SignalType::STRING: + FWE_LOG_WARN( "Window functions are not supported for string signals" ) + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; case SignalType::UNKNOWN: - FWE_LOG_WARN( "Window functions are not supported for signal ID: " + std::to_string( signalID ) + - " associated with Campaign SyncId: " + condition.mCondition.metadata.collectionSchemeID + - " as signal is of type UNKNOWN" ); - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + return ExpressionErrorCode::SUCCESSFUL; // Leave result as undefined #ifdef FWE_FEATURE_VISION_SYSTEM_DATA case SignalType::COMPLEX_SIGNAL: FWE_LOG_WARN( "Window functions are not supported for complex signals" ) - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; #endif } - return ExpressionErrorCode::SIGNAL_NOT_FOUND; + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; } ExpressionErrorCode @@ -1139,6 +1378,11 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, resultValue = expression->booleanValue; return ExpressionErrorCode::SUCCESSFUL; } + if ( expression->nodeType == ExpressionNodeType::STRING ) + { + resultValue = expression->stringValue; + return ExpressionErrorCode::SUCCESSFUL; + } if ( expression->nodeType == ExpressionNodeType::SIGNAL ) { return getLatestSignalValue( expression->signalID, condition, resultValue ); @@ -1148,6 +1392,46 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, return getSampleWindowFunction( expression->function.windowFunction, expression->signalID, condition, resultValue ); } + if ( expression->nodeType == ExpressionNodeType::IS_NULL_FUNCTION ) + { + if ( ( expression->left == nullptr ) || ( expression->left->nodeType != ExpressionNodeType::SIGNAL ) ) + { + FWE_LOG_ERROR( "isNull function does not have signal ID as parameter" ); + return ExpressionErrorCode::TYPE_MISMATCH; + } + auto ret = isNewSignalValueAvailable( expression->left->signalID, condition, resultValue, conditionId ); + if ( ( ret == ExpressionErrorCode::SUCCESSFUL ) && ( resultValue.isBoolOrDouble() ) ) + { + // Revert the result value of this function + resultValue = !resultValue.asBool(); + } + return ret; + } + if ( expression->nodeType == ExpressionNodeType::CUSTOM_FUNCTION ) + { + std::vector argResults( expression->function.customFunctionParams.size() ); + for ( size_t i = 0; i < expression->function.customFunctionParams.size(); i++ ) + { + auto argRet = eval( expression->function.customFunctionParams[i], + condition, + argResults[i], + remainingStackDepth - 1, + conditionId ); + if ( argRet != ExpressionErrorCode::SUCCESSFUL ) + { + return argRet; + } + } + auto customFunction = mCustomFunctionCallbacks.find( expression->function.customFunctionName ); + if ( ( customFunction == mCustomFunctionCallbacks.end() ) || ( !customFunction->second.invokeCallback ) ) + { + return ExpressionErrorCode::NOT_IMPLEMENTED_FUNCTION; + } + auto funcRes = + customFunction->second.invokeCallback( expression->function.customFunctionInvocationId, argResults ); + resultValue = std::move( funcRes.value ); + return funcRes.error; + } InspectionValue leftResult; InspectionValue rightResult; @@ -1173,6 +1457,12 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, } } + if ( leftResult.isUndefined() || + ( ( expression->nodeType != ExpressionNodeType::OPERATOR_LOGICAL_NOT ) && rightResult.isUndefined() ) ) + { + return ExpressionErrorCode::SUCCESSFUL; // Leave result as undefined + } + switch ( expression->nodeType ) { case ExpressionNodeType::OPERATOR_SMALLER: @@ -1204,7 +1494,11 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, resultValue = leftResult.asDouble() >= rightResult.asDouble(); return ExpressionErrorCode::SUCCESSFUL; case ExpressionNodeType::OPERATOR_EQUAL: - if ( ( !leftResult.isBoolOrDouble() ) || ( !rightResult.isBoolOrDouble() ) ) + if ( leftResult.isString() && rightResult.isString() ) + { + resultValue = *leftResult.stringVal == *rightResult.stringVal; + } + else if ( ( !leftResult.isBoolOrDouble() ) || ( !rightResult.isBoolOrDouble() ) ) { return ExpressionErrorCode::TYPE_MISMATCH; } @@ -1214,7 +1508,11 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, } return ExpressionErrorCode::SUCCESSFUL; case ExpressionNodeType::OPERATOR_NOT_EQUAL: - if ( ( !leftResult.isBoolOrDouble() ) || ( !rightResult.isBoolOrDouble() ) ) + if ( leftResult.isString() && rightResult.isString() ) + { + resultValue = *leftResult.stringVal != *rightResult.stringVal; + } + else if ( ( !leftResult.isBoolOrDouble() ) || ( !rightResult.isBoolOrDouble() ) ) { return ExpressionErrorCode::TYPE_MISMATCH; } @@ -1245,7 +1543,11 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, resultValue = !leftResult.asBool(); return ExpressionErrorCode::SUCCESSFUL; case ExpressionNodeType::OPERATOR_ARITHMETIC_PLUS: - if ( ( !leftResult.isBoolOrDouble() ) || ( !rightResult.isBoolOrDouble() ) ) + if ( leftResult.isString() && rightResult.isString() ) + { + resultValue = *leftResult.stringVal + *rightResult.stringVal; + } + else if ( ( !leftResult.isBoolOrDouble() ) || ( !rightResult.isBoolOrDouble() ) ) { return ExpressionErrorCode::TYPE_MISMATCH; } @@ -1276,7 +1578,7 @@ CollectionInspectionEngine::eval( const ExpressionNode *expression, resultValue = leftResult.asDouble() / rightResult.asDouble(); return ExpressionErrorCode::SUCCESSFUL; default: - return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; + return ExpressionErrorCode::NOT_IMPLEMENTED_FUNCTION; } } @@ -1290,5 +1592,79 @@ CollectionInspectionEngine::generateEventID( Timestamp timestamp ) return eventId; } +ExpressionErrorCode +CollectionInspectionEngine::isNewSignalValueAvailable( SignalID signalID, + ActiveCondition &condition, + InspectionValue &resultValue, + uint32_t conditionId ) +{ + if ( mSignalToBufferTypeMap.find( signalID ) == mSignalToBufferTypeMap.end() ) + { + // Signal not collected by any active condition, leave result as undefined: + return ExpressionErrorCode::SUCCESSFUL; + } + auto signalType = mSignalToBufferTypeMap[signalID]; + + // coverity[autosar_cpp14_m6_4_6_violation] + // coverity[misra_cpp_2008_rule_6_4_6_violation] compiler warning is preferred over a default-clause + switch ( signalType ) + { + case SignalType::UINT8: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::INT8: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::UINT16: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::INT16: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::UINT32: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::INT32: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::UINT64: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::INT64: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::FLOAT: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::DOUBLE: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::BOOLEAN: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::STRING: + return isNewSignalValueAvailableType( signalID, condition, resultValue, conditionId ); + case SignalType::UNKNOWN: + return ExpressionErrorCode::SUCCESSFUL; // Leave result as undefined +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + case SignalType::COMPLEX_SIGNAL: + FWE_LOG_WARN( "Complex signals are not supported in evaluation" ) + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; +#endif + } + return ExpressionErrorCode::NOT_IMPLEMENTED_TYPE; +} + +template +ExpressionErrorCode +CollectionInspectionEngine::isNewSignalValueAvailableType( SignalID signalID, + ActiveCondition &condition, + InspectionValue &resultValue, + uint32_t conditionId ) +{ + auto *s = condition.getEvaluationSignalsBufferPtr( signalID ); + if ( s != nullptr ) // Otherwise leave result as undefined + { + resultValue = ( s->mCounter != 0 ) && ( !s->mBuffer[s->mCurrentPosition].isAlreadyConsumed( conditionId ) ); + } + return ExpressionErrorCode::SUCCESSFUL; +} + +void +CollectionInspectionEngine::registerCustomFunction( const std::string &name, CustomFunctionCallbacks callbacks ) +{ + FWE_LOG_TRACE( "Registering custom function " + name ); + mCustomFunctionCallbacks.emplace( name, std::move( callbacks ) ); +} + } // namespace IoTFleetWise } // namespace Aws diff --git a/src/CollectionInspectionEngine.h b/src/CollectionInspectionEngine.h index d0b1613e..4829d8f2 100644 --- a/src/CollectionInspectionEngine.h +++ b/src/CollectionInspectionEngine.h @@ -7,6 +7,7 @@ #include "CollectionInspectionAPITypes.h" #include "EventTypes.h" #include "ICollectionScheme.h" +#include "Listener.h" #include "LoggingModule.h" #include "MessageTypes.h" #include "OBDDataTypes.h" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +37,7 @@ namespace Aws namespace IoTFleetWise { +static constexpr Timestamp MIN_FETCH_TRIGGER_MS = 1000; // Rule A14-8-2 suggests to use class template specialization instead of function template specialization template class NotifyRawBufferManager @@ -116,16 +119,19 @@ class CollectionInspectionEngine /** * @brief Construct the CollectionInspectionEngine which handles multiple conditions * + * @param minFetchTriggerIntervalMs the minimum interval in milliseconds between two fetch triggers + * * @param sendDataOnlyOncePerCondition if true only data with a millisecond timestamp, bigger than the timestamp the * condition last sent out data, will be included. */ - CollectionInspectionEngine( bool sendDataOnlyOncePerCondition = true ); + CollectionInspectionEngine( uint32_t minFetchTriggerIntervalMs = MIN_FETCH_TRIGGER_MS, + bool sendDataOnlyOncePerCondition = true ); void onChangeInspectionMatrix( const std::shared_ptr &inspectionMatrix, const TimePoint ¤tTime ); /** - * @brief Go through all conditions with changed condition signals and evaluate condition + * @brief Go through all conditions incl. fetch conditions with changed condition signals and evaluate condition * * This needs to be called directly after new signals are added to the CollectionEngine. * If multiple samples of the same signal are added to CollectionEngine without calling @@ -135,6 +141,21 @@ class CollectionInspectionEngine */ bool evaluateConditions( const TimePoint ¤tTime ); + /** + * @brief The callback function used to notify any listeners on fetch condition evaluation + * + * @param fetchRequestID fetch request id for which the evaluation was done + * @param evaluationResult evaluation result (true/false) + */ + using OnFetchConditionEvaluationCallback = + std::function; + + void + subscribeToFetchConditionEvaluationUpdate( OnFetchConditionEvaluationCallback callback ) + { + mFetchConditionEvaluationListeners.subscribe( callback ); + } + /** * @brief Copy for a triggered condition data out of the signal buffer * @@ -150,6 +171,12 @@ class CollectionInspectionEngine * @return if dataReadyToBeSent() is false a nullptr otherwise the collected data will be returned */ CollectionInspectionEngineOutput collectNextDataToSend( const TimePoint ¤tTime, uint32_t &waitTimeMs ); + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // coverity[autosar_cpp14_a18_1_2_violation] std::vector specialization is acceptable in this usecase + std::unordered_map> forwardConditionForCampaignPartitions(); +#endif + /** * @brief Give a new signal to the collection engine to cached it * @@ -160,12 +187,17 @@ class CollectionInspectionEngine * The signals should come in ordered by time (oldest signals first) * * @param id id of the obd based or can based signal + * @param fetchRequestID fetch request ID that signal was collected for * @param receiveTime timestamp at which time was the signal seen on the physical bus * @param currentMonotonicTimeMs current monotonic time for window function evaluation * @param value the signal value */ template - void addNewSignal( SignalID id, const TimePoint &receiveTime, const Timestamp ¤tMonotonicTimeMs, T value ); + void addNewSignal( SignalID id, + FetchRequestID fetchRequestID, + const TimePoint &receiveTime, + const Timestamp ¤tMonotonicTimeMs, + T value ); /** * @brief Add new signal buffer entry to mSignalBuffers (SignalHistoryBufferCollection) @@ -174,7 +206,8 @@ class CollectionInspectionEngine * size. */ template - void addSignalBuffer( const InspectionMatrixSignalCollectionInfo &signal ); + void addSignalBuffer( const InspectionMatrixSignalCollectionInfo &signal, + SignalBufferConditionID signalBufferConditionIndex ); /** * @brief Add new raw CAN Frame history buffer. If frame is not needed call will be just ignored @@ -203,8 +236,11 @@ class CollectionInspectionEngine void setActiveDTCs( const DTCInfo &activeDTCs ); + void registerCustomFunction( const std::string &name, CustomFunctionCallbacks callbacks ); + private: static const uint32_t MAX_SAMPLE_MEMORY = 20 * 1024 * 1024; // 20MB max for all samples + uint32_t mMinFetchTriggerIntervalMs; static inline double EVAL_EQUAL_DISTANCE() { @@ -557,6 +593,16 @@ class CollectionInspectionEngine return ++counter; } + ExpressionErrorCode isNewSignalValueAvailable( SignalID signalID, + ActiveCondition &condition, + InspectionValue &resultValue, + uint32_t conditionId ); + template + ExpressionErrorCode isNewSignalValueAvailableType( SignalID signalID, + ActiveCondition &condition, + InspectionValue &resultValue, + uint32_t conditionId ); + // VSS supported datatypes using SignalHistoryBuffersVar = boost::variant>, std::vector>, @@ -569,11 +615,16 @@ class CollectionInspectionEngine std::vector>, std::vector>, std::vector>>; - using SignalHistoryBufferCollection = std::unordered_map; + using SignalHistoryBufferCollection = + std::unordered_map>; SignalHistoryBufferCollection - mSignalBuffers; /**< signal history buffer. First vector has the signalID as index. In the nested vector + mSignalBuffers; /**< signal history buffer. Vector has the "virtual" index, that is based of + * condition index and fetch request id, and signalID as index. In the nested vector * the different subsampling of this signal are stored. */ + using FetchRequestToConditionIndexMap = std::unordered_map; + FetchRequestToConditionIndexMap mFetchRequestToConditionIndexMap; + using SignalToBufferTypeMap = std::unordered_map; SignalToBufferTypeMap mSignalToBufferTypeMap; @@ -582,10 +633,12 @@ class CollectionInspectionEngine */ template std::vector> * - getSignalHistoryBuffersPtr( SignalID signalID ) + getSignalHistoryBuffersPtr( SignalID signalID, SignalBufferConditionID signalBufferConditionIndex ) { std::vector> *resVec = nullptr; - if ( mSignalBuffers.find( signalID ) == mSignalBuffers.end() ) + + auto outerMapIt = mSignalBuffers.find( signalBufferConditionIndex ); + if ( outerMapIt == mSignalBuffers.end() || outerMapIt->second.find( signalID ) == outerMapIt->second.end() ) { // create a new map entry auto mapEntryVec = std::vector>{}; @@ -594,7 +647,7 @@ class CollectionInspectionEngine SignalHistoryBuffersVar mapEntry = mapEntryVec; FWE_LOG_TRACE( "Creating new signalHistoryBuffer vector for Signal " + std::to_string( signalID ) + " with type " + boost::core::demangle( typeid( T ).name() ) ); - mSignalBuffers.insert( { signalID, mapEntry } ); + mSignalBuffers[signalBufferConditionIndex].insert( { signalID, mapEntry } ); } catch ( ... ) { @@ -606,7 +659,7 @@ class CollectionInspectionEngine try { - auto signalBufferVectorPtr = mSignalBuffers.find( signalID ); + auto signalBufferVectorPtr = mSignalBuffers.find( signalBufferConditionIndex )->second.find( signalID ); resVec = boost::get>>( &( signalBufferVectorPtr->second ) ); if ( resVec == nullptr ) { @@ -628,9 +681,11 @@ class CollectionInspectionEngine */ template SignalHistoryBuffer * - getSignalHistoryBufferPtr( SignalID signalID, uint32_t minimumSampleIntervalMs ) + getSignalHistoryBufferPtr( SignalID signalID, + SignalBufferConditionID signalBufferConditionIndex, + uint32_t minimumSampleIntervalMs ) { - auto signalHistoryBufferVectorPtr = getSignalHistoryBuffersPtr( signalID ); + auto signalHistoryBufferVectorPtr = getSignalHistoryBuffersPtr( signalID, signalBufferConditionIndex ); if ( signalHistoryBufferVectorPtr != nullptr ) { for ( auto &buffer : *signalHistoryBufferVectorPtr ) @@ -645,10 +700,14 @@ class CollectionInspectionEngine } template - bool allocateBufferVector( SignalID signalID, size_t &usedBytes ); + bool allocateBufferVector( SignalID signalID, + SignalBufferConditionID signalBufferConditionIndex, + size_t &usedBytes ); template - void updateBufferFixedWindowFunction( SignalID signalID, Timestamp timestamp ); + void updateBufferFixedWindowFunction( SignalID signalID, + SignalBufferConditionID signalBufferConditionIndex, + Timestamp timestamp ); template ExpressionErrorCode getLatestBufferSignalValue( SignalID id, ActiveCondition &condition, InspectionValue &result ); @@ -679,9 +738,16 @@ class CollectionInspectionEngine mConditionsTriggeredWaitingPublished; // bit is set if condition is triggered and waits for its data to be sent // out, bit is not set if condition is not triggered + std::bitset + mFetchConditionsWithConditionCurrentlyTrue; // bit is set if the fetch condition evaluated to true the last time + // indexed by fetch request ID which is unique and generated on + // scheme arrival + std::vector mConditions; std::shared_ptr mActiveInspectionMatrix; + ThreadSafeListeners mFetchConditionEvaluationListeners; + void collectData( ActiveCondition &condition, uint32_t conditionId, Timestamp &newestSignalTimestamp, @@ -690,24 +756,38 @@ class CollectionInspectionEngine Timestamp mNextWindowFunctionTimesOut{ 0 }; bool mSendDataOnlyOncePerCondition{ false }; + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // coverity[autosar_cpp14_a18_1_2_violation] std::vector specialization is acceptable in this usecase + std::unordered_map> mForwardConditionCurrentlyTrueForCampaignPartitions; +#endif + std::unordered_map mLastFetchTrigger; std::shared_ptr mRawBufferManager{ nullptr }; + + std::unordered_map mCustomFunctionCallbacks; + void cleanupCustomFunctions( const ExpressionNode *expression ); }; template void CollectionInspectionEngine::addNewSignal( SignalID id, + FetchRequestID fetchRequestID, const TimePoint &receiveTime, const Timestamp ¤tMonotonicTimeMs, T value ) { - if ( mSignalBuffers.find( id ) == mSignalBuffers.end() ) + auto signalBufferConditionIndex = mFetchRequestToConditionIndexMap[fetchRequestID]; + auto outerSignalBufferMapIt = mSignalBuffers.find( signalBufferConditionIndex ); + + if ( outerSignalBufferMapIt == mSignalBuffers.end() || + outerSignalBufferMapIt->second.find( id ) == outerSignalBufferMapIt->second.end() ) { // Signal not collected by any active condition return; } // Iterate through all sampling intervals of the signal std::vector> *signalHistoryBufferPtr = nullptr; - signalHistoryBufferPtr = getSignalHistoryBuffersPtr( id ); + signalHistoryBufferPtr = getSignalHistoryBuffersPtr( id, signalBufferConditionIndex ); if ( signalHistoryBufferPtr == nullptr ) { // Invalid access to the map Buffer datatype diff --git a/src/CollectionInspectionWorkerThread.cpp b/src/CollectionInspectionWorkerThread.cpp index 3318217f..bb0780c3 100644 --- a/src/CollectionInspectionWorkerThread.cpp +++ b/src/CollectionInspectionWorkerThread.cpp @@ -13,6 +13,11 @@ #include #include +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include "StreamManager.h" +#include +#endif + namespace Aws { namespace IoTFleetWise @@ -22,7 +27,13 @@ bool CollectionInspectionWorkerThread::init( const std::shared_ptr &inputSignalBuffer, const std::shared_ptr &outputCollectedData, uint32_t idleTimeMs, - std::shared_ptr rawBufferManager ) + std::shared_ptr rawBufferManager +#ifdef FWE_FEATURE_STORE_AND_FORWARD + , + std::shared_ptr streamForwarder, + std::shared_ptr streamManager +#endif +) { mInputSignalBuffer = inputSignalBuffer; mOutputCollectedData = outputCollectedData; @@ -33,6 +44,10 @@ CollectionInspectionWorkerThread::init( const std::shared_ptr &inp mCollectionInspectionEngine.setRawDataBufferManager( rawBufferManager ); mRawBufferManager = std::move( rawBufferManager ); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + mStreamForwarder = std::move( streamForwarder ); + mStreamManager = std::move( streamManager ); +#endif return true; } @@ -152,6 +167,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::UINT8: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.uint8Val ); @@ -159,6 +175,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::INT8: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.int8Val ); @@ -166,6 +183,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::UINT16: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.uint16Val ); @@ -173,6 +191,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::INT16: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.int16Val ); @@ -180,6 +199,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::UINT32: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.uint32Val ); @@ -187,6 +207,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::INT32: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.int32Val ); @@ -194,6 +215,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::UINT64: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.uint64Val ); @@ -201,6 +223,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::INT64: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.int64Val ); @@ -208,6 +231,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::FLOAT: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.floatVal ); @@ -215,6 +239,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::DOUBLE: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.doubleVal ); @@ -222,10 +247,26 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::BOOLEAN: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.boolVal ); break; + case SignalType::STRING: + consumer->mCollectionInspectionEngine.addNewSignal( + inputSignal.signalID, + inputSignal.fetchRequestID, + calculateMonotonicTime( currentTime, inputSignal.receiveTime ), + currentTime.monotonicTimeMs, + signalValue.value.uint32Val ); + if ( consumer->mRawBufferManager != nullptr ) + { + consumer->mRawBufferManager->decreaseHandleUsageHint( + inputSignal.signalID, + signalValue.value.uint32Val, + RawData::BufferHandleUsageStage::COLLECTED_NOT_IN_HISTORY_BUFFER ); + } + break; case SignalType::UNKNOWN: FWE_LOG_WARN( "UNKNOWN signal [signal id: " + std::to_string( inputSignal.signalID ) + " ] should not be processed" ); @@ -234,6 +275,7 @@ CollectionInspectionWorkerThread::doWork( void *data ) case SignalType::COMPLEX_SIGNAL: consumer->mCollectionInspectionEngine.addNewSignal( inputSignal.signalID, + inputSignal.fetchRequestID, calculateMonotonicTime( currentTime, inputSignal.receiveTime ), currentTime.monotonicTimeMs, signalValue.value.uint32Val ); @@ -335,6 +377,30 @@ CollectionInspectionWorkerThread::collectDataAndUpload() { uint32_t collectedDataPackages = 0; uint32_t waitTimeMs = this->mIdleTimeMs; + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // TODO consider priority, queue is shared between collecting and forwarding + if ( this->mStreamForwarder != nullptr ) + { + for ( const auto &campaign : this->mCollectionInspectionEngine.forwardConditionForCampaignPartitions() ) + { + for ( Aws::IoTFleetWise::Store::PartitionID pID = 0; pID < campaign.second.size(); ++pID ) + { + if ( campaign.second[pID] ) + { + this->mStreamForwarder->beginForward( + campaign.first, pID, Store::StreamForwarder::Source::CONDITION ); + } + else + { + this->mStreamForwarder->cancelForward( + campaign.first, pID, Store::StreamForwarder::Source::CONDITION ); + } + } + } + } +#endif + auto collectedData = this->mCollectionInspectionEngine.collectNextDataToSend( this->mClock->timeSinceEpoch(), waitTimeMs ); while ( ( ( collectedData.triggeredCollectionSchemeData != nullptr ) @@ -347,6 +413,30 @@ CollectionInspectionWorkerThread::collectDataAndUpload() TraceModule::get().incrementVariable( TraceVariable::CE_TRIGGERS ); if ( collectedData.triggeredCollectionSchemeData != nullptr ) { +#ifdef FWE_FEATURE_STORE_AND_FORWARD + auto result = Store::StreamManager::ReturnCode::STREAM_NOT_FOUND; + if ( mStreamManager != nullptr ) + { + result = mStreamManager->appendToStreams( *collectedData.triggeredCollectionSchemeData ); + } + if ( result == Store::StreamManager::ReturnCode::SUCCESS ) + { + // Successfully appended + } + else if ( result == Store::StreamManager::ReturnCode::EMPTY_DATA ) + { + FWE_LOG_INFO( + "The trigger for Campaign: " + collectedData.triggeredCollectionSchemeData->metadata.campaignArn + + " activated eventID: " + std::to_string( collectedData.triggeredCollectionSchemeData->eventID ) + + " but no data is available to ingest" ); + } + else if ( result != Store::StreamManager::ReturnCode::STREAM_NOT_FOUND ) + { + FWE_LOG_ERROR( "Failed to store FWE data with eventID " + + std::to_string( collectedData.triggeredCollectionSchemeData->eventID ) ); + } + else +#endif { if ( this->mOutputCollectedData->push( collectedData.triggeredCollectionSchemeData ) ) { diff --git a/src/CollectionInspectionWorkerThread.h b/src/CollectionInspectionWorkerThread.h index f8cb0662..d44ce7af 100644 --- a/src/CollectionInspectionWorkerThread.h +++ b/src/CollectionInspectionWorkerThread.h @@ -17,6 +17,11 @@ #include #include +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include "StreamForwarder.h" +#include "StreamManager.h" +#endif + namespace Aws { namespace IoTFleetWise @@ -53,6 +58,12 @@ class CollectionInspectionWorkerThread uint32_t idleTimeMs, /**< if no new data is available sleep for this amount of milliseconds */ std::shared_ptr rawBufferManager = nullptr /**< the raw buffer manager which is informed what data is used */ +#ifdef FWE_FEATURE_STORE_AND_FORWARD + , + std::shared_ptr streamForwarder = nullptr, + std::shared_ptr streamManager = nullptr +#endif + ); /** @@ -106,6 +117,10 @@ class CollectionInspectionWorkerThread uint32_t mIdleTimeMs{ DEFAULT_THREAD_IDLE_TIME_MS }; std::shared_ptr mClock = ClockHandler::getClock(); std::shared_ptr mRawBufferManager{ nullptr }; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + std::shared_ptr mStreamForwarder; + std::shared_ptr mStreamManager; +#endif }; } // namespace IoTFleetWise diff --git a/src/CollectionSchemeIngestion.cpp b/src/CollectionSchemeIngestion.cpp index bc607c08..3e34284a 100644 --- a/src/CollectionSchemeIngestion.cpp +++ b/src/CollectionSchemeIngestion.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 #include "CollectionSchemeIngestion.h" -#include "CollectionInspectionAPITypes.h" #include "LoggingModule.h" #include #include @@ -22,6 +21,7 @@ namespace IoTFleetWise #ifdef FWE_FEATURE_VISION_SYSTEM_DATA std::atomic CollectionSchemeIngestion::mPartialSignalCounter( 0 ); // NOLINT Global atomic signal counter #endif +CustomFunctionInvocationID CollectionSchemeIngestion::mCustomFunctionNextInvocationId{ 1 }; // NOLINT CollectionSchemeIngestion::~CollectionSchemeIngestion() { @@ -84,6 +84,150 @@ CollectionSchemeIngestion::build() FWE_LOG_TRACE( "Building CollectionScheme with ID: " + mProtoCollectionSchemeMessagePtr->campaign_sync_id() ); + // build fetch informations + if ( mProtoCollectionSchemeMessagePtr->signal_fetch_information_size() > 0 ) + { + uint32_t numNodesCondition = 0U; + uint32_t numNodesAction = 0U; + + for ( int i = 0; i < mProtoCollectionSchemeMessagePtr->signal_fetch_information_size(); i++ ) + { + const Schemas::CollectionSchemesMsg::FetchInformation &signal_fetch_information = + mProtoCollectionSchemeMessagePtr->signal_fetch_information( i ); + + if ( signal_fetch_information.fetchConfig_case() == + Schemas::CollectionSchemesMsg::FetchInformation::kTimeBased ) + { + // empty (nullptr) condition node for time-based fetching + } + else if ( ( signal_fetch_information.fetchConfig_case() == + Schemas::CollectionSchemesMsg::FetchInformation::kConditionBased ) && + signal_fetch_information.condition_based().has_condition_tree() ) + { + numNodesCondition += + getNumberOfNodes( signal_fetch_information.condition_based().condition_tree(), MAX_EQUATION_DEPTH ); + } + else + { + continue; + } + + for ( int j = 0; j < signal_fetch_information.actions_size(); j++ ) + { + numNodesAction += getNumberOfNodes( signal_fetch_information.actions( j ), MAX_EQUATION_DEPTH ); + } + } + + if ( numNodesCondition > 0 ) + { + FWE_LOG_TRACE( "The CollectionScheme has some fetch conditions => will build " + + std::to_string( numNodesCondition ) + " condition nodes for this purpose" ); + + // realloc for mExpressionNodesForFetchCondition is not allowed after reservation + // as pointers to its elements will be set during AST building process and be used later + mExpressionNodesForFetchCondition.reserve( numNodesCondition ); + } + + if ( numNodesAction > 0 ) + { + FWE_LOG_TRACE( "The CollectionScheme has some fetch actions => will build " + + std::to_string( numNodesAction ) + " fetch nodes for this purpose" ); + + // realloc for mExpressionNodesForFetchAction is not allowed after reservation + // as pointers to its elements will be set during AST building process and be used later + mExpressionNodesForFetchAction.reserve( numNodesAction ); + } + + std::size_t currentIndexCondition = 0U; + std::size_t currentIndexAction = 0U; + + for ( int i = 0; i < mProtoCollectionSchemeMessagePtr->signal_fetch_information_size(); i++ ) + { + const Schemas::CollectionSchemesMsg::FetchInformation &signal_fetch_information = + mProtoCollectionSchemeMessagePtr->signal_fetch_information( i ); + + mFetchInformations.emplace_back(); + + FetchInformation &fetchInformation = mFetchInformations.back(); + + fetchInformation.signalID = signal_fetch_information.signal_id(); + + if ( signal_fetch_information.fetchConfig_case() == + Schemas::CollectionSchemesMsg::FetchInformation::kTimeBased ) + { + // no need to set fetchInformation.condition to nullptr here as it is default constructed value + fetchInformation.maxExecutionPerInterval = signal_fetch_information.time_based().max_execution_count(); + fetchInformation.executionPeriodMs = signal_fetch_information.time_based().execution_frequency_ms(); + fetchInformation.executionIntervalMs = + signal_fetch_information.time_based().reset_max_execution_count_interval_ms(); + } + else if ( ( signal_fetch_information.fetchConfig_case() == + Schemas::CollectionSchemesMsg::FetchInformation::kConditionBased ) && + signal_fetch_information.condition_based().has_condition_tree() ) + { + fetchInformation.condition = serializeNode( signal_fetch_information.condition_based().condition_tree(), + mExpressionNodesForFetchCondition, + currentIndexCondition, + MAX_EQUATION_DEPTH ); + + if ( fetchInformation.condition == nullptr ) + { + FWE_LOG_WARN( "Fetch information #" + std::to_string( i ) + + " contains invalid condition => will ignore the fetch information" ); + mFetchInformations.pop_back(); + continue; + } + + fetchInformation.triggerOnlyOnRisingEdge = + ( signal_fetch_information.condition_based().condition_trigger_mode() == + Schemas::CollectionSchemesMsg:: + ConditionBasedFetchConfig_ConditionTriggerMode_TRIGGER_ONLY_ON_RISING_EDGE ); + } + else + { + FWE_LOG_WARN( "Fetch information #" + std::to_string( i ) + + " contains unsupported configuration => will ignore the fetch information" ); + mFetchInformations.pop_back(); + continue; + } + + bool isActionsValid = true; + + for ( int j = 0; j < signal_fetch_information.actions_size(); j++ ) + { + ExpressionNode *action = serializeNode( signal_fetch_information.actions( j ), + mExpressionNodesForFetchAction, + currentIndexAction, + MAX_EQUATION_DEPTH ); + + if ( action == nullptr ) + { + FWE_LOG_WARN( "Action #" + std::to_string( j ) + " of fetch information #" + std::to_string( i ) + + " is invalid => will ignore the fetch information" ); + isActionsValid = false; + break; + } + + fetchInformation.actions.push_back( action ); + } + + if ( !isActionsValid ) + { + mFetchInformations.pop_back(); + } + } + } + + if ( mFetchInformations.empty() ) + { + FWE_LOG_TRACE( "The CollectionScheme does not have any valid fetch information" ); + } + else + { + FWE_LOG_TRACE( "Adding " + std::to_string( mFetchInformations.size() ) + + " fetch informations into the CollectionScheme" ); + } + // Build Collected Signals for ( int signalIndex = 0; signalIndex < mProtoCollectionSchemeMessagePtr->signal_information_size(); ++signalIndex ) @@ -113,6 +257,9 @@ CollectionSchemeIngestion::build() signalInfo.minimumSampleIntervalMs = signalInformation.minimum_sample_period_ms(); signalInfo.fixedWindowPeriod = signalInformation.fixed_window_period_ms(); signalInfo.isConditionOnlySignal = signalInformation.condition_only_signal(); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + signalInfo.dataPartitionId = signalInformation.data_partition_id(); +#endif FWE_LOG_TRACE( "Adding signalID: " + std::to_string( signalInfo.signalID ) + " to list of signals to collect" + additionalTraceInfo ); @@ -139,6 +286,27 @@ CollectionSchemeIngestion::build() mCollectedRawCAN.emplace_back( rawCAN ); } +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // calculate total number of nodes ahead of time, + // as we can only allocate mExpressionNodes once + uint32_t numForwardConditionNodes = 0; + if ( mProtoCollectionSchemeMessagePtr->has_store_and_forward_configuration() ) + { + for ( auto i = 0; + i < mProtoCollectionSchemeMessagePtr->store_and_forward_configuration().partition_configuration_size(); + ++i ) + { + auto protoPartitionConfiguration = + mProtoCollectionSchemeMessagePtr->store_and_forward_configuration().partition_configuration( i ); + if ( protoPartitionConfiguration.has_upload_options() ) + { + numForwardConditionNodes += getNumberOfNodes( + protoPartitionConfiguration.upload_options().condition_tree(), MAX_EQUATION_DEPTH ); + } + } + } +#endif + // condition node if ( mProtoCollectionSchemeMessagePtr->collection_scheme_type_case() == Schemas::CollectionSchemesMsg::CollectionScheme::kConditionBasedCollectionScheme ) @@ -146,6 +314,9 @@ CollectionSchemeIngestion::build() auto numNodes = getNumberOfNodes( mProtoCollectionSchemeMessagePtr->condition_based_collection_scheme().condition_tree(), MAX_EQUATION_DEPTH ); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + numNodes += numForwardConditionNodes; +#endif FWE_LOG_INFO( "CollectionScheme is Condition Based. Building AST with " + std::to_string( numNodes ) + " nodes" ); @@ -171,6 +342,10 @@ CollectionSchemeIngestion::build() .time_based_collection_scheme_period_ms() ) + " ms" ); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + mExpressionNodes.reserve( numForwardConditionNodes + 1 ); +#endif + mExpressionNodes.emplace_back(); ExpressionNode ¤tNode = mExpressionNodes.back(); currentNode.booleanValue = true; @@ -200,6 +375,41 @@ CollectionSchemeIngestion::build() } #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + if ( mProtoCollectionSchemeMessagePtr->has_store_and_forward_configuration() ) + { + FWE_LOG_INFO( "Store and Forward configuration was set CollectionScheme ID: " + + mProtoCollectionSchemeMessagePtr->campaign_sync_id() ); + for ( auto i = 0; + i < mProtoCollectionSchemeMessagePtr->store_and_forward_configuration().partition_configuration_size(); + ++i ) + { + auto protoPartitionConfiguration = + mProtoCollectionSchemeMessagePtr->store_and_forward_configuration().partition_configuration( i ); + PartitionConfiguration partitionConfiguration; + if ( protoPartitionConfiguration.has_storage_options() ) + { + partitionConfiguration.storageOptions.storageLocation = + protoPartitionConfiguration.storage_options().storage_location(); + partitionConfiguration.storageOptions.maximumSizeInBytes = + protoPartitionConfiguration.storage_options().maximum_size_in_bytes(); + partitionConfiguration.storageOptions.minimumTimeToLiveInSeconds = + protoPartitionConfiguration.storage_options().minimum_time_to_live_in_seconds(); + } + if ( protoPartitionConfiguration.has_upload_options() ) + { + auto currentIndex = mExpressionNodes.size(); + partitionConfiguration.uploadOptions.conditionTree = + serializeNode( protoPartitionConfiguration.upload_options().condition_tree(), + mExpressionNodes, + currentIndex, + MAX_EQUATION_DEPTH ); + } + mStoreAndForwardConfig.emplace_back( partitionConfiguration ); + } + } +#endif + FWE_LOG_INFO( "Successfully built CollectionScheme ID: " + mProtoCollectionSchemeMessagePtr->campaign_sync_id() ); // Set ready flag to true @@ -264,6 +474,18 @@ CollectionSchemeIngestion::getPartialSignalIdToSignalPathLookupTable() const } #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD +const ICollectionScheme::StoreAndForwardConfig & +CollectionSchemeIngestion::getStoreAndForwardConfiguration() const +{ + if ( !mReady ) + { + return INVALID_STORE_AND_FORWARD_CONFIG; + } + return mStoreAndForwardConfig; +} +#endif + const ICollectionScheme::ExpressionNode_t & CollectionSchemeIngestion::getAllExpressionNodes() const { @@ -374,6 +596,25 @@ CollectionSchemeIngestion::getNumberOfNodes( const Schemas::CommonTypesMsg::Cond sum += getNumberOfNodes( node.node_operator().right_child(), depth - 1 ); } } + else if ( node.node_case() == Schemas::CommonTypesMsg::ConditionNode::kNodeFunction ) + { + if ( node.node_function().functionType_case() == + Schemas::CommonTypesMsg::ConditionNode_NodeFunction::kCustomFunction ) + { + for ( int i = 0; i < node.node_function().custom_function().params_size(); i++ ) + { + sum += getNumberOfNodes( node.node_function().custom_function().params( i ), depth - 1 ); + } + } + else if ( node.node_function().functionType_case() == + Schemas::CommonTypesMsg::ConditionNode_NodeFunction::kIsNullFunction ) + { + if ( node.node_function().is_null_function().has_expression() ) + { + sum += getNumberOfNodes( node.node_function().is_null_function().expression(), depth - 1 ); + } + } + } return sum; } @@ -421,6 +662,13 @@ CollectionSchemeIngestion::serializeNode( const Schemas::CommonTypesMsg::Conditi std::to_string( static_cast( currentNode->booleanValue ) ) ); return currentNode; } + else if ( node.node_case() == Schemas::CommonTypesMsg::ConditionNode::kNodeStringValue ) + { + currentNode->stringValue = node.node_string_value(); + currentNode->nodeType = ExpressionNodeType::STRING; + FWE_LOG_TRACE( "Creating STRING node with value: " + currentNode->stringValue ); + return currentNode; + } else if ( node.node_case() == Schemas::CommonTypesMsg::ConditionNode::kNodeFunction ) { if ( node.node_function().functionType_case() == @@ -457,6 +705,69 @@ CollectionSchemeIngestion::serializeNode( const Schemas::CommonTypesMsg::Conditi FWE_LOG_TRACE( "Creating Window FUNCTION node for Signal ID:" + std::to_string( currentNode->signalID ) ); return currentNode; } + else if ( node.node_function().functionType_case() == + Schemas::CommonTypesMsg::ConditionNode_NodeFunction::kCustomFunction ) + { + currentNode->function.customFunctionName = node.node_function().custom_function().function_name(); + currentNode->function.customFunctionInvocationId = mCustomFunctionNextInvocationId; + mCustomFunctionNextInvocationId++; + bool isParamsValid = true; + + for ( int i = 0; i < node.node_function().custom_function().params_size(); i++ ) + { + FWE_LOG_TRACE( "Parsing param #" + std::to_string( i ) + " of CustomFunction node " + + currentNode->function.customFunctionName ); + + ExpressionNode *param = serializeNode( node.node_function().custom_function().params( i ), + expressionNodes, + nextIndex, + remainingDepth - 1 ); + + if ( param == nullptr ) + { + FWE_LOG_WARN( "Param #" + std::to_string( i ) + " of CustomFunction node " + + currentNode->function.customFunctionName + + " is invalid => will ignore the CustomFunction node" ); + isParamsValid = false; + break; + } + + currentNode->function.customFunctionParams.push_back( param ); + } + + if ( isParamsValid ) + { + currentNode->nodeType = ExpressionNodeType::CUSTOM_FUNCTION; + + FWE_LOG_TRACE( "Creating CustomFunction node with name " + currentNode->function.customFunctionName + + " and " + std::to_string( currentNode->function.customFunctionParams.size() ) + + " params" ); + + return currentNode; + } + } + else if ( node.node_function().functionType_case() == + Schemas::CommonTypesMsg::ConditionNode_NodeFunction::kIsNullFunction ) + { + // If no expression node as parameter this isNull is invalid + if ( node.node_function().is_null_function().has_expression() ) + { + currentNode->nodeType = ExpressionNodeType::IS_NULL_FUNCTION; + FWE_LOG_TRACE( "Processing isNull expression" ); + ExpressionNode *left = serializeNode( node.node_function().is_null_function().expression(), + expressionNodes, + nextIndex, + remainingDepth - 1 ); + + currentNode->left = left; + FWE_LOG_TRACE( "Setting right child to nullptr" ); + currentNode->right = nullptr; + + FWE_LOG_TRACE( "Creating IsNullFunction node" ); + return currentNode; + } + FWE_LOG_WARN( "Invalid isNull function" ); + } else { FWE_LOG_WARN( "Unsupported Function Node Type" ); @@ -539,6 +850,19 @@ CollectionSchemeIngestion::getCollectionSchemeID() const return mProtoCollectionSchemeMessagePtr->campaign_sync_id(); } +#ifdef FWE_FEATURE_STORE_AND_FORWARD +const std::string & +CollectionSchemeIngestion::getCampaignArn() const +{ + if ( !mReady ) + { + return INVALID_CAMPAIGN_ARN; + } + + return mProtoCollectionSchemeMessagePtr->campaign_arn(); +} +#endif + const SyncID & CollectionSchemeIngestion::getDecoderManifestID() const { @@ -718,5 +1042,38 @@ CollectionSchemeIngestion::getS3UploadMetadata() const } #endif +const ICollectionScheme::ExpressionNode_t & +CollectionSchemeIngestion::getAllExpressionNodesForFetchCondition() const +{ + if ( !mReady ) + { + return INVALID_EXPRESSION_NODES; + } + + return mExpressionNodesForFetchCondition; +} + +const ICollectionScheme::ExpressionNode_t & +CollectionSchemeIngestion::getAllExpressionNodesForFetchAction() const +{ + if ( !mReady ) + { + return INVALID_EXPRESSION_NODES; + } + + return mExpressionNodesForFetchAction; +} + +const ICollectionScheme::FetchInformation_t & +CollectionSchemeIngestion::getAllFetchInformations() const +{ + if ( !mReady ) + { + return INVALID_FETCH_INFORMATIONS; + } + + return mFetchInformations; +} + } // namespace IoTFleetWise } // namespace Aws diff --git a/src/CollectionSchemeIngestion.h b/src/CollectionSchemeIngestion.h index b20ae2c1..3af8e82c 100644 --- a/src/CollectionSchemeIngestion.h +++ b/src/CollectionSchemeIngestion.h @@ -3,6 +3,7 @@ #pragma once +#include "CollectionInspectionAPITypes.h" #include "ICollectionScheme.h" #include "SignalTypes.h" #include "collection_schemes.pb.h" @@ -15,6 +16,9 @@ #include #include #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include +#endif namespace Aws { @@ -55,6 +59,10 @@ class CollectionSchemeIngestion : public ICollectionScheme const SyncID &getCollectionSchemeID() const override; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + const std::string &getCampaignArn() const override; +#endif + const SyncID &getDecoderManifestID() const override; uint64_t getStartTime() const override; @@ -96,6 +104,15 @@ class CollectionSchemeIngestion : public ICollectionScheme S3UploadMetadata getS3UploadMetadata() const override; #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + const StoreAndForwardConfig &getStoreAndForwardConfiguration() const override; +#endif + + const ExpressionNode_t &getAllExpressionNodesForFetchCondition() const override; + const ExpressionNode_t &getAllExpressionNodesForFetchAction() const override; + + const FetchInformation_t &getAllFetchInformations() const override; + private: /** * @brief The CollectionScheme message that will hold the deserialized proto. @@ -139,6 +156,28 @@ class CollectionSchemeIngestion : public ICollectionScheme S3UploadMetadata mS3UploadMetadata; #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + /** + * @brief Configuration of store and forward campaign + */ + StoreAndForwardConfig mStoreAndForwardConfig; +#endif + + /** + * @brief Vector representing all expression nodes used for fetching conditions. + */ + ExpressionNode_t mExpressionNodesForFetchCondition; + + /** + * @brief Vector representing all expression nodes used for fetching actions. + */ + ExpressionNode_t mExpressionNodesForFetchAction; + + /** + * @brief Vector representing all fetch informations. + */ + FetchInformation_t mFetchInformations; + /** * @brief Function used to Flatten the Abstract Syntax Tree (AST) */ @@ -174,6 +213,11 @@ class CollectionSchemeIngestion : public ICollectionScheme */ PartialSignalID getOrInsertPartialSignalId( SignalID signalId, const Schemas::CommonTypesMsg::SignalPath &path ); #endif + + /** + * @brief Next custom function invocation ID to assign + */ + static CustomFunctionInvocationID mCustomFunctionNextInvocationId; // NOLINT }; } // namespace IoTFleetWise diff --git a/src/CollectionSchemeManager.cpp b/src/CollectionSchemeManager.cpp index 30919f82..f785d3ab 100644 --- a/src/CollectionSchemeManager.cpp +++ b/src/CollectionSchemeManager.cpp @@ -19,9 +19,17 @@ namespace IoTFleetWise CollectionSchemeManager::CollectionSchemeManager( std::shared_ptr schemaPersistencyPtr, CANInterfaceIDTranslator &canIDTranslator, std::shared_ptr checkinSender, - std::shared_ptr rawDataBufferManager ) + std::shared_ptr rawDataBufferManager +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , + GetActuatorNamesCallback getActuatorNamesCallback +#endif + ) : mCheckinSender( std::move( checkinSender ) ) , mRawDataBufferManager( std::move( rawDataBufferManager ) ) +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , mGetActuatorNamesCallback( std::move( getActuatorNamesCallback ) ) +#endif , mSchemaPersistency( std::move( schemaPersistencyPtr ) ) , mCANIDTranslator( canIDTranslator ) { @@ -122,6 +130,10 @@ CollectionSchemeManager::printWakeupStatus( std::string &wakeupStr ) const wakeupStr += mProcessCollectionScheme ? "Yes" : "No"; wakeupStr += ", the DecoderManifest: "; wakeupStr += mProcessDecoderManifest ? "Yes" : "No"; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + wakeupStr += ", the StateTemplate: "; + wakeupStr += mProcessStateTemplates ? "Yes" : "No"; +#endif } void @@ -132,11 +144,17 @@ CollectionSchemeManager::doWork( void *data ) // Retrieve data from persistent storage static_cast( collectionSchemeManager->retrieve( DataType::COLLECTION_SCHEME_LIST ) ); static_cast( collectionSchemeManager->retrieve( DataType::DECODER_MANIFEST ) ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + static_cast( collectionSchemeManager->retrieve( DataType::STATE_TEMPLATE_LIST ) ); +#endif bool initialCheckinDocumentsUpdate = true; while ( true ) { bool decoderManifestChanged = false; bool enabledCollectionSchemeMapChanged = false; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + bool stateTemplatesChanged = false; +#endif if ( collectionSchemeManager->mProcessDecoderManifest ) { collectionSchemeManager->mProcessDecoderManifest = false; @@ -157,13 +175,29 @@ CollectionSchemeManager::doWork( void *data ) } TraceModule::get().sectionEnd( TraceSection::MANAGER_COLLECTION_BUILD ); } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + if ( collectionSchemeManager->mProcessStateTemplates ) + { + collectionSchemeManager->mProcessStateTemplates = false; + TraceModule::get().sectionBegin( TraceSection::MANAGER_LAST_KNOWN_STATE_BUILD ); + if ( collectionSchemeManager->processStateTemplates() ) + { + stateTemplatesChanged = true; + } + TraceModule::get().sectionEnd( TraceSection::MANAGER_LAST_KNOWN_STATE_BUILD ); + } +#endif auto checkTime = collectionSchemeManager->mClock->timeSinceEpoch(); if ( collectionSchemeManager->checkTimeLine( checkTime ) ) { enabledCollectionSchemeMapChanged = true; } - bool documentsChanged = decoderManifestChanged || enabledCollectionSchemeMapChanged; + bool documentsChanged = decoderManifestChanged +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + || stateTemplatesChanged +#endif + || enabledCollectionSchemeMapChanged; if ( documentsChanged || initialCheckinDocumentsUpdate ) { @@ -176,17 +210,28 @@ CollectionSchemeManager::doWork( void *data ) TraceModule::get().sectionBegin( TraceSection::MANAGER_EXTRACTION ); FWE_LOG_TRACE( "Start extraction at system time " + std::to_string( checkTime.systemTimeMs ) ); auto inspectionMatrixOutput = std::make_shared(); - TraceModule::get().sectionBegin( TraceSection::COLLECTION_SCHEME_CHANGE_TO_FIRST_DATA ); - - // Extract InspectionMatrix from mEnabledCollectionSchemeMap - collectionSchemeManager->updateActiveCollectionSchemeListeners(); - collectionSchemeManager->matrixExtractor( inspectionMatrixOutput ); - std::string enabled; - std::string idle; - collectionSchemeManager->printExistingCollectionSchemes( enabled, idle ); - FWE_LOG_INFO( "FWE activated collection schemes:" + enabled + " using decoder manifest:" + - collectionSchemeManager->mCurrentDecoderManifestID + " resulting in " + - std::to_string( inspectionMatrixOutput->conditions.size() ) + " inspection conditions" ); + auto fetchMatrixOutput = std::make_shared(); + if ( decoderManifestChanged || enabledCollectionSchemeMapChanged ) + { + TraceModule::get().sectionBegin( TraceSection::COLLECTION_SCHEME_CHANGE_TO_FIRST_DATA ); + + // Extract InspectionMatrix and FetchMatrix from mEnabledCollectionSchemeMap + collectionSchemeManager->updateActiveCollectionSchemeListeners(); + collectionSchemeManager->matrixExtractor( inspectionMatrixOutput, fetchMatrixOutput ); + std::string enabled; + std::string idle; + collectionSchemeManager->printExistingCollectionSchemes( enabled, idle ); + FWE_LOG_INFO( "FWE activated collection schemes:" + enabled + " using decoder manifest:" + + collectionSchemeManager->mCurrentDecoderManifestID + " resulting in " + + std::to_string( inspectionMatrixOutput->conditions.size() ) + " inspection conditions" ); + } + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + if ( decoderManifestChanged || stateTemplatesChanged ) + { + collectionSchemeManager->lastKnownStateUpdater( collectionSchemeManager->lastKnownStateExtractor() ); + } +#endif // Extract decoder dictionary std::map> decoderDictionaryMap; @@ -200,12 +245,18 @@ CollectionSchemeManager::doWork( void *data ) // Only notify the listeners after both have been extracted since the decoder dictionary // extraction might have modified the inspection matrix. collectionSchemeManager->decoderDictionaryUpdater( decoderDictionaryMap ); - collectionSchemeManager->inspectionMatrixUpdater( inspectionMatrixOutput ); + if ( decoderManifestChanged || enabledCollectionSchemeMapChanged ) + { + collectionSchemeManager->inspectionMatrixUpdater( inspectionMatrixOutput ); + collectionSchemeManager->fetchMatrixUpdater( fetchMatrixOutput ); + } // Update the Raw Buffer Config if ( collectionSchemeManager->mRawDataBufferManager != nullptr ) { std::unordered_map updatedSignals; + collectionSchemeManager->updateRawDataBufferConfigStringSignals( updatedSignals ); + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA std::shared_ptr complexDataDictionary; auto decoderDictionary = decoderDictionaryMap.find( VehicleDataSourceProtocol::COMPLEX_DATA ); @@ -303,6 +354,12 @@ CollectionSchemeManager::updateCheckinDocuments() { checkinMsg.emplace_back( mCurrentDecoderManifestID ); } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + for ( auto &stateTemplate : mStateTemplates ) + { + checkinMsg.emplace_back( stateTemplate.second->id ); + } +#endif mCheckinSender->onCheckinDocumentsChanged( checkinMsg ); } @@ -326,6 +383,17 @@ CollectionSchemeManager::onDecoderManifestUpdate( const IDecoderManifestPtr &dec mWait.notify(); } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +void +CollectionSchemeManager::onStateTemplatesChanged( std::shared_ptr lastKnownStateIngestion ) +{ + std::lock_guard lock( mSchemaUpdateMutex ); + mLastKnownStateIngestionInput = lastKnownStateIngestion; + mStateTemplatesAvailable = true; + mWait.notify(); +} +#endif + void CollectionSchemeManager::updateAvailable() { @@ -342,6 +410,14 @@ CollectionSchemeManager::updateAvailable() mProcessDecoderManifest = true; } mDecoderManifestAvailable = false; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + if ( mStateTemplatesAvailable && mLastKnownStateIngestionInput != nullptr ) + { + mLastKnownStateIngestion = mLastKnownStateIngestionInput; + mProcessStateTemplates = true; + } + mStateTemplatesAvailable = false; +#endif } bool @@ -399,6 +475,10 @@ CollectionSchemeManager::processDecoderManifest() // store the new DM, update mCurrentDecoderManifestID mCurrentDecoderManifestID = mDecoderManifest->getID(); store( DataType::DECODER_MANIFEST ); + + // Notify components about custom signal decoder format map change + mCustomSignalDecoderFormatMapChangeListeners.notify( + mCurrentDecoderManifestID, mDecoderManifest->getSignalIDToCustomSignalDecoderFormatMap() ); return true; } @@ -433,6 +513,60 @@ CollectionSchemeManager::processCollectionScheme() } } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +bool +CollectionSchemeManager::processStateTemplates() +{ + if ( ( mLastKnownStateIngestion == nullptr ) || ( !mLastKnownStateIngestion->build() ) ) + { + FWE_LOG_ERROR( "Incoming StateTemplate does not exist or fails to build" ); + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::STATE_TEMPLATE_ERROR ); + return false; + } + + auto stateTemplatesDiff = mLastKnownStateIngestion->getStateTemplatesDiff(); + if ( stateTemplatesDiff == nullptr ) + { + return false; + } + + if ( stateTemplatesDiff->version < mLastStateTemplatesDiffVersion ) + { + FWE_LOG_TRACE( "Ignoring state templates diff with version " + std::to_string( stateTemplatesDiff->version ) + + " as it is older than the current version " + std::to_string( mLastStateTemplatesDiffVersion ) ); + return false; + } + + mLastStateTemplatesDiffVersion = stateTemplatesDiff->version; + bool modified = false; + + for ( const auto &stateTemplateId : stateTemplatesDiff->stateTemplatesToRemove ) + { + if ( mStateTemplates.erase( stateTemplateId ) != 0U ) + { + modified = true; + } + } + + for ( const auto &stateTemplate : stateTemplatesDiff->stateTemplatesToAdd ) + { + if ( mStateTemplates.find( stateTemplate->id ) != mStateTemplates.end() ) + { + continue; + } + modified = true; + mStateTemplates.emplace( stateTemplate->id, stateTemplate ); + } + + if ( modified ) + { + store( DataType::STATE_TEMPLATE_LIST ); + } + + return modified; +} +#endif + TimePoint CollectionSchemeManager::calculateMonotonicTime( const TimePoint &currTime, Timestamp systemTimeMs ) { diff --git a/src/CollectionSchemeManager.h b/src/CollectionSchemeManager.h index c4aabdf5..22c7efe5 100644 --- a/src/CollectionSchemeManager.h +++ b/src/CollectionSchemeManager.h @@ -9,6 +9,7 @@ #include "Clock.h" #include "ClockHandler.h" #include "CollectionInspectionAPITypes.h" +#include "DataFetchManagerAPITypes.h" #include "ICollectionScheme.h" #include "ICollectionSchemeList.h" #include "IDecoderDictionary.h" @@ -28,11 +29,16 @@ #include #include #include +#include #include #ifdef FWE_FEATURE_VISION_SYSTEM_DATA #include "MessageTypes.h" -#include +#endif + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateIngestion.h" +#include "LastKnownStateTypes.h" #endif namespace Aws @@ -90,6 +96,15 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding using OnInspectionMatrixChangeCallback = std::function &inspectionMatrix )>; + /** + * @brief Callback to notify the change of fetch configuration matrix. + * Need to be used along with inspection matrix change callback. + * + * @param fetchMatrix - all currently active fetch configuration. + * @return none + * */ + using OnFetchMatrixChangeCallback = std::function &fetchMatrix )>; + /** * @brief Callback to notify the change of active collection schemes * @@ -97,6 +112,24 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding using OnCollectionSchemeListChangeCallback = std::function &activeCollectionSchemes )>; + /** + * @brief Callback to notify about the change of custom signal decoder format map. + * It is used to notify data consumers, not the data sources. + * @param currentDecoderManifestID sync id of the decoder manifest that is used + * @param customSignalDecoderFormatMap const shared pointer pointing to a constant custom signal decoder format map + * */ + using OnCustomSignalDecoderFormatMapChangeCallback = + std::function; + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + using OnStateTemplatesChangeCallback = std::function stateTemplates )>; +#endif + +#ifdef FWE_FEATURE_REMOTE_COMMANDS + using GetActuatorNamesCallback = std::function>()>; +#endif + CollectionSchemeManager( std::shared_ptr schemaPersistencyPtr, /**< shared pointer to collectionSchemePersistency object */ @@ -107,6 +140,12 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding std::shared_ptr rawDataBufferManager = nullptr /**< rawDataBufferManager Optional manager to handle raw data. If not given, raw data collection will be disabled */ +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , + GetActuatorNamesCallback getActuatorNamesCallback = + nullptr /**< Callback to get the names of actuators. TODO: Once the decoder manifest supports the + READ/WRITE/READ_WRITE indication for each signal, this can be removed */ +#endif ); ~CollectionSchemeManager(); @@ -157,6 +196,10 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding */ void onDecoderManifestUpdate( const IDecoderManifestPtr &decoderManifest ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + void onStateTemplatesChanged( std::shared_ptr lastKnownStateIngestion ); +#endif + /** * @brief Returns the current list of collection scheme ARNs * @return List of collection scheme ARNs @@ -183,6 +226,16 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding mInspectionMatrixChangeListeners.subscribe( callback ); } + /** + * @brief Subscribe to changes in the fetch matrix + * @param callback - function that will be called when the fetch matrix changes + */ + void + subscribeToFetchMatrixChange( OnFetchMatrixChangeCallback callback ) + { + mFetchMatrixChangeListeners.subscribe( callback ); + } + /** * @brief Subscribe to changes in the collection scheme list * @param callback A function that will be called when the collection scheme list changes @@ -193,6 +246,24 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding mCollectionSchemeListChangeListeners.subscribe( callback ); } + /** + * @brief Subscribe to changes in the custom signal decoder format map + * @param callback A function that will be called when the custom signal decoder format map changes + */ + void + subscribeToCustomSignalDecoderFormatMapChange( OnCustomSignalDecoderFormatMapChangeCallback callback ) + { + mCustomSignalDecoderFormatMapChangeListeners.subscribe( callback ); + } + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + void + subscribeToStateTemplatesChange( OnStateTemplatesChangeCallback callback ) + { + mStateTemplatesChangeListeners.subscribe( callback ); + } +#endif + private: /** * @brief Starts main thread @@ -308,6 +379,13 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding #endif ); + /** + * @brief Fills up and creates the BufferConfig with string signals + * @param updatedSignals map of the signals that will be updated by Raw Buffer Manager + */ + void updateRawDataBufferConfigStringSignals( + std::unordered_map &updatedSignals ); + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA /** * @brief only executed from within decoderDictionaryExtractor to put a complex signal into the dictionary @@ -346,10 +424,13 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding void decoderDictionaryUpdater( std::map> &decoderDictionaryMap ); - void matrixExtractor( const std::shared_ptr &inspectionMatrix ); + void matrixExtractor( const std::shared_ptr &inspectionMatrix, + const std::shared_ptr &fetchMatrix ); void inspectionMatrixUpdater( const std::shared_ptr &inspectionMatrix ); + void fetchMatrixUpdater( const std::shared_ptr &fetchMatrix ); + bool retrieve( DataType retrieveType ); void store( DataType storeType ); @@ -358,6 +439,13 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding bool processCollectionScheme(); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + bool processStateTemplates(); + + std::shared_ptr lastKnownStateExtractor(); + void lastKnownStateUpdater( std::shared_ptr stateTemplates ); +#endif + void updateCheckinDocuments(); void updateAvailable(); @@ -420,7 +508,12 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding ThreadSafeListeners mActiveDecoderDictionaryChangeListeners; ThreadSafeListeners mInspectionMatrixChangeListeners; + ThreadSafeListeners mFetchMatrixChangeListeners; ThreadSafeListeners mCollectionSchemeListChangeListeners; + ThreadSafeListeners mCustomSignalDecoderFormatMapChangeListeners; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + ThreadSafeListeners mStateTemplatesChangeListeners; +#endif /* * parameters used in onCollectionSchemeAvailable() @@ -444,6 +537,19 @@ class CollectionSchemeManager // NOLINT(clang-analyzer-optin.performance.Padding */ IDecoderManifestPtr mDecoderManifestInput; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + bool mStateTemplatesAvailable{ false }; + bool mProcessStateTemplates{ false }; + std::shared_ptr mLastKnownStateIngestionInput; + std::shared_ptr mLastKnownStateIngestion; + std::unordered_map> mStateTemplates; + uint64_t mLastStateTemplatesDiffVersion{ 0 }; +#endif + +#ifdef FWE_FEATURE_REMOTE_COMMANDS + GetActuatorNamesCallback mGetActuatorNamesCallback; +#endif + // flag used by main thread to check if collectionScheme needs to be processed bool mProcessCollectionScheme{ false }; // flag used by main thread to check if DM needs to be processed diff --git a/src/CommandResponseDataSender.cpp b/src/CommandResponseDataSender.cpp new file mode 100644 index 00000000..ccd0cf63 --- /dev/null +++ b/src/CommandResponseDataSender.cpp @@ -0,0 +1,201 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CommandResponseDataSender.h" +#include "CommandTypes.h" +#include "ICommandDispatcher.h" +#include "IConnectionTypes.h" +#include "LoggingModule.h" +#include "TopicConfig.h" +#include "TraceModule.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class CommandResponseDataToPersist : public DataToPersist +{ +public: + CommandResponseDataToPersist( CommandID commandID, std::shared_ptr data ) + : mCommandID( std::move( commandID ) ) + , mData( std::move( data ) ) + { + } + + SenderDataType + getDataType() const override + { + return SenderDataType::COMMAND_RESPONSE; + } + + Json::Value + getMetadata() const override + { + Json::Value metadata; + metadata["commandID"] = mCommandID; + return metadata; + } + + std::string + getFilename() const override + { + return "command-" + mCommandID + ".bin"; + }; + + boost::variant, std::shared_ptr> + getData() const override + { + return mData; + } + +private: + CommandID mCommandID; + std::shared_ptr mData; +}; + +static Schemas::Commands::Status +internalCommandStatusToProto( CommandStatus status ) +{ + // coverity[misra_cpp_2008_rule_6_4_6_violation] compiler warning is preferred over a default-clause + switch ( status ) + { + case CommandStatus::SUCCEEDED: + return Schemas::Commands::COMMAND_STATUS_SUCCEEDED; + case CommandStatus::EXECUTION_TIMEOUT: + return Schemas::Commands::COMMAND_STATUS_EXECUTION_TIMEOUT; + case CommandStatus::EXECUTION_FAILED: + return Schemas::Commands::COMMAND_STATUS_EXECUTION_FAILED; + case CommandStatus::IN_PROGRESS: + return Schemas::Commands::COMMAND_STATUS_IN_PROGRESS; + } + return Schemas::Commands::COMMAND_STATUS_UNSPECIFIED; +} + +CommandResponseDataSender::CommandResponseDataSender( std::shared_ptr commandResponseSender ) + : mCommandResponseSender( std::move( commandResponseSender ) ) +{ +} + +void +CommandResponseDataSender::processData( std::shared_ptr data, OnDataProcessedCallback callback ) +{ + if ( data == nullptr ) + { + FWE_LOG_WARN( "Nothing to send as the input is empty" ); + return; + } + + auto commandResponse = std::dynamic_pointer_cast( data ); + if ( commandResponse == nullptr ) + { + FWE_LOG_WARN( "Nothing to send as the input is not a valid CommandResponse" ); + return; + } + + if ( mCommandResponseSender == nullptr ) + { + FWE_LOG_ERROR( "No sender for command response provided" ); + return; + } + + FWE_LOG_INFO( "Ready to send response for command with ID: " + commandResponse->id ); + + auto protoStatus = internalCommandStatusToProto( commandResponse->status ); + if ( protoStatus == Schemas::Commands::COMMAND_STATUS_UNSPECIFIED ) + { + FWE_LOG_ERROR( "Unknown command status: " + std::to_string( static_cast( commandResponse->status ) ) ); + return; + } + + mProtoCommandResponseMsg.set_command_id( commandResponse->id ); + mProtoCommandResponseMsg.set_status( protoStatus ); + mProtoCommandResponseMsg.set_reason_code( commandResponse->reasonCode ); + mProtoCommandResponseMsg.set_reason_description( commandResponse->reasonDescription ); + + auto protoOutput = std::make_shared(); + + if ( !mProtoCommandResponseMsg.SerializeToString( &( *protoOutput ) ) ) + { + FWE_LOG_ERROR( "Serialization failed for command response with ID: " + commandResponse->id ); + return; + } + + mCommandResponseSender->sendBuffer( + mCommandResponseSender->getTopicConfig().commandResponseTopic( commandResponse->id ), + reinterpret_cast( protoOutput->data() ), + protoOutput->size(), + [protoOutput, commandId = commandResponse->id, callback]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_INFO( "A command response payload of size: " + std::to_string( protoOutput->size() ) + + " bytes has been uploaded for command ID: " + commandId ); + callback( true, nullptr ); + } + else + { + FWE_LOG_ERROR( "Failed to send command response for command ID: " + commandId + + " with error: " + std::to_string( static_cast( result ) ) ); + callback( false, std::make_shared( commandId, protoOutput ) ); + } + }, + QoS::AT_LEAST_ONCE ); + + TraceModule::get().incrementVariable( TraceVariable::MQTT_COMMAND_RESPONSE_MESSAGE_SENT_OUT ); + TraceModule::get().decrementAtomicVariable( TraceAtomicVariable::QUEUE_PENDING_COMMAND_RESPONSES ); +} + +void +CommandResponseDataSender::processPersistedData( std::istream &data, + const Json::Value &metadata, + OnPersistedDataProcessedCallback callback ) +{ + auto commandID = metadata["commandID"].asString(); + + if ( !mCommandResponseSender->isAlive() ) + { + callback( false ); + return; + } + + data.seekg( 0, std::ios::end ); + auto size = data.tellg(); + auto dataAsArray = std::vector( static_cast( size ) ); + data.seekg( 0, std::ios::beg ); + data.read( dataAsArray.data(), static_cast( size ) ); + + if ( !data.good() ) + { + FWE_LOG_ERROR( "Failed to read persisted command response for commandID '" + commandID + "'" ); + callback( false ); + return; + } + + auto buf = reinterpret_cast( dataAsArray.data() ); + auto bufSize = static_cast( size ); + mCommandResponseSender->sendBuffer( + mCommandResponseSender->getTopicConfig().commandResponseTopic( commandID ), + buf, + bufSize, + [callback, size]( ConnectivityError result ) { + if ( result != ConnectivityError::Success ) + { + callback( false ); + return; + } + + FWE_LOG_INFO( "A Payload of size: " + std::to_string( size ) + " bytes has been uploaded" ); + callback( true ); + }, + QoS::AT_LEAST_ONCE ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CommandResponseDataSender.h b/src/CommandResponseDataSender.h new file mode 100644 index 00000000..50a675c1 --- /dev/null +++ b/src/CommandResponseDataSender.h @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "DataSenderTypes.h" +#include "ISender.h" +#include "command_response.pb.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Sends command responses + */ +class CommandResponseDataSender : public DataSender +{ + +public: + CommandResponseDataSender( std::shared_ptr commandResponseSender ); + + ~CommandResponseDataSender() override = default; + + /** + * @brief Process command response and prepare data for upload + */ + void processData( std::shared_ptr data, OnDataProcessedCallback callback ) override; + + void processPersistedData( std::istream &data, + const Json::Value &metadata, + OnPersistedDataProcessedCallback callback ) override; + +private: + /** + * @brief member variable used to hold the command response data and minimize heap fragmentation + */ + Schemas::Commands::CommandResponse mProtoCommandResponseMsg; + std::shared_ptr mCommandResponseSender; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CommandSchema.cpp b/src/CommandSchema.cpp new file mode 100644 index 00000000..2bb5bd91 --- /dev/null +++ b/src/CommandSchema.cpp @@ -0,0 +1,290 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CommandSchema.h" +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include "LoggingModule.h" +#include "QueueTypes.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include "TraceModule.h" +#include "command_request.pb.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +static CommandReasonCode +setSignalValue( const Schemas::Commands::ActuatorCommand &protoActuatorCommand, + ActuatorCommandRequest &commandRequest, + Timestamp receivedTime, + std::shared_ptr &rawBufferManager ) +{ + if ( protoActuatorCommand.has_double_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.double_value(), SignalType::DOUBLE ); + } + else if ( protoActuatorCommand.has_float_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.float_value(), SignalType::FLOAT ); + } + else if ( protoActuatorCommand.has_boolean_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.boolean_value(), SignalType::BOOLEAN ); + } + else if ( protoActuatorCommand.has_uint8_value() ) + { + auto value = protoActuatorCommand.uint8_value(); + if ( value > UINT8_MAX ) + { + FWE_LOG_ERROR( "Invalid command '" + commandRequest.commandID + + "', uint8 value out of range: " + std::to_string( value ) ); + return REASON_CODE_ARGUMENT_OUT_OF_RANGE; + } + commandRequest.signalValueWrapper.setVal( static_cast( value ), SignalType::UINT8 ); + } + else if ( protoActuatorCommand.has_int8_value() ) + { + auto value = protoActuatorCommand.int8_value(); + if ( ( value < INT8_MIN ) || ( value > INT8_MAX ) ) + { + FWE_LOG_ERROR( "Invalid command '" + commandRequest.commandID + + "', int8 value out of range: " + std::to_string( value ) ); + return REASON_CODE_ARGUMENT_OUT_OF_RANGE; + } + commandRequest.signalValueWrapper.setVal( static_cast( value ), SignalType::INT8 ); + } + else if ( protoActuatorCommand.has_uint16_value() ) + { + auto value = protoActuatorCommand.uint16_value(); + if ( value > UINT16_MAX ) + { + FWE_LOG_ERROR( "Invalid command '" + commandRequest.commandID + + "', uint16 value out of range: " + std::to_string( value ) ); + return REASON_CODE_ARGUMENT_OUT_OF_RANGE; + } + commandRequest.signalValueWrapper.setVal( static_cast( value ), SignalType::UINT16 ); + } + else if ( protoActuatorCommand.has_int16_value() ) + { + auto value = protoActuatorCommand.int16_value(); + if ( ( value < INT16_MIN ) || ( value > INT16_MAX ) ) + { + FWE_LOG_ERROR( "Invalid command '" + commandRequest.commandID + + "', int16 value out of range: " + std::to_string( value ) ); + return REASON_CODE_ARGUMENT_OUT_OF_RANGE; + } + commandRequest.signalValueWrapper.setVal( static_cast( value ), SignalType::INT16 ); + } + else if ( protoActuatorCommand.has_uint32_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.uint32_value(), SignalType::UINT32 ); + } + else if ( protoActuatorCommand.has_int32_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.int32_value(), SignalType::INT32 ); + } + else if ( protoActuatorCommand.has_uint64_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.uint64_value(), SignalType::UINT64 ); + } + else if ( protoActuatorCommand.has_int64_value() ) + { + commandRequest.signalValueWrapper.setVal( protoActuatorCommand.int64_value(), SignalType::INT64 ); + } + else if ( protoActuatorCommand.has_string_value() ) + { + auto signalId = protoActuatorCommand.signal_id(); + auto bufferHandle = + rawBufferManager->push( reinterpret_cast( protoActuatorCommand.string_value().data() ), + protoActuatorCommand.string_value().size(), + receivedTime, + signalId ); + if ( bufferHandle == RawData::INVALID_BUFFER_HANDLE ) + { + FWE_LOG_WARN( "Signal id " + std::to_string( signalId ) + " was rejected by RawBufferManager" ); + return REASON_CODE_REJECTED; + } + // immediately set usage hint so buffer handle does not get directly deleted again + rawBufferManager->increaseHandleUsageHint( signalId, bufferHandle, RawData::BufferHandleUsageStage::UPLOADING ); + commandRequest.signalValueWrapper.setVal( + SignalValue::RawDataVal{ signalId, bufferHandle }, SignalType::STRING ); + } + else + { + FWE_LOG_ERROR( "Invalid command '" + commandRequest.commandID + "', none of the expected value fields is set" ); + return REASON_CODE_COMMAND_REQUEST_PARSING_FAILED; + } + + return REASON_CODE_UNSPECIFIED; +} + +CommandSchema::CommandSchema( std::shared_ptr receiverCommandRequest, + std::shared_ptr commandResponses, + std::shared_ptr rawBufferManager ) + : mCommandResponses( std::move( commandResponses ) ) + , mRawBufferManager( std::move( rawBufferManager ) ) +{ + // Register the listeners + receiverCommandRequest->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onCommandRequestReceived( receivedMessage ); + } ); +} + +void +CommandSchema::onCommandRequestReceived( const ReceivedConnectivityMessage &receivedMessage ) +{ + // Check for a empty input data + if ( ( receivedMessage.buf == nullptr ) || ( receivedMessage.size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty command data from Cloud" ); + return; + } + + TraceModule::get().incrementVariable( TraceVariable::COMMAND_REQUESTS_RECEIVED ); + + Schemas::Commands::CommandRequest protoCommandRequest; + // Verify we have not accidentally linked against a version of the library which is incompatible with the version of + // the headers we compiled with. + GOOGLE_PROTOBUF_VERIFY_VERSION; + + if ( !protoCommandRequest.ParseFromArray( receivedMessage.buf, static_cast( receivedMessage.size ) ) ) + { + FWE_LOG_ERROR( "Failed to parse CommandRequest proto" ); + mCommandResponses->push( std::make_shared( + "", CommandStatus::EXECUTION_FAILED, REASON_CODE_COMMAND_REQUEST_PARSING_FAILED, "" ) ); + return; + } + + // Check if command has already timed out when it was received: + auto currentTimeMs = mClock->systemTimeSinceEpochMs(); + auto issuedTimestampMs = protoCommandRequest.issued_timestamp_ms(); + if ( issuedTimestampMs == 0 ) // TODO: Remove this if once cloud supports issued_timestamp_ms + { + issuedTimestampMs = currentTimeMs; + } + if ( currentTimeMs < issuedTimestampMs ) + { + FWE_LOG_WARN( "Issued time " + std::to_string( issuedTimestampMs ) + " is later than current time " + + std::to_string( currentTimeMs ) + " for command id " + protoCommandRequest.command_id() ); + } + if ( ( protoCommandRequest.timeout_ms() > 0 ) && + ( ( issuedTimestampMs + protoCommandRequest.timeout_ms() ) <= currentTimeMs ) ) + { + FWE_LOG_ERROR( "Command Request with ID " + protoCommandRequest.command_id() + " timed out" ); + mCommandResponses->push( std::make_shared( protoCommandRequest.command_id(), + CommandStatus::EXECUTION_TIMEOUT, + REASON_CODE_TIMED_OUT_BEFORE_DISPATCH, + "" ) ); + return; + } + + if ( protoCommandRequest.has_actuator_command() ) + { + FWE_LOG_INFO( "Building ActuatorCommandRequest with ID: " + protoCommandRequest.command_id() ); + ActuatorCommandRequest commandRequest; + + auto &protoActuatorCommand = protoCommandRequest.actuator_command(); + + commandRequest.commandID = protoCommandRequest.command_id(); + commandRequest.signalID = protoActuatorCommand.signal_id(); + commandRequest.issuedTimestampMs = issuedTimestampMs; + commandRequest.executionTimeoutMs = protoCommandRequest.timeout_ms(); + commandRequest.decoderID = protoActuatorCommand.decoder_manifest_sync_id(); + + auto reasonCode = setSignalValue( protoActuatorCommand, commandRequest, currentTimeMs, mRawBufferManager ); + if ( reasonCode != REASON_CODE_UNSPECIFIED ) + { + mCommandResponses->push( std::make_shared( + commandRequest.commandID, CommandStatus::EXECUTION_FAILED, reasonCode, "" ) ); + return; + } + + mActuatorCommandRequestListeners.notify( commandRequest ); + } + else if ( protoCommandRequest.has_last_known_state_command() ) + { + FWE_LOG_INFO( "Building LastKnownStateCommandRequest with ID: " + protoCommandRequest.command_id() ); + auto &protoLastKnownStateCommand = protoCommandRequest.last_known_state_command(); + + if ( protoLastKnownStateCommand.state_template_information_size() == 0 ) + { + FWE_LOG_ERROR( "Invalid command '" + protoCommandRequest.command_id() + + "', no state template information provided" ); + return; + } + + for ( const auto &stateTemplateInformation : protoLastKnownStateCommand.state_template_information() ) + { + LastKnownStateCommandRequest commandRequest; + commandRequest.commandID = protoCommandRequest.command_id(); + commandRequest.stateTemplateID = stateTemplateInformation.state_template_sync_id(); + commandRequest.receivedTime = mClock->timeSinceEpoch(); + + if ( stateTemplateInformation.has_activate_operation() ) + { + commandRequest.operation = LastKnownStateOperation::ACTIVATE; + commandRequest.deactivateAfterSeconds = + stateTemplateInformation.activate_operation().deactivate_after_seconds(); + } + else if ( stateTemplateInformation.has_deactivate_operation() ) + { + commandRequest.operation = LastKnownStateOperation::DEACTIVATE; + } + else if ( stateTemplateInformation.has_fetch_snapshot_operation() ) + { + commandRequest.operation = LastKnownStateOperation::FETCH_SNAPSHOT; + } + else + { + FWE_LOG_ERROR( "Invalid state template information for state template ID '" + + commandRequest.stateTemplateID + "' and command ID '" + commandRequest.commandID + + "', none of the expected operation fields is present" ); + continue; + } + + mLastKnownStateCommandRequestListeners.notify( commandRequest ); + } + } + else + { + FWE_LOG_ERROR( "Invalid command '" + protoCommandRequest.command_id() + + "', none of the expected command types is present" ); + } +} + +void +CommandSchema::onRejectedCommandResponseReceived( const ReceivedConnectivityMessage &receivedMessage ) +{ + Json::Reader reader; + Json::Value root; + if ( !reader.parse( std::string( receivedMessage.buf, receivedMessage.buf + receivedMessage.size ), root ) ) + { + FWE_LOG_ERROR( "A command response was rejected, but the rejected message could not be parsed." ); + return; + } + + std::string error; + if ( root.isMember( "error" ) ) + { + error = root["error"].asString(); + } + + std::string errorMessage; + if ( root.isMember( "errorMessage" ) ) + { + errorMessage = root["errorMessage"].asString(); + } + + FWE_LOG_ERROR( "A command response was rejected. Error: '" + error + "', Message: '" + errorMessage + "'" ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CommandSchema.h b/src/CommandSchema.h new file mode 100644 index 00000000..c1aecc40 --- /dev/null +++ b/src/CommandSchema.h @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "IReceiver.h" +#include "Listener.h" +#include "RawDataManager.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Setting a CommandRequest proto byte size limit for file received from Cloud + */ +constexpr size_t COMMAND_REQUEST_BYTE_SIZE_LIMIT = 128000000; + +/** + * @brief This class handles received and sent Command messages + */ +class CommandSchema +{ +public: + /** + * @brief Callback function used to notify when a new ActuatorCommandRequest arrives from the Cloud. + * + * @param commandRequest + */ + using OnActuatorCommandRequestReceivedCallback = + std::function; + + /** + * @brief Callback function used to notify when a new LastKnownStateCommandRequest arrives from the Cloud. + * + * @param commandRequest + */ + using OnLastKnownStateCommandRequestReceivedCallback = + std::function; + + /** + * @param receiverCommandRequest Receiver for a command_request.proto message on a CommandRequest topic + * @param commandResponses queue to send command responses in case of failure when processing a received command + * @param rawBufferManager Raw data buffer manager for storing string signal values + */ + CommandSchema( std::shared_ptr receiverCommandRequest, + std::shared_ptr commandResponses, + std::shared_ptr rawBufferManager ); + + ~CommandSchema() = default; + + CommandSchema( const CommandSchema & ) = delete; + CommandSchema &operator=( const CommandSchema & ) = delete; + CommandSchema( CommandSchema && ) = delete; + CommandSchema &operator=( CommandSchema && ) = delete; + + void + subscribeToActuatorCommandRequestReceived( OnActuatorCommandRequestReceivedCallback callback ) + { + mActuatorCommandRequestListeners.subscribe( callback ); + } + + void + subscribeToLastKnownStateCommandRequestReceived( OnLastKnownStateCommandRequestReceivedCallback callback ) + { + mLastKnownStateCommandRequestListeners.subscribe( callback ); + } + + /** + * @brief Callback called when receiving a new message confirming a command response was rejected. + * @param receivedMessage struct containing message data and metadata + */ + static void onRejectedCommandResponseReceived( const ReceivedConnectivityMessage &receivedMessage ); + +private: + /** + * @brief Callback that should be called whenever a new message with a CommandRequest is received from the Cloud. + * @param receivedMessage struct containing message data and metadata + */ + void onCommandRequestReceived( const ReceivedConnectivityMessage &receivedMessage ); + + ThreadSafeListeners mActuatorCommandRequestListeners; + ThreadSafeListeners mLastKnownStateCommandRequestListeners; + std::shared_ptr mCommandResponses; + + std::shared_ptr mClock = ClockHandler::getClock(); + std::shared_ptr mRawBufferManager; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CommandTypes.h b/src/CommandTypes.h new file mode 100644 index 00000000..945053bd --- /dev/null +++ b/src/CommandTypes.h @@ -0,0 +1,121 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +struct ActuatorCommandRequest +{ + + /** + * @brief Unique Command ID generated on the cloud + */ + CommandID commandID; + + /** + * @brief Decoder Manifest sync id associated with this command request + */ + SyncID decoderID; + + /** + * @brief Signal ID associated with the Command + */ + SignalID signalID{ 0 }; + + /** + * @brief Contains signal value and type to be set with the Command + */ + SignalValueWrapper signalValueWrapper; + + /** + * @brief Timestamp in ms since epoch of when the command was issued in the cloud + */ + Timestamp issuedTimestampMs{ 0 }; + + /** + * @brief Command execution timeout in ms since `issuedTimestampMs` + */ + Timestamp executionTimeoutMs{ 0 }; +}; + +enum class LastKnownStateOperation +{ + ACTIVATE = 0, + DEACTIVATE = 1, + FETCH_SNAPSHOT = 2, +}; + +struct LastKnownStateCommandRequest +{ + /** + * @brief Unique Command ID generated on the cloud + */ + CommandID commandID; + + /** + * @brief State template sync id associated with this command request + */ + SyncID stateTemplateID; + + /** + * @brief The operation that should be applied to this state template + */ + LastKnownStateOperation operation; + + /** + * @brief Make the collection to be stopped after this time. + * + * This is only used when the operation is ACTIVATE + */ + uint32_t deactivateAfterSeconds{ 0 }; + + /** + * @brief Time when this command was received + */ + TimePoint receivedTime{ 0, 0 }; +}; + +/** + * Response related to a single command to be sent to the cloud + */ +struct CommandResponse : DataToSend +{ + CommandID id; + CommandStatus status; + CommandReasonCode reasonCode; + CommandReasonDescription reasonDescription; + + CommandResponse( CommandID commandID, + CommandStatus commandStatus, + CommandReasonCode commandReasonCode, + CommandReasonDescription commandReasonDescription ) + : id( std::move( commandID ) ) + , status( std::move( commandStatus ) ) + , reasonCode( commandReasonCode ) + , reasonDescription( std::move( commandReasonDescription ) ) + { + } + + ~CommandResponse() override = default; + + SenderDataType + getDataType() const override + { + return SenderDataType::COMMAND_RESPONSE; + } +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CustomDataSource.cpp b/src/CustomDataSource.cpp deleted file mode 100644 index 3873fc2e..00000000 --- a/src/CustomDataSource.cpp +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "CustomDataSource.h" -#include "LoggingModule.h" -#include "Timer.h" -#include -#include -#include -#include - -namespace Aws -{ -namespace IoTFleetWise -{ -const char * -CustomDataSource::getThreadName() -{ - // Maximum allowed: 15 characters - return "CustomDataSource"; -} - -bool -CustomDataSource::start() -{ - // Prevent concurrent stop/init - std::lock_guard lock( mThreadMutex ); - // On multi core systems the shared variable mShouldStop must be updated for - // all cores before starting the thread otherwise thread will directly end - mShouldStop.store( false ); - // Make sure the thread goes into sleep immediately to wait for - // the manifest to be available - mShouldSleep.store( true ); - if ( !mThread.create( doWork, this ) ) - { - FWE_LOG_TRACE( "Thread failed to start" ); - } - else - { - FWE_LOG_TRACE( "Thread started" ); - const auto name = getThreadName(); - if ( name != nullptr ) - { - if ( strnlen( name, 16 ) > 15 ) - { - FWE_LOG_WARN( "Thread name '" + std::string( name ) + - "' is larger than 15 chars. Setting the thread name will likely fail." ); - } - mThread.setThreadName( name ); - } - } - - return mThread.isActive() && mThread.isValid(); -} - -SignalID -CustomDataSource::getSignalIdFromStartBit( uint16_t startBit ) -{ - std::lock_guard lock( mExtractionOngoing ); - for ( auto &signal : mUsedMessageFormat.mSignals ) - { - if ( signal.mFirstBitPosition == startBit ) - { - return signal.mSignalID; - } - } - return INVALID_SIGNAL_ID; -} - -std::vector -CustomDataSource::getSignalInfo() -{ - std::lock_guard lock( mExtractionOngoing ); - return mUsedMessageFormat.mSignals; -} - -bool -CustomDataSource::stop() -{ - std::lock_guard lock( mThreadMutex ); - mShouldStop.store( true ); - mWait.notify(); - mThread.release(); - mShouldStop.store( false, std::memory_order_relaxed ); - FWE_LOG_TRACE( "Thread stopped" ); - return !mThread.isActive(); -} - -bool -CustomDataSource::shouldStop() const -{ - return mShouldStop.load( std::memory_order_relaxed ); -} - -bool -CustomDataSource::shouldSleep() const -{ - return mShouldSleep.load( std::memory_order_relaxed ); -} - -void -CustomDataSource::doWork( void *data ) -{ - CustomDataSource *customDataSource = static_cast( data ); - Timer pollTimer; - while ( !customDataSource->shouldStop() ) - { - if ( customDataSource->shouldSleep() ) - { - // We either just started or there was a decoder dictionary update that we can't use. - // We should sleep - FWE_LOG_TRACE( "No valid decoding information available so sleep" ); - // Wait here for the decoder dictionary to come. - customDataSource->mWait.wait( Signal::WaitWithPredicate ); - // At this point, we should be able to see events coming as the channel is also - // woken up. - } - if ( customDataSource->mNewMessageFormatExtracted ) - { - std::lock_guard lock( customDataSource->mExtractionOngoing ); - customDataSource->mUsedMessageFormat = customDataSource->mExtractedMessageFormat; - customDataSource->mNewMessageFormatExtracted = false; - } - if ( ( !customDataSource->shouldSleep() ) && ( !customDataSource->mUsedMessageFormat.mSignals.empty() ) && - ( pollTimer.getElapsedMs().count() >= static_cast( customDataSource->mPollIntervalMs ) ) ) - { - customDataSource->pollData(); - pollTimer.reset(); - } - customDataSource->mWait.wait( customDataSource->mPollIntervalMs ); - } -} - -void -CustomDataSource::setFilter( CANChannelNumericID canChannel, CANRawFrameID canRawFrameId ) -{ - std::shared_ptr canDecoderDictionary; - { - std::lock_guard lock( mExtractionOngoing ); - mCanChannel = canChannel; - mCanRawFrameId = canRawFrameId; - canDecoderDictionary = mLastReceivedDictionary; - } - matchDictionaryToFilter( canDecoderDictionary, canChannel, canRawFrameId ); -} - -void -CustomDataSource::matchDictionaryToFilter( std::shared_ptr &dictionary, - CANChannelNumericID canChannel, - CANRawFrameID canRawFrameId ) -{ - if ( canChannel == INVALID_CAN_SOURCE_NUMERIC_ID ) - { - FWE_LOG_TRACE( "CAN channel invalid, so requesting sleep" ); - mShouldSleep = true; - return; - } - if ( dictionary == nullptr ) - { - FWE_LOG_TRACE( "No decoder dictionary, so requesting sleep" ); - mShouldSleep = true; - return; - } - for ( auto &channel : dictionary->canMessageDecoderMethod ) - { - if ( channel.first == canChannel ) - { - for ( auto &frame : channel.second ) - { - if ( frame.first == canRawFrameId ) - { - { - std::lock_guard lock( mExtractionOngoing ); - mExtractedMessageFormat = frame.second.format; - mNewMessageFormatExtracted = true; - mShouldSleep = false; - } - FWE_LOG_TRACE( "Dictionary with relevant information for CustomDataSource so waking up" ); - mWait.notify(); - return; - } - } - break; - } - } - FWE_LOG_TRACE( "Dictionary has no relevant information for CustomDataSource" ); - mShouldSleep = true; // Nothing found -} - -void -CustomDataSource::onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, - VehicleDataSourceProtocol networkProtocol ) -{ - if ( networkProtocol != VehicleDataSourceProtocol::RAW_SOCKET ) - { - return; - } - CANChannelNumericID canChannel = 0; - CANRawFrameID canRawFrameId = 0; - auto canDecoderDictionary = std::dynamic_pointer_cast( dictionary ); - { - std::lock_guard lock( mExtractionOngoing ); - mLastReceivedDictionary = canDecoderDictionary; - canChannel = mCanChannel; - canRawFrameId = mCanRawFrameId; - } - matchDictionaryToFilter( canDecoderDictionary, canChannel, canRawFrameId ); -} - -CustomDataSource::~CustomDataSource() -{ - if ( isRunning() ) - { - stop(); - } -} - -} // namespace IoTFleetWise -} // namespace Aws diff --git a/src/CustomDataSource.h b/src/CustomDataSource.h deleted file mode 100644 index cb70ba69..00000000 --- a/src/CustomDataSource.h +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#include "IDecoderDictionary.h" -#include "MessageTypes.h" -#include "Signal.h" -#include "SignalTypes.h" -#include "Thread.h" -#include "VehicleDataSourceTypes.h" -#include -#include -#include -#include -#include - -namespace Aws -{ -namespace IoTFleetWise -{ - -/** - * To implement a custom data source create a new class and inherit from CustomDataSource - * then call setFilter() then start() and provide an implementation for pollData - */ -class CustomDataSource -{ -public: - CustomDataSource() = default; - - void onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, - VehicleDataSourceProtocol networkProtocol ); - - bool start(); - bool stop(); - - /** - * Use a specific CAN channel and message ID combination for this custom data source - * Only if this message is collected by a collection scheme this custom data gets active - * - * @param canChannel the edge internal number correspondng to the 'interfaceId' defined in the DecoderManifest - * @param canRawFrameId the 'messageId' defined in the DecoderManifest - */ - void setFilter( CANChannelNumericID canChannel, CANRawFrameID canRawFrameId ); - - /** - * Get the unique SignalID, which is used to assign values to signals - * - * @param startBit defined in the DecoderManifest in the message given to setFilter - * - * @return a signalId which can be use to set a value for this signal as if it was read on CAN. INVALID_SIGNAL_ID if - * no signal is defined for this startBit in the DecoderManifest - */ - SignalID getSignalIdFromStartBit( uint16_t startBit ); - - /** - * Returns the signal information from the decoder manifest - * - * @return Signal info - */ - std::vector getSignalInfo(); - - virtual ~CustomDataSource(); - CustomDataSource( const CustomDataSource & ) = delete; - CustomDataSource &operator=( const CustomDataSource & ) = delete; - CustomDataSource( CustomDataSource && ) = delete; - CustomDataSource &operator=( CustomDataSource && ) = delete; - -protected: - virtual const char *getThreadName(); - - /** - * Will be called from inside doWork function when data should be polled - * the interval this function should be called is defined in mPollIntervalMs - */ - virtual void pollData() = 0; - void - setPollIntervalMs( uint32_t pollIntervalMs ) - { - mPollIntervalMs = pollIntervalMs; - }; - uint32_t mPollIntervalMs = DEFAULT_POLL_INTERVAL_MS; - - bool - isRunning() - { - return mThread.isValid() && mThread.isActive(); - } - -private: - void matchDictionaryToFilter( std::shared_ptr &dictionary, - CANChannelNumericID canChannel, - CANRawFrameID canRawFrameId ); - // Main work function that runs in new thread - static void doWork( void *data ); - bool shouldStop() const; - - bool shouldSleep() const; - - Thread mThread; - std::atomic mShouldStop{ false }; - std::atomic mShouldSleep{ false }; - mutable std::mutex mThreadMutex; - Signal mWait; - CANChannelNumericID mCanChannel = INVALID_CAN_SOURCE_NUMERIC_ID; - CANRawFrameID mCanRawFrameId = 0; - - CANMessageFormat mExtractedMessageFormat; - std::atomic mNewMessageFormatExtracted{ false }; - CANMessageFormat mUsedMessageFormat; - mutable std::mutex mExtractionOngoing; - - std::shared_ptr mLastReceivedDictionary; - - static const uint32_t DEFAULT_POLL_INTERVAL_MS = 50; // Default poll every 50ms data -}; - -} // namespace IoTFleetWise -} // namespace Aws diff --git a/src/CustomFunctionMath.cpp b/src/CustomFunctionMath.cpp new file mode 100644 index 00000000..515f4309 --- /dev/null +++ b/src/CustomFunctionMath.cpp @@ -0,0 +1,194 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CustomFunctionMath.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace CustomFunctionMath +{ + +CustomFunctionInvokeResult +absFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() != 1 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + if ( args[0].isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( !args[0].isBoolOrDouble() ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + return { ExpressionErrorCode::SUCCESSFUL, std::abs( args[0].asDouble() ) }; +} + +CustomFunctionInvokeResult +minFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() < 2 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + double minimum = DBL_MAX; + for ( const auto &arg : args ) + { + if ( arg.isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( !arg.isBoolOrDouble() ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + minimum = std::min( minimum, arg.asDouble() ); + } + return { ExpressionErrorCode::SUCCESSFUL, minimum }; +} + +CustomFunctionInvokeResult +maxFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() < 2 ) + { + return { ExpressionErrorCode::TYPE_MISMATCH }; + } + double maximum = DBL_MIN; + for ( const auto &arg : args ) + { + if ( arg.isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( !arg.isBoolOrDouble() ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + maximum = std::max( maximum, arg.asDouble() ); + } + return { ExpressionErrorCode::SUCCESSFUL, maximum }; +} + +CustomFunctionInvokeResult +powFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() != 2 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + if ( args[0].isUndefined() || args[1].isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( ( !args[0].isBoolOrDouble() ) || ( !args[1].isBoolOrDouble() ) ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + // coverity[misra_cpp_2008_rule_19_3_1_violation] errno is used by `pow` to indicate a domain error + // coverity[autosar_cpp14_m19_3_1_violation] errno is used by `pow` to indicate a domain error + errno = 0; + // coverity[autosar_cpp14_a0_4_4_violation] Range errors are detected via errno + auto powRes = std::pow( args[0].asDouble(), args[1].asDouble() ); + // coverity[misra_cpp_2008_rule_19_3_1_violation] errno is used by `pow` to indicate a domain error + // coverity[autosar_cpp14_m19_3_1_violation] errno is used by `pow` to indicate a domain error + if ( errno != 0 ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + return { ExpressionErrorCode::SUCCESSFUL, powRes }; +} + +CustomFunctionInvokeResult +logFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() != 2 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + if ( args[0].isUndefined() || args[1].isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( ( !args[0].isBoolOrDouble() ) || ( !args[1].isBoolOrDouble() ) ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + // coverity[misra_cpp_2008_rule_19_3_1_violation] errno is used by `log` to indicate a domain error + // coverity[autosar_cpp14_m19_3_1_violation] errno is used by `log` to indicate a domain error + errno = 0; + auto logBase = std::log( args[0].asDouble() ); + // coverity[misra_cpp_2008_rule_19_3_1_violation] errno is used by `log` to indicate a domain error + // coverity[autosar_cpp14_m19_3_1_violation] errno is used by `log` to indicate a domain error + if ( errno != 0 ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + // coverity[misra_cpp_2008_rule_19_3_1_violation] errno is used by `log` to indicate a domain error + // coverity[autosar_cpp14_m19_3_1_violation] errno is used by `log` to indicate a domain error + errno = 0; + auto logNum = std::log( args[1].asDouble() ); + // coverity[misra_cpp_2008_rule_19_3_1_violation] errno is used by `log` to indicate a domain error + // coverity[autosar_cpp14_m19_3_1_violation] errno is used by `log` to indicate a domain error + if ( errno != 0 ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + return { ExpressionErrorCode::SUCCESSFUL, logNum / logBase }; +} + +CustomFunctionInvokeResult +ceilFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() != 1 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + if ( args[0].isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( !args[0].isBoolOrDouble() ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + return { ExpressionErrorCode::SUCCESSFUL, std::ceil( args[0].asDouble() ) }; +} + +CustomFunctionInvokeResult +floorFunc( CustomFunctionInvocationID invocationId, const std::vector &args ) +{ + static_cast( invocationId ); + if ( args.size() != 1 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + if ( args[0].isUndefined() ) + { + return ExpressionErrorCode::SUCCESSFUL; // Undefined result + } + if ( !args[0].isBoolOrDouble() ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + return { ExpressionErrorCode::SUCCESSFUL, std::floor( args[0].asDouble() ) }; +} + +} // namespace CustomFunctionMath +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CustomFunctionMath.h b/src/CustomFunctionMath.h new file mode 100644 index 00000000..9c1e795b --- /dev/null +++ b/src/CustomFunctionMath.h @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace CustomFunctionMath +{ + +CustomFunctionInvokeResult absFunc( CustomFunctionInvocationID invocationId, const std::vector &args ); + +CustomFunctionInvokeResult minFunc( CustomFunctionInvocationID invocationId, const std::vector &args ); + +CustomFunctionInvokeResult maxFunc( CustomFunctionInvocationID invocationId, const std::vector &args ); + +CustomFunctionInvokeResult powFunc( CustomFunctionInvocationID invocationId, const std::vector &args ); + +CustomFunctionInvokeResult logFunc( CustomFunctionInvocationID invocationId, const std::vector &args ); + +CustomFunctionInvokeResult ceilFunc( CustomFunctionInvocationID invocationId, + const std::vector &args ); + +CustomFunctionInvokeResult floorFunc( CustomFunctionInvocationID invocationId, + const std::vector &args ); + +} // namespace CustomFunctionMath +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CustomFunctionMultiRisingEdgeTrigger.cpp b/src/CustomFunctionMultiRisingEdgeTrigger.cpp new file mode 100644 index 00000000..2289a73b --- /dev/null +++ b/src/CustomFunctionMultiRisingEdgeTrigger.cpp @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CustomFunctionMultiRisingEdgeTrigger.h" +#include "LoggingModule.h" +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +CustomFunctionMultiRisingEdgeTrigger::CustomFunctionMultiRisingEdgeTrigger( + std::shared_ptr namedSignalDataSource, + std::shared_ptr rawBufferManager ) + : mNamedSignalDataSource( std::move( namedSignalDataSource ) ) + , mRawBufferManager( std::move( rawBufferManager ) ) +{ +} + +CustomFunctionInvokeResult +CustomFunctionMultiRisingEdgeTrigger::invoke( CustomFunctionInvocationID invocationId, + const std::vector &args ) +{ + if ( ( args.size() < 2 ) || ( ( args.size() % 2 ) != 0 ) ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + auto invocationState = mInvocationStates.find( invocationId ); + if ( invocationState == mInvocationStates.end() ) // First invocation + { + InvocationState state; + state.lastConditionValues.resize( args.size() / 2 ); + for ( size_t i = 0; i < args.size(); i += 2 ) + { + if ( ( !args[i].isString() ) || ( ( !args[i + 1].isUndefined() ) && ( !args[i + 1].isBoolOrDouble() ) ) ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + // coverity[check_return] False positive, this result does not need checking + state.lastConditionValues[i / 2] = args[i + 1].isUndefined() || args[i + 1].asBool(); + } + mInvocationStates.emplace( invocationId, std::move( state ) ); + return { ExpressionErrorCode::SUCCESSFUL, false }; + } + + if ( invocationState->second.lastConditionValues.size() != ( args.size() / 2 ) ) + { + // Number of arguments has somehow changed since the first invocation. + return ExpressionErrorCode::TYPE_MISMATCH; + } + bool atLeastOneRisingEdge = false; + for ( size_t i = 0; i < args.size(); i += 2 ) + { + if ( ( !args[i].isString() ) || ( ( !args[i + 1].isUndefined() ) && ( !args[i + 1].isBoolOrDouble() ) ) ) + { + // Type of arguments has somehow changed since the first invocation + return ExpressionErrorCode::TYPE_MISMATCH; + } + + auto currentValue = args[i + 1].isUndefined() || args[i + 1].asBool(); + if ( ( !args[i + 1].isUndefined() ) && currentValue && + ( !invocationState->second.lastConditionValues[i / 2] ) ) // Rising edge + { + atLeastOneRisingEdge = true; + mTriggeredConditions.push_back( *args[i].stringVal ); + } + invocationState->second.lastConditionValues[i / 2] = currentValue; + } + return { ExpressionErrorCode::SUCCESSFUL, atLeastOneRisingEdge }; +} + +void +CustomFunctionMultiRisingEdgeTrigger::conditionEnd( const std::unordered_set &collectedSignalIds, + Timestamp timestamp, + CollectionInspectionEngineOutput &output ) +{ + // Only add to the collected data if we have a valid value: + if ( mTriggeredConditions.empty() ) + { + return; + } + // Clear the current value: + auto triggeredConditions = std::move( mTriggeredConditions ); + // Only add to the collected data if collection was triggered: + if ( !output.triggeredCollectionSchemeData ) + { + return; + } + if ( ( mRawBufferManager == nullptr ) || ( mNamedSignalDataSource == nullptr ) ) + { + FWE_LOG_WARN( "namedSignalInterface missing from config or raw buffer manager disabled" ); + return; + } + auto signalId = mNamedSignalDataSource->getNamedSignalID( "Vehicle.MultiRisingEdgeTrigger" ); + if ( signalId == INVALID_SIGNAL_ID ) + { + FWE_LOG_WARN( "Vehicle.MultiRisingEdgeTrigger not present in decoder manifest" ); + return; + } + if ( collectedSignalIds.find( signalId ) == collectedSignalIds.end() ) + { + return; + } + Json::Value root = Json::arrayValue; + for ( const auto &conditionName : triggeredConditions ) + { + root.append( conditionName ); + } + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + auto jsonString = Json::writeString( builder, root ); + auto bufferHandle = mRawBufferManager->push( + reinterpret_cast( jsonString.data() ), jsonString.size(), timestamp, signalId ); + if ( bufferHandle == RawData::INVALID_BUFFER_HANDLE ) + { + return; + } + // immediately set usage hint so buffer handle does not get directly deleted again + mRawBufferManager->increaseHandleUsageHint( + signalId, bufferHandle, RawData::BufferHandleUsageStage::COLLECTION_INSPECTION_ENGINE_SELECTED_FOR_UPLOAD ); + output.triggeredCollectionSchemeData->signals.emplace_back( + CollectedSignal{ signalId, timestamp, bufferHandle, SignalType::STRING } ); +} + +void +CustomFunctionMultiRisingEdgeTrigger::cleanup( CustomFunctionInvocationID invocationId ) +{ + mInvocationStates.erase( invocationId ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/CustomFunctionMultiRisingEdgeTrigger.h b/src/CustomFunctionMultiRisingEdgeTrigger.h new file mode 100644 index 00000000..545d38e5 --- /dev/null +++ b/src/CustomFunctionMultiRisingEdgeTrigger.h @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "NamedSignalDataSource.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * MULTI_RISING_EDGE_TRIGGER custom function implementation + * + * Custom function signature: + * + * bool custom_function('MULTI_RISING_EDGE_TRIGGER', + * string conditionName1, bool condition1, + * string conditionName2, bool condition2, + * string conditionName3, bool condition3, + * ... + * ); + * + * The function takes a variable number of pairs of arguments, with each pair being a string name + * of the condition and a Boolean value of the condition itself. + * + * The function will return true when one or more of the conditions has a rising edge (false -> + * true). Additionally it will produce a string signal called `Vehicle.MultiTriggerInfo` that + * contains a JSON serialized array of strings containing the names of the conditions that have a + * rising edge. + */ +class CustomFunctionMultiRisingEdgeTrigger +{ +public: + CustomFunctionMultiRisingEdgeTrigger( std::shared_ptr namedSignalDataSource, + std::shared_ptr rawBufferManager ); + + CustomFunctionInvokeResult invoke( CustomFunctionInvocationID invocationId, + const std::vector &args ); + + void conditionEnd( const std::unordered_set &collectedSignalIds, + Timestamp timestamp, + CollectionInspectionEngineOutput &output ); + + void cleanup( CustomFunctionInvocationID invocationId ); + +private: + struct InvocationState + { + // coverity[autosar_cpp14_a18_1_2_violation] std::vector specialization is acceptable in this usecase + std::vector lastConditionValues; + }; + std::unordered_map mInvocationStates; + std::vector mTriggeredConditions; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr mRawBufferManager; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DataFetchManager.cpp b/src/DataFetchManager.cpp new file mode 100644 index 00000000..9494833d --- /dev/null +++ b/src/DataFetchManager.cpp @@ -0,0 +1,232 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "DataFetchManager.h" +#include "DataFetchManagerAPITypes.h" +#include "LoggingModule.h" +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +DataFetchManager::DataFetchManager() + : mFetchRequestTimer() +{ +} + +bool +DataFetchManager::start() +{ + // Prevent concurrent stop/init + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( false ); + if ( !mThread.create( doWork, this ) ) + { + FWE_LOG_TRACE( "Data Fetch Manager Thread failed to start" ); + } + else + { + FWE_LOG_TRACE( "Data Fetch Manager Thread started" ); + mThread.setThreadName( "fwDFMng" ); + } + + return mThread.isActive() && mThread.isValid(); +} + +bool +DataFetchManager::stop() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return true; + } + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( true, std::memory_order_relaxed ); + FWE_LOG_TRACE( "DataFetchManager request stop" ); + mWait.notify(); + mThread.release(); + FWE_LOG_TRACE( "Stop finished" ); + mShouldStop.store( false, std::memory_order_relaxed ); + return !mThread.isActive(); +} + +bool +DataFetchManager::shouldStop() +{ + return mShouldStop.load( std::memory_order_relaxed ); +} + +void +DataFetchManager::doWork( void *data ) +{ + DataFetchManager *fetchManager = static_cast( data ); + while ( !fetchManager->shouldStop() ) + { + uint64_t minTimeToWaitMs = UINT64_MAX; + if ( fetchManager->mUpdatedFetchMatrixAvailable ) + { + std::lock_guard lock( fetchManager->mFetchMatrixMutex ); + fetchManager->mUpdatedFetchMatrixAvailable = false; + fetchManager->mCurrentFetchMatrix = fetchManager->mFetchMatrix; + } + + if ( fetchManager->mCurrentFetchMatrix ) + { + { + std::lock_guard lock( fetchManager->mFetchQueueMutex ); + // If the queue is not empty, pop an element and process it + while ( ( !fetchManager->mFetchQueue.empty() ) && ( !fetchManager->shouldStop() ) ) + { + auto requestID = fetchManager->mFetchQueue.front(); + fetchManager->executeFetch( requestID ); + fetchManager->mFetchQueue.pop(); + } + } + // Iterate over the fetch matrix and schedule periodic requests + auto currentTime = fetchManager->mClock->monotonicTimeSinceEpochMs(); + for ( const auto &periodicalRequest : fetchManager->mCurrentFetchMatrix->periodicalFetchRequestSetup ) + { + auto requestID = periodicalRequest.first; + auto &executionInfo = fetchManager->mLastExecutionInformation[requestID]; + + // Check if it's time to execute this request + if ( ( executionInfo.lastExecutionMonotonicTimeMs == 0 ) || + ( ( currentTime - executionInfo.lastExecutionMonotonicTimeMs ) >= + periodicalRequest.second.fetchFrequencyMs ) ) + { + // TODO: max executions and reset interval parameters are not yet supported by the cloud and are + // ignored on edge Push the request to the queue + fetchManager->executeFetch( requestID ); + // Update execution info + executionInfo.lastExecutionMonotonicTimeMs = currentTime; + } + // Calculate time to next execution for this request + uint64_t timeToNextExecution = + executionInfo.lastExecutionMonotonicTimeMs + periodicalRequest.second.fetchFrequencyMs; + minTimeToWaitMs = std::min( minTimeToWaitMs, timeToNextExecution - currentTime ); + } + } + + if ( minTimeToWaitMs < UINT64_MAX ) + { + FWE_LOG_TRACE( "Waiting for: " + std::to_string( minTimeToWaitMs ) + " ms." ); + fetchManager->mWait.wait( static_cast( minTimeToWaitMs ) ); + } + else + { + fetchManager->mWait.wait( Signal::WaitWithPredicate ); + } + } +} + +void +DataFetchManager::onFetchRequest( const FetchRequestID &fetchRequestID, const bool &evaluationResult ) +{ + std::lock_guard lock( mFetchQueueMutex ); + // Interface is extendable to have start/stop fetch in place + // Ignore "false/stop" for now + if ( !evaluationResult ) + { + return; + } + + if ( mFetchQueue.size() >= mFetchQueueMaxSize ) + { + FWE_LOG_WARN( "Fetch Queue full, discarding fetch request ID " + std::to_string( fetchRequestID ) ); + return; + } + + mFetchQueue.push( fetchRequestID ); + FWE_LOG_TRACE( "New fetch request was handed over" ); + mWait.notify(); +} + +void +DataFetchManager::onChangeFetchMatrix( std::shared_ptr fetchMatrix ) +{ + std::lock_guard lock( mFetchMatrixMutex ); + if ( fetchMatrix == nullptr ) + { + FWE_LOG_ERROR( "Cannot set an empty fetch matrix" ); + } + mFetchMatrix = fetchMatrix; + mUpdatedFetchMatrixAvailable = true; + FWE_LOG_INFO( "Fetch Matrix updated" ); + mWait.notify(); +} + +FetchErrorCode +DataFetchManager::executeFetch( const FetchRequestID &fetchRequestID ) +{ + if ( mFetchMatrix == nullptr ) + { + return FetchErrorCode::NOT_IMPLEMENTED; + } + auto fetchRequestIterator = mFetchMatrix->fetchRequests.find( fetchRequestID ); + if ( fetchRequestIterator == mFetchMatrix->fetchRequests.end() ) + { + FWE_LOG_ERROR( "Unknown FetchRequestID : " + std::to_string( fetchRequestID ) ); + return FetchErrorCode::SIGNAL_NOT_FOUND; + } + + if ( fetchRequestIterator->second.empty() ) + { + FWE_LOG_ERROR( "No actions specified for FetchRequestID : " + std::to_string( fetchRequestID ) ); + return FetchErrorCode::SIGNAL_NOT_FOUND; + } + + for ( const auto &request : fetchRequestIterator->second ) + { + auto functionIt = mSupportedCustomFetchFunctionsMap.find( request.functionName ); + if ( functionIt == mSupportedCustomFetchFunctionsMap.end() ) + { + FWE_LOG_ERROR( "Unknown Custom function : " + request.functionName ); + continue; + } + + FWE_LOG_TRACE( "Dispatched fetch request ID " + std::to_string( fetchRequestID ) + " for SignalID " + + std::to_string( request.signalID ) ); + auto result = functionIt->second( request.signalID, fetchRequestID, request.args ); + + if ( result != FetchErrorCode::SUCCESSFUL ) + { + FWE_LOG_ERROR( "Failed to execute Custom function: " + request.functionName + " for SignalID " + + std::to_string( request.signalID ) ); + return result; + } + } + + return FetchErrorCode::SUCCESSFUL; +} + +void +DataFetchManager::registerCustomFetchFunction( const std::string &functionName, CustomFetchFunction function ) +{ + mSupportedCustomFetchFunctionsMap[functionName] = std::move( function ); + FWE_LOG_TRACE( "Registered custom function for fetch " + functionName ); +} + +bool +DataFetchManager::isAlive() +{ + return mThread.isValid() && mThread.isActive(); +} + +DataFetchManager::~DataFetchManager() +{ + // To make sure the thread stops during teardown of tests. + if ( isAlive() ) + { + stop(); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DataFetchManager.h b/src/DataFetchManager.h new file mode 100644 index 00000000..84819e7a --- /dev/null +++ b/src/DataFetchManager.h @@ -0,0 +1,89 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "DataFetchManagerAPITypes.h" +#include "Signal.h" +#include "SignalTypes.h" +#include "Thread.h" +#include "Timer.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class DataFetchManager +{ +public: + DataFetchManager(); + ~DataFetchManager(); + + DataFetchManager( const DataFetchManager & ) = delete; + DataFetchManager &operator=( const DataFetchManager & ) = delete; + DataFetchManager( DataFetchManager && ) = delete; + DataFetchManager &operator=( DataFetchManager && ) = delete; + + bool start(); + bool stop(); + bool isAlive(); + + /** + * @brief callback to be invoked to receive fetch request when condition triggered + */ + void onFetchRequest( const FetchRequestID &fetchRequestID, const bool &evaluationResult ); + + /** + * @brief callback to be invoked when new fetch matrix is received + */ + void onChangeFetchMatrix( std::shared_ptr fetchMatrix ); + + /** + * @brief Function to register known custom fetch function and their name + */ + void registerCustomFetchFunction( const std::string &functionName, CustomFetchFunction function ); + +private: + static void doWork( void *data ); + bool shouldStop(); + + /** + * @brief calls corresponding custom function for this fetch request + * + * @param fetchRequestID fetch request to execute + * @return FetchErrorCode success if execution was completed + */ + FetchErrorCode executeFetch( const FetchRequestID &fetchRequestID ); + + Thread mThread; + std::atomic mShouldStop{ false }; + std::mutex mThreadMutex; + Signal mWait; + + Timer mFetchRequestTimer; + std::shared_ptr mFetchMatrix; + std::shared_ptr mCurrentFetchMatrix; + std::mutex mFetchMatrixMutex; + std::atomic mUpdatedFetchMatrixAvailable{ false }; + std::unordered_map mLastExecutionInformation; + std::queue mFetchQueue; + std::mutex mFetchQueueMutex; + size_t mFetchQueueMaxSize{ 1000 }; + + std::unordered_map mSupportedCustomFetchFunctionsMap; + + std::shared_ptr mClock{ ClockHandler::getClock() }; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DataFetchManagerAPITypes.h b/src/DataFetchManagerAPITypes.h new file mode 100644 index 00000000..24ff7d91 --- /dev/null +++ b/src/DataFetchManagerAPITypes.h @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "SignalTypes.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +struct FetchRequest +{ + SignalID signalID{ 0U }; + std::string functionName; + std::vector args; +}; + +struct LastExecutionInfo +{ + uint64_t lastExecutionMonotonicTimeMs{ 0 }; + // TODO: below parameters are not yet supported by the cloud and are ignored on edge + uint64_t firstExecutionMonotonicTimeMs{ 0 }; + uint32_t executionCount{ 0 }; +}; + +struct PeriodicalFetchParameters +{ + uint64_t fetchFrequencyMs{ 0U }; + // TODO: below parameters are not yet supported by the cloud and are therefore ignored on edge + uint64_t maxExecutionCount{ 0U }; + uint64_t maxExecutionCountResetPeriodMs{ 0U }; +}; + +struct FetchMatrix +{ + // Map of all fetch requests that were registered in the collection schemes + std::unordered_map> fetchRequests; + // Map of periodical fetch parameters provided in the collection schemes + std::unordered_map periodicalFetchRequestSetup; +}; + +enum class FetchErrorCode +{ + SUCCESSFUL, + SIGNAL_NOT_FOUND, + UNSUPPORTED_PARAMETERS, + NOT_IMPLEMENTED +}; + +using CustomFetchFunction = + std::function & )>; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DataSenderManagerWorkerThread.h b/src/DataSenderManagerWorkerThread.h index b592d93b..79b9aa6a 100644 --- a/src/DataSenderManagerWorkerThread.h +++ b/src/DataSenderManagerWorkerThread.h @@ -77,6 +77,10 @@ class DataSenderManagerWorkerThread std::shared_ptr mDataSenderManager; std::shared_ptr mConnectivityModule; +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::shared_ptr mCommandResponses; +#endif + Timer mTimer; Timer mRetrySendingPersistedDataTimer; }; diff --git a/src/DataSenderProtoReader.cpp b/src/DataSenderProtoReader.cpp new file mode 100644 index 00000000..b15e0e99 --- /dev/null +++ b/src/DataSenderProtoReader.cpp @@ -0,0 +1,114 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "DataSenderProtoReader.h" +#include "CANDataTypes.h" +#include "LoggingModule.h" +#include "OBDDataTypes.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +DataSenderProtoReader::DataSenderProtoReader( CANInterfaceIDTranslator &canIDTranslator ) + : mIDTranslator( canIDTranslator ) +{ +} + +DataSenderProtoReader::~DataSenderProtoReader() +{ + google::protobuf::ShutdownProtobufLibrary(); +} + +bool +DataSenderProtoReader::setupVehicleData( const std::string &data ) +{ + mVehicleData.Clear(); + return mVehicleData.ParseFromString( data ); +} + +bool +DataSenderProtoReader::deserializeVehicleData( TriggeredCollectionSchemeData &out ) +{ + out.eventID = mVehicleData.collection_event_id(); + out.triggerTime = mVehicleData.collection_event_time_ms_epoch(); + + // metadata + out.metadata.collectionSchemeID = mVehicleData.campaign_sync_id(); + out.metadata.decoderID = mVehicleData.decoder_sync_id(); + + // signals + for ( auto i = 0; i < mVehicleData.captured_signals_size(); ++i ) + { + auto protoSignal = mVehicleData.captured_signals( i ); + CollectedSignal signal{}; + signal.signalID = protoSignal.signal_id(); + auto receiveTime = + protoSignal.relative_time_ms() + static_cast( mVehicleData.collection_event_time_ms_epoch() ); + if ( receiveTime >= 0 ) + { + signal.receiveTime = static_cast( receiveTime ); + } + signal.value.setVal( protoSignal.double_value(), SignalType::DOUBLE ); + out.signals.emplace_back( signal ); + } + + // can frames + for ( auto i = 0; i < mVehicleData.can_frames_size(); ++i ) + { + auto protoFrame = mVehicleData.can_frames( i ); + if ( protoFrame.byte_values().size() > MAX_CAN_FRAME_BYTE_SIZE ) + { + FWE_LOG_WARN( "Skipping malformed CAN frame with size larger than " + + std::to_string( MAX_CAN_FRAME_BYTE_SIZE ) + + ". fID: " + std::to_string( protoFrame.message_id() ) ); + continue; + } + + CollectedCanRawFrame frame{}; + frame.size = static_cast( protoFrame.byte_values().size() ); + std::array buf = {}; + std::copy_n( protoFrame.byte_values().begin(), frame.size, buf.begin() ); + frame.data = buf; + auto receiveTime = + protoFrame.relative_time_ms() + static_cast( mVehicleData.collection_event_time_ms_epoch() ); + if ( receiveTime >= 0 ) + { + frame.receiveTime = static_cast( receiveTime ); + } + frame.frameID = protoFrame.message_id(); + frame.channelId = mIDTranslator.getChannelNumericID( protoFrame.interface_id() ); + out.canFrames.emplace_back( frame ); + } + + // dtc info + if ( mVehicleData.has_dtc_data() ) + { + DTCInfo info; + // NOTE: mSID is purposefully omitted because it is not serialized + auto receiveTime = mVehicleData.dtc_data().relative_time_ms() + + static_cast( mVehicleData.collection_event_time_ms_epoch() ); + if ( receiveTime >= 0 ) + { + info.receiveTime = static_cast( receiveTime ); + } + for ( auto code : mVehicleData.dtc_data().active_dtc_codes() ) + { + info.mDTCCodes.emplace_back( code ); + } + out.mDTCInfo = info; + } + + return true; +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DataSenderProtoReader.h b/src/DataSenderProtoReader.h new file mode 100644 index 00000000..5a68fd2d --- /dev/null +++ b/src/DataSenderProtoReader.h @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CANInterfaceIDTranslator.h" +#include "CollectionInspectionAPITypes.h" +#include "vehicle_data.pb.h" +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class DataSenderProtoReader +{ +public: + DataSenderProtoReader( CANInterfaceIDTranslator &canIDTranslator ); + ~DataSenderProtoReader(); + + DataSenderProtoReader( const DataSenderProtoReader & ) = delete; + DataSenderProtoReader &operator=( const DataSenderProtoReader & ) = delete; + DataSenderProtoReader( DataSenderProtoReader && ) = delete; + DataSenderProtoReader &operator=( DataSenderProtoReader && ) = delete; + + bool setupVehicleData( const std::string &data ); + bool deserializeVehicleData( TriggeredCollectionSchemeData &out ); + +private: + Schemas::VehicleDataMsg::VehicleData mVehicleData{}; + CANInterfaceIDTranslator mIDTranslator; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DataSenderProtoWriter.cpp b/src/DataSenderProtoWriter.cpp index 5b76323c..19a818dd 100644 --- a/src/DataSenderProtoWriter.cpp +++ b/src/DataSenderProtoWriter.cpp @@ -2,16 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 #include "DataSenderProtoWriter.h" +#include "LoggingModule.h" #include "SignalTypes.h" #include #include #include #include -#ifdef FWE_FEATURE_VISION_SYSTEM_DATA -#include "LoggingModule.h" -#endif - namespace Aws { namespace IoTFleetWise @@ -115,6 +112,33 @@ DataSenderProtoWriter::append( const CollectedSignal &msg ) capturedSignal.set_double_value( signalPhysicalValue ); size += sizeof( double ); break; + case SignalType::STRING: { + if ( mRawDataBufferManager == nullptr ) + { + FWE_LOG_WARN( "Raw Data Buffer not initalized so impossible to send data for signal: " + + std::to_string( msg.signalID ) ); + return; + } + auto loanedRawDataFrame = mRawDataBufferManager->borrowFrame( + msg.signalID, static_cast( signalValue.value.uint32Val ) ); + if ( loanedRawDataFrame.isNull() ) + { + FWE_LOG_WARN( "Could not capture the frame from buffer handle" ); + return; + } + mRawDataBufferManager->increaseHandleUsageHint( + msg.signalID, signalValue.value.uint32Val, RawData::BufferHandleUsageStage::HANDED_OVER_TO_SENDER ); + mRawDataBufferManager->decreaseHandleUsageHint( + msg.signalID, + signalValue.value.uint32Val, + RawData::BufferHandleUsageStage::COLLECTION_INSPECTION_ENGINE_SELECTED_FOR_UPLOAD ); + + auto data = loanedRawDataFrame.getData(); + auto stringSize = loanedRawDataFrame.getSize(); + capturedSignal.set_string_value( reinterpret_cast( data ), stringSize ); + size += STRING_OVERHEAD + stringSize; + break; + } case SignalType::UNKNOWN: // UNKNOWN signal should not be processed return; diff --git a/src/DataSenderTypes.h b/src/DataSenderTypes.h index 2779cdc7..43a1071e 100644 --- a/src/DataSenderTypes.h +++ b/src/DataSenderTypes.h @@ -25,6 +25,12 @@ enum class SenderDataType #ifdef FWE_FEATURE_VISION_SYSTEM_DATA VISION_SYSTEM = 1, #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + COMMAND_RESPONSE = 2, +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + LAST_KNOWN_STATE = 3, +#endif }; /** @@ -95,6 +101,14 @@ senderDataTypeToString( SenderDataType dataType ) #ifdef FWE_FEATURE_VISION_SYSTEM_DATA case SenderDataType::VISION_SYSTEM: return "VisionSystem"; +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + case SenderDataType::COMMAND_RESPONSE: + return "CommandResponse"; +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + case SenderDataType::LAST_KNOWN_STATE: + return "LastKnownState"; #endif default: return ""; @@ -126,6 +140,20 @@ stringToSenderDataType( const std::string &dataType, SenderDataType &output ) return true; } #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + else if ( dataType == "CommandResponse" ) + { + output = SenderDataType::COMMAND_RESPONSE; + return true; + } +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + else if ( dataType == "LastKnownState" ) + { + output = SenderDataType::LAST_KNOWN_STATE; + return true; + } +#endif return false; } diff --git a/src/DecoderDictionaryExtractor.cpp b/src/DecoderDictionaryExtractor.cpp index 680dc370..2fdb1ff5 100644 --- a/src/DecoderDictionaryExtractor.cpp +++ b/src/DecoderDictionaryExtractor.cpp @@ -54,6 +54,10 @@ CollectionSchemeManager::addSignalToDecoderDictionaryMap( { decoderDictionaryMap[networkType] = std::make_shared(); } + else if ( networkType == VehicleDataSourceProtocol::CUSTOM_DECODING ) + { + decoderDictionaryMap[networkType] = std::make_shared(); + } #ifdef FWE_FEATURE_VISION_SYSTEM_DATA // Currently we don't have decoder dictionary for this type of network protocol, create one else if ( networkType == VehicleDataSourceProtocol::COMPLEX_DATA ) @@ -177,6 +181,28 @@ CollectionSchemeManager::addSignalToDecoderDictionaryMap( .format.mSignals.emplace_back( format ); } } + else if ( networkType == VehicleDataSourceProtocol::CUSTOM_DECODING ) + { + auto customDecoderDictionaryPtr = std::dynamic_pointer_cast( + decoderDictionaryMap[VehicleDataSourceProtocol::CUSTOM_DECODING] ); + auto customSignalDecoderFormat = mDecoderManifest->getCustomSignalDecoderFormat( signalId ); + if ( !customDecoderDictionaryPtr ) + { + FWE_LOG_WARN( "Can not cast dictionary to CustomDecoderDictionary for Custom Decoded Signal ID: " + + std::to_string( signalId ) ); + } + else if ( customSignalDecoderFormat.mInterfaceId.empty() ) + { + FWE_LOG_WARN( "Custom Decoded signal ID has empty interfaceID: " + std::to_string( signalId ) ); + } + else + { + customDecoderDictionaryPtr + ->customDecoderMethod[customSignalDecoderFormat.mInterfaceId][customSignalDecoderFormat.mDecoder] = + customSignalDecoderFormat; + FWE_LOG_TRACE( "Custom Decoded Signal ID: " + std::to_string( signalId ) ); + } + } #ifdef FWE_FEATURE_VISION_SYSTEM_DATA else if ( networkType == VehicleDataSourceProtocol::COMPLEX_DATA ) { @@ -358,6 +384,30 @@ CollectionSchemeManager::decoderDictionaryExtractor( } } #endif + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + for ( const auto &stateTemplate : mStateTemplates ) + { + if ( stateTemplate.second->decoderManifestID != mCurrentDecoderManifestID ) + { + continue; + } + + for ( const auto &lksSignal : stateTemplate.second->signals ) + { + addSignalToDecoderDictionaryMap( lksSignal.signalID, + decoderDictionaryMap +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + , + partialSignalTypes, + lksSignal.signalID, + // Complex types are not supported for Last Known State + SignalPath() +#endif + ); + } + } +#endif } #ifdef FWE_FEATURE_VISION_SYSTEM_DATA diff --git a/src/DecoderManifestIngestion.cpp b/src/DecoderManifestIngestion.cpp index ba0a1d1c..7d62ffe5 100644 --- a/src/DecoderManifestIngestion.cpp +++ b/src/DecoderManifestIngestion.cpp @@ -8,6 +8,7 @@ #include "OBDDataTypes.h" #include #include +#include #ifdef FWE_FEATURE_VISION_SYSTEM_DATA #include @@ -144,6 +145,29 @@ DecoderManifestIngestion::getComplexDataType( ComplexDataTypeId typeId ) const } #endif +CustomSignalDecoderFormat +DecoderManifestIngestion::getCustomSignalDecoderFormat( SignalID signalID ) const +{ + if ( !mReady ) + { + return INVALID_CUSTOM_SIGNAL_DECODER_FORMAT; + } + + auto it = mSignalToCustomDecoder->find( signalID ); + if ( it == mSignalToCustomDecoder->end() ) + { + return INVALID_CUSTOM_SIGNAL_DECODER_FORMAT; + } + + return it->second; +} + +SignalIDToCustomSignalDecoderFormatMapPtr +DecoderManifestIngestion::getSignalIDToCustomSignalDecoderFormatMap() const +{ + return mSignalToCustomDecoder; +} + bool DecoderManifestIngestion::copyData( const std::uint8_t *inputBuffer, const size_t size ) { @@ -210,7 +234,7 @@ DecoderManifestIngestion::build() #ifdef FWE_FEATURE_VISION_SYSTEM_DATA && ( mProtoDecoderManifest.complex_signals_size() == 0 ) #endif - ) + && ( mProtoDecoderManifest.custom_decoding_signals_size() == 0 ) ) { // Error, missing required decoding information in the Decoder mProtoDecoderManifest FWE_LOG_ERROR( @@ -436,6 +460,37 @@ DecoderManifestIngestion::build() } #endif + // Reserve Map memory upfront as program already know the number of signals. + // This optimization can avoid multiple rehashes and improve overall build performance + SignalIDToCustomSignalDecoderFormatMap signalToCustomDecoderMap; + signalToCustomDecoderMap.reserve( static_cast( mProtoDecoderManifest.custom_decoding_signals_size() ) ); + for ( int i = 0; i < mProtoDecoderManifest.custom_decoding_signals_size(); i++ ) + { + const auto &customDecodedSignal = mProtoDecoderManifest.custom_decoding_signals( i ); + auto signalId = customDecodedSignal.signal_id(); + mSignalToVehicleDataSourceProtocol[signalId] = VehicleDataSourceProtocol::CUSTOM_DECODING; + + if ( customDecodedSignal.interface_id().empty() ) + { + FWE_LOG_WARN( "Custom signal with empty interface_id and signal id:" + std::to_string( signalId ) ); + } + else + { + // For backward compatibility, default to double + auto signalType = convertPrimitiveTypeToSignalType( customDecodedSignal.primitive_type() ) + .get_value_or( SignalType::DOUBLE ); + mSignalIDToTypeMap[signalId] = signalType; + signalToCustomDecoderMap[signalId] = CustomSignalDecoderFormat{ + customDecodedSignal.interface_id(), customDecodedSignal.custom_decoding_id(), signalId, signalType }; + + FWE_LOG_TRACE( "Adding custom signal with id: " + std::to_string( signalId ) + " with interface ID: '" + + customDecodedSignal.interface_id() + "' custom decoding size: '" + + std::to_string( customDecodedSignal.custom_decoding_id().size() ) + "'" ); + } + } + mSignalToCustomDecoder = + std::make_shared( std::move( signalToCustomDecoderMap ) ); + FWE_LOG_TRACE( "Decoder Manifest build succeeded" ); // Set our ready flag to true mReady = true; @@ -469,6 +524,8 @@ DecoderManifestIngestion::convertPrimitiveTypeToSignalType( Schemas::DecoderMani return SignalType::FLOAT; case Schemas::DecoderManifestMsg::PrimitiveType::FLOAT64: return SignalType::DOUBLE; + case Schemas::DecoderManifestMsg::PrimitiveType::STRING: + return SignalType::STRING; case Schemas::DecoderManifestMsg::PrimitiveType::NULL_: return SignalType::DOUBLE; default: diff --git a/src/DecoderManifestIngestion.h b/src/DecoderManifestIngestion.h index 2da29282..94b7e981 100644 --- a/src/DecoderManifestIngestion.h +++ b/src/DecoderManifestIngestion.h @@ -63,6 +63,10 @@ class DecoderManifestIngestion : public IDecoderManifest PIDSignalDecoderFormat getPIDSignalDecoderFormat( SignalID signalID ) const override; + CustomSignalDecoderFormat getCustomSignalDecoderFormat( SignalID signalID ) const override; + + SignalIDToCustomSignalDecoderFormatMapPtr getSignalIDToCustomSignalDecoderFormatMap() const override; + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA ComplexSignalDecoderFormat getComplexSignalDecoderFormat( SignalID signalID ) const override; @@ -154,6 +158,11 @@ class DecoderManifestIngestion : public IDecoderManifest */ static boost::optional convertPrimitiveTypeToSignalType( Schemas::DecoderManifestMsg::PrimitiveType primitiveType ); + + /** + * @brief A dictionary used to obtain the custom decoder for a given signal + */ + SignalIDToCustomSignalDecoderFormatMapPtr mSignalToCustomDecoder; }; } // namespace IoTFleetWise diff --git a/src/DeviceShadowOverSomeip.cpp b/src/DeviceShadowOverSomeip.cpp new file mode 100644 index 00000000..2798a1c3 --- /dev/null +++ b/src/DeviceShadowOverSomeip.cpp @@ -0,0 +1,213 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "DeviceShadowOverSomeip.h" +#include "IConnectionTypes.h" +#include "LoggingModule.h" +#include "TopicConfig.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +static inline v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode +connectivityToDeviceShadowError( ConnectivityError error ) +{ + switch ( error ) + { + case ConnectivityError::Success: + return v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::NO_ERROR; + case ConnectivityError::NotConfigured: + case ConnectivityError::WrongInputData: + case ConnectivityError::TypeNotSupported: + return v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::INVALID_REQUEST; + case ConnectivityError::NoConnection: + case ConnectivityError::QuotaReached: + case ConnectivityError::TransmissionError: + return v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::SHADOW_SERVICE_UNREACHABLE; + } + return v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::UNKNOWN; +} + +DeviceShadowOverSomeip::DeviceShadowOverSomeip( std::shared_ptr sender ) + : mMqttSender( std::move( sender ) ) + , mClientTokenRandomPrefix( boost::uuids::to_string( boost::uuids::random_generator()() ) + "-" ) +{ +} + +void +DeviceShadowOverSomeip::onDataReceived( const ReceivedConnectivityMessage &receivedMessage ) +{ + std::string responseDocument( receivedMessage.buf, receivedMessage.buf + receivedMessage.size ); + + // Ignore requests and delta updates: + if ( boost::ends_with( receivedMessage.mqttTopic, "/get" ) || + boost::ends_with( receivedMessage.mqttTopic, "/update" ) || + boost::ends_with( receivedMessage.mqttTopic, "/delete" ) || + boost::ends_with( receivedMessage.mqttTopic, "/update/delta" ) ) + { + return; + } + + // Documents update: + const std::string updateDocumentsSuffix = "/update/documents"; + if ( boost::ends_with( receivedMessage.mqttTopic, updateDocumentsSuffix ) ) + { + if ( !boost::starts_with( receivedMessage.mqttTopic, mMqttSender->getTopicConfig().deviceShadowPrefix ) ) + { + FWE_LOG_ERROR( "Received documents update for incorrect thing" ); + return; + } + std::string shadowName; + auto namedPrefix = mMqttSender->getTopicConfig().namedDeviceShadowPrefix; + if ( boost::starts_with( receivedMessage.mqttTopic, namedPrefix ) ) + { + shadowName = receivedMessage.mqttTopic.substr( namedPrefix.size(), + receivedMessage.mqttTopic.size() - namedPrefix.size() - + updateDocumentsSuffix.size() ); + } + FWE_LOG_INFO( "Received documents update" + ( shadowName.empty() ? "" : " for shadow " + shadowName ) ); + + fireShadowChangedEvent( shadowName, responseDocument ); + return; + } + + // Otherwise it's a response to a request, get the callback via the clientToken: + Json::Reader jsonReader; + Json::Value responseJson; + if ( !jsonReader.parse( responseDocument, responseJson ) ) + { + FWE_LOG_ERROR( "JSON parse error" ); + return; + } + const auto &clientToken = responseJson["clientToken"].asString(); + ResponseCallback callback; + { + std::lock_guard lock( mRequestTableMutex ); + auto requestIt = mRequestTable.find( clientToken ); + if ( requestIt == mRequestTable.end() ) + { + // Ignore responses to requests from other clients + return; + } + callback = std::move( requestIt->second ); + mRequestTable.erase( requestIt ); + } + + v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode errorCode; + std::string errorMessage; + if ( boost::ends_with( receivedMessage.mqttTopic, "/accepted" ) ) + { + errorCode = v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::NO_ERROR; + FWE_LOG_INFO( "Received accepted response for clientToken " + clientToken ); + } + else if ( boost::ends_with( receivedMessage.mqttTopic, "/rejected" ) ) + { + errorCode = v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::REJECTED; + errorMessage = responseJson["message"].asString(); + FWE_LOG_ERROR( "Received rejected response for clientToken " + clientToken + " with message " + errorMessage ); + } + else + { + errorCode = v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::UNKNOWN; + FWE_LOG_ERROR( "Received unknown response for clientToken " + clientToken ); + } + + callback( errorCode, errorMessage, responseDocument ); +} + +void +DeviceShadowOverSomeip::sendRequest( const std::string &topic, + const std::string &requestDocument, + ResponseCallback callback ) +{ + // Add the client token to the request document: + Json::Reader jsonReader; + Json::Value requestJson; + if ( !jsonReader.parse( requestDocument, requestJson ) ) + { + FWE_LOG_ERROR( "JSON parse error" ); + callback( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::INVALID_REQUEST, "JSON parse error", "" ); + return; + } + // coverity[misra_cpp_2008_rule_5_2_10_violation] For std::atomic this must be performed in a single statement + // coverity[autosar_cpp14_m5_2_10_violation] For std::atomic this must be performed in a single statement + auto clientToken = mClientTokenRandomPrefix + std::to_string( mClientTokenCounter++ ); + requestJson["clientToken"] = clientToken; + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + auto requestDocumentWithClientToken = Json::writeString( builder, requestJson ); + + FWE_LOG_INFO( "Sending request to topic " + topic + " with clientToken " + clientToken ); + // It can happen that the response is received via onDataReceived before the transmit callback + // below is called, so add the request to the request table now, and erase it again in the case + // of connectivity error. + { + std::lock_guard lock( mRequestTableMutex ); + mRequestTable.emplace( clientToken, callback ); + } + mMqttSender->sendBuffer( + topic, + reinterpret_cast( requestDocumentWithClientToken.data() ), + requestDocumentWithClientToken.size(), + [this, clientToken, callback]( ConnectivityError result ) { + if ( result != ConnectivityError::Success ) + { + FWE_LOG_ERROR( "Connectivity error: " + connectivityErrorToString( result ) ); + size_t requestsErased{}; + { + std::lock_guard lock( mRequestTableMutex ); + requestsErased = mRequestTable.erase( clientToken ); + } + if ( requestsErased == 1 ) + { + callback( connectivityToDeviceShadowError( result ), connectivityErrorToString( result ), "" ); + } + return; + } + } ); +} + +void +DeviceShadowOverSomeip::getShadow( const std::shared_ptr _client, + std::string _shadowName, + getShadowReply_t _reply ) +{ + static_cast( _client ); + sendRequest( mMqttSender->getTopicConfig().getDeviceShadowTopic( _shadowName ), "{}", _reply ); +} + +void +DeviceShadowOverSomeip::updateShadow( const std::shared_ptr _client, + std::string _shadowName, + std::string _updateDocument, + updateShadowReply_t _reply ) +{ + static_cast( _client ); + sendRequest( mMqttSender->getTopicConfig().updateDeviceShadowTopic( _shadowName ), _updateDocument, _reply ); +} + +void +DeviceShadowOverSomeip::deleteShadow( const std::shared_ptr _client, + std::string _shadowName, + deleteShadowReply_t _reply ) +{ + static_cast( _client ); + sendRequest( mMqttSender->getTopicConfig().deleteDeviceShadowTopic( _shadowName ), + "{}", + [_reply]( auto errorCode, const auto &errorMessage, const auto &responseDocument ) { + static_cast( responseDocument ); + _reply( errorCode, errorMessage ); + } ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/DeviceShadowOverSomeip.h b/src/DeviceShadowOverSomeip.h new file mode 100644 index 00000000..1ba8491d --- /dev/null +++ b/src/DeviceShadowOverSomeip.h @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "IReceiver.h" +#include "ISender.h" +#include "v1/commonapi/DeviceShadowOverSomeipInterface.hpp" +#include "v1/commonapi/DeviceShadowOverSomeipInterfaceStubDefault.hpp" +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class DeviceShadowOverSomeip : public v1_0::commonapi::DeviceShadowOverSomeipInterfaceStubDefault +{ +public: + DeviceShadowOverSomeip( std::shared_ptr sender ); + ~DeviceShadowOverSomeip() override = default; + + DeviceShadowOverSomeip( const DeviceShadowOverSomeip & ) = delete; + DeviceShadowOverSomeip &operator=( const DeviceShadowOverSomeip & ) = delete; + DeviceShadowOverSomeip( DeviceShadowOverSomeip && ) = delete; + DeviceShadowOverSomeip &operator=( DeviceShadowOverSomeip && ) = delete; + + void getShadow( const std::shared_ptr _client, + std::string _shadowName, + getShadowReply_t _reply ) override; + + void updateShadow( const std::shared_ptr _client, + std::string _shadowName, + std::string _updateDocument, + updateShadowReply_t _reply ) override; + + void deleteShadow( const std::shared_ptr _client, + std::string _shadowName, + deleteShadowReply_t _reply ) override; + + void onDataReceived( const ReceivedConnectivityMessage &receivedMessage ); + +private: + std::shared_ptr mMqttSender; + std::string mClientTokenRandomPrefix; + std::atomic mClientTokenCounter{}; + using ResponseCallback = std::function; + std::unordered_map mRequestTable; + std::mutex mRequestTableMutex; + void sendRequest( const std::string &topic, const std::string &requestDocument, ResponseCallback callback ); +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/ExampleSomeipInterfaceWrapper.h b/src/ExampleSomeipInterfaceWrapper.h new file mode 100644 index 00000000..84c23b4e --- /dev/null +++ b/src/ExampleSomeipInterfaceWrapper.h @@ -0,0 +1,375 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "ISomeipInterfaceWrapper.h" +#include "LoggingModule.h" +#include "RawDataManager.h" +#include "v1/commonapi/ExampleSomeipInterfaceProxy.hpp" +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * This class is the wrapper class for the example SOME/IP interface. It's responsible for building + * SOME/IP interface proxy and expose the proxy as CommonAPI::Proxy base class shared pointer. The + * SOME/IP method is encapsulated in a method wrapper with unified input signature. + * + * To support custom SOME/IP interface: + * 1. Create a new wrapper class by copying this class + * 2. Update the class name with the new interface name + * 3. Update the Proxy class name with the new interface proxy name + * 4. Create a new method wrapper to encapsulate the method call. Take referenceMethodWrapper as example + * 5. Update mSupportedActuatorInfo with a new entry containing the actuator name, signal type and + * method wrapper function name + */ +class ExampleSomeipInterfaceWrapper : public ISomeipInterfaceWrapper +{ +public: + ExampleSomeipInterfaceWrapper( std::string domain, + std::string instance, + std::string connection, + std::function>( + std::string, std::string, std::string )> buildProxy, + std::shared_ptr rawBufferManager, + bool subscribeToLongRunningCommandStatus ) + : mDomain( std::move( domain ) ) + , mInstance( std::move( instance ) ) + , mConnection( std::move( connection ) ) + , mBuildProxy( std::move( buildProxy ) ) + , mRawBufferManager( std::move( rawBufferManager ) ) + , mSubscribeToLongRunningCommandStatus( subscribeToLongRunningCommandStatus ) + , mSupportedActuatorInfo( { + { "Vehicle.actuator1", + { SignalType::INT32, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper1( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + { "Vehicle.actuator2", + { SignalType::INT64, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper2( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + { "Vehicle.actuator3", + { SignalType::BOOLEAN, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper3( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + { "Vehicle.actuator4", + { SignalType::FLOAT, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper4( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + { "Vehicle.actuator5", + { SignalType::DOUBLE, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper5( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + { "Vehicle.actuator9", + { SignalType::STRING, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper9( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + { "Vehicle.actuator20", + { SignalType::INT32, + [this]( auto signalValue, + auto commandId, + auto issuedTimestampMs, + auto executionTimeoutMs, + auto notifyStatusCallback ) { + referenceMethodWrapper20( + signalValue, commandId, issuedTimestampMs, executionTimeoutMs, notifyStatusCallback ); + } } }, + } ) + { + } + ~ExampleSomeipInterfaceWrapper() override + { + if ( mLongRunningCommandStatusSubscription.has_value() ) + { + mProxy->getNotifyLRCStatusEvent().unsubscribe( *mLongRunningCommandStatusSubscription ); + } + }; + + ExampleSomeipInterfaceWrapper( const ExampleSomeipInterfaceWrapper & ) = delete; + ExampleSomeipInterfaceWrapper &operator=( const ExampleSomeipInterfaceWrapper & ) = delete; + ExampleSomeipInterfaceWrapper( ExampleSomeipInterfaceWrapper && ) = delete; + ExampleSomeipInterfaceWrapper &operator=( ExampleSomeipInterfaceWrapper && ) = delete; + + bool + init() override + { + mProxy = mBuildProxy( mDomain, mInstance, mConnection ); + if ( mProxy == nullptr ) + { + FWE_LOG_ERROR( "Failed to build proxy " ); + return false; + } + + if ( mSubscribeToLongRunningCommandStatus ) + { + /* Subscribe to the long running command broadcast messages with + the given lambda as the handler.*/ + mLongRunningCommandStatusSubscription = mProxy->getNotifyLRCStatusEvent().subscribe( + // coverity[autosar_cpp14_a5_1_9_violation] This lambda isn't duplicated anywhere + [this]( const std::string &commandID, + const int32_t &commandStatus, + const int32_t &commandReasonCode, + const std::string &commandReasonDescription ) { + std::lock_guard lock( mCommandIDToNotifyCallbackMapMutex ); + auto commandCallback = mCommandIDToNotifyCallbackMap.find( commandID ); + if ( commandCallback == mCommandIDToNotifyCallbackMap.end() ) + { + FWE_LOG_ERROR( "Unknown command ID: " + commandID ); + return; + } + + // coverity[autosar_cpp14_a7_2_1_violation] Basic checking is performed above + CommandStatus cmdStatus = static_cast( commandStatus ); + CommandReasonCode cmdReasonCode = static_cast( commandReasonCode ); + + commandCallback->second( cmdStatus, cmdReasonCode, commandReasonDescription ); + } ); + } + + return true; + } + + std::shared_ptr + getProxy() const override + { + return std::static_pointer_cast( mProxy ); + } + + // Create one method wrapper for each SOME/IP method + void + referenceMethodWrapper1( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + FWE_LOG_TRACE( "set actuator value to " + std::to_string( signalValue.value.int32Val ) + " for command ID " + + commandId ); + mProxy->setInt32Async( + signalValue.value.int32Val, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + }, + &info ); + } + + void + referenceMethodWrapper2( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + FWE_LOG_TRACE( "set actuator value to " + std::to_string( signalValue.value.int64Val ) + " for command ID " + + commandId ); + mProxy->setInt64Async( + signalValue.value.int64Val, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + }, + &info ); + } + + void + referenceMethodWrapper3( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + FWE_LOG_TRACE( "set actuator value to " + std::to_string( signalValue.value.boolVal ) + " for command ID " + + commandId ); + mProxy->setBooleanAsync( + signalValue.value.boolVal, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + }, + &info ); + } + + void + referenceMethodWrapper4( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + FWE_LOG_TRACE( "set actuator value to " + std::to_string( signalValue.value.floatVal ) + " for command ID " + + commandId ); + mProxy->setFloatAsync( + signalValue.value.floatVal, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + }, + &info ); + } + + void + referenceMethodWrapper5( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + FWE_LOG_TRACE( "set actuator value to " + std::to_string( signalValue.value.doubleVal ) + " for command ID " + + commandId ); + mProxy->setDoubleAsync( + signalValue.value.doubleVal, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + }, + &info ); + } + + void + referenceMethodWrapper9( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + auto loanedFrame = mRawBufferManager->borrowFrame( signalValue.value.rawDataVal.signalId, + signalValue.value.rawDataVal.handle ); + if ( loanedFrame.isNull() ) + { + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, "" ); + return; + } + std::string stringVal; + stringVal.assign( reinterpret_cast( loanedFrame.getData() ), loanedFrame.getSize() ); + FWE_LOG_TRACE( "set actuator value to " + stringVal + " for command ID " + commandId ); + mProxy->setStringAsync( + stringVal, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + }, + &info ); + } + + void + referenceMethodWrapper20( SignalValueWrapper signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) + { + CommonAPI::CallInfo info( commonapiGetRemainingTimeout( issuedTimestampMs, executionTimeoutMs ) ); + { + std::lock_guard lock( mCommandIDToNotifyCallbackMapMutex ); + auto result = mCommandIDToNotifyCallbackMap.emplace( commandId, notifyStatusCallback ); + if ( !result.second ) + { + FWE_LOG_ERROR( "Duplicate command ID: " + commandId ); + return; + } + } + FWE_LOG_TRACE( "set actuator value to " + std::to_string( signalValue.value.int32Val ) + " for command ID " + + commandId ); + mProxy->setInt32LongRunningAsync( + commandId, + signalValue.value.int32Val, + // coverity[autosar_cpp14_a5_1_9_violation] Local variables need to be captured, so cannot be made common + [this, commandId, notifyStatusCallback]( const CommonAPI::CallStatus &callStatus ) { + notifyStatusCallback( commonapiCallStatusToCommandStatus( callStatus ), + commonapiCallStatusToReasonCode( callStatus ), + commonapiCallStatusToString( callStatus ) ); + std::lock_guard lock( mCommandIDToNotifyCallbackMapMutex ); + mCommandIDToNotifyCallbackMap.erase( commandId ); + }, + &info ); + } + + const std::unordered_map & + getSupportedActuatorInfo() const override + { + return mSupportedActuatorInfo; + } + +private: + std::shared_ptr> mProxy; + std::string mDomain; + std::string mInstance; + std::string mConnection; + std::function>( + std::string, std::string, std::string )> + mBuildProxy; + std::shared_ptr mRawBufferManager; + bool mSubscribeToLongRunningCommandStatus; + boost::optional + mLongRunningCommandStatusSubscription; + std::unordered_map mSupportedActuatorInfo; + // Mutex to ensure atomic insertion to the command ID to notification callback map + std::mutex mCommandIDToNotifyCallbackMapMutex; + std::unordered_map mCommandIDToNotifyCallbackMap; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/ExampleUDSInterface.cpp b/src/ExampleUDSInterface.cpp new file mode 100644 index 00000000..7a414a2a --- /dev/null +++ b/src/ExampleUDSInterface.cpp @@ -0,0 +1,366 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "ExampleUDSInterface.h" +#include "IRemoteDiagnostics.h" +#include "ISOTPOverCANOptions.h" +#include "LoggingModule.h" +#include +#include +#include // IWYU pragma: keep +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +static bool +openCANChannelPort( struct EcuConnectionInfo &info, bool isFunctional ) +{ + ISOTPOverCANSenderReceiverOptions optionsECU; + optionsECU.mSourceCANId = + isFunctional ? info.communicationParams.functionalAddress : info.communicationParams.physicalRequestID; + optionsECU.mSocketCanIFName = info.communicationParams.canBus; + optionsECU.mDestinationCANId = isFunctional ? 0 : info.communicationParams.physicalResponseID; + optionsECU.mP2TimeoutMs = 10000; + if ( ( !info.isotpSenderReceiver.init( optionsECU ) ) || ( !info.isotpSenderReceiver.connect() ) ) + { + FWE_LOG_ERROR( "Failed to initialize the ECU with Req CAN id: " + + std::to_string( info.communicationParams.physicalRequestID ) + + " Resp CAN id: " + std::to_string( info.communicationParams.physicalResponseID ) ); + return false; + } + FWE_LOG_TRACE( "Successfully initialized ECU with Req CAN id: " + + std::to_string( info.communicationParams.physicalRequestID ) + + " Resp CAN id: " + std::to_string( info.communicationParams.physicalResponseID ) ); + return true; +} + +void +ExampleUDSInterface::readDTCInfo( int32_t targetAddress, + UDSSubFunction subfn, + UDSStatusMask mask, + UDSResponseCallback callback, + const std::string &token ) +{ + std::vector sendPDU = { 0x19, static_cast( subfn ), static_cast( mask ) }; + Aws::IoTFleetWise::UdsDtcRequest udsDTCrequest; + udsDTCrequest.targetAddress = targetAddress; + udsDTCrequest.sendPDU = sendPDU; + udsDTCrequest.token = token; + udsDTCrequest.callback = callback; + addUdsDtcRequest( udsDTCrequest ); +} + +void +ExampleUDSInterface::readDTCInfoByDTCAndRecordNumber( int32_t targetAddress, + UDSSubFunction subfn, + uint32_t dtc, + uint8_t recordNumber, + UDSResponseCallback callback, + const std::string &token ) +{ + std::vector sendPDU; + sendPDU.push_back( 0x19 ); + sendPDU.push_back( static_cast( subfn ) ); + sendPDU.push_back( static_cast( ( dtc >> 16 ) & 0xFF ) ); + sendPDU.push_back( static_cast( ( dtc >> 8 ) & 0xFF ) ); + sendPDU.push_back( static_cast( dtc & 0xFF ) ); + sendPDU.push_back( recordNumber ); + + Aws::IoTFleetWise::UdsDtcRequest udsDTCrequest; + udsDTCrequest.targetAddress = targetAddress; + udsDTCrequest.sendPDU = sendPDU; + udsDTCrequest.token = token; + udsDTCrequest.callback = callback; + + addUdsDtcRequest( udsDTCrequest ); +} + +bool +ExampleUDSInterface::findTargetAddress( int target, EcuConfig &out ) +{ + for ( auto &config : mEcuConfig ) + { + if ( config.targetAddress == target ) + { + out = config; + return true; + } + } + return false; +} + +static void +openConnection( const EcuConfig &ecu, std::vector &connectionInfo, bool isFunctional ) +{ + EcuConnectionInfo info; + info.communicationParams = ecu; + + if ( isFunctional ) + { + if ( std::none_of( + connectionInfo.begin(), connectionInfo.end(), [&info]( const EcuConnectionInfo &existing ) -> bool { + return existing.communicationParams.physicalRequestID == + info.communicationParams.physicalRequestID; + } ) ) + { + if ( !openCANChannelPort( info, isFunctional ) ) + { + FWE_LOG_ERROR( "Could not open CAN channel" ); + return; + } + connectionInfo.emplace_back( info ); + } + } + else + { + if ( std::none_of( connectionInfo.begin(), + connectionInfo.end(), + [&info, ecu]( const EcuConnectionInfo &existing ) -> bool { + return ( existing.communicationParams.physicalRequestID == + info.communicationParams.physicalRequestID ) && + ( existing.communicationParams.physicalResponseID == ecu.physicalResponseID ); + } ) ) + { + if ( !openCANChannelPort( info, isFunctional ) ) + { + FWE_LOG_ERROR( "Could not open CAN channel" ); + return; + } + connectionInfo.emplace_back( info ); + } + } +} + +bool +ExampleUDSInterface::executeRequest( std::vector &sendPDU, int32_t targetAddress, DTCResponse &response ) +{ + if ( targetAddress == -1 ) + { + std::vector openFunctionalConnection; + std::vector openPhysicalConnection; + for ( const auto &ecu : mEcuConfig ) + { + openConnection( ecu, openFunctionalConnection, true ); + openConnection( ecu, openPhysicalConnection, false ); + } + + if ( openPhysicalConnection.empty() ) + { + FWE_LOG_ERROR( "No CAN Connection found" ); + return false; + } + + for ( auto &connection : openFunctionalConnection ) + { + if ( !connection.isotpSenderReceiver.sendPDU( sendPDU ) ) + { + FWE_LOG_ERROR( "Send PDU failed for Functional Address " + + std::to_string( connection.communicationParams.physicalRequestID ) ); + } + else + { + FWE_LOG_TRACE( "Successfully Sent PDU for Functional Address " + + std::to_string( connection.communicationParams.physicalRequestID ) ); + } + connection.isotpSenderReceiver.disconnect(); + } + + for ( auto &connection : openPhysicalConnection ) + { + if ( connection.isotpSenderReceiver.receivePDU( connection.data ) == true ) + { + FWE_LOG_TRACE( "Received data: " + getStringFromBytes( connection.data ) ); + UDSDTCInfo dtcInfo; + dtcInfo.targetAddress = connection.communicationParams.targetAddress; + for ( uint32_t i = 2; i < connection.data.size(); i++ ) + { + dtcInfo.dtcBuffer.push_back( connection.data[i] ); + } + // Only send back dtc info if data was received + if ( !dtcInfo.dtcBuffer.empty() ) + { + response.dtcInfo.push_back( dtcInfo ); + response.result = 1; + } + } + connection.isotpSenderReceiver.disconnect(); + } + if ( response.dtcInfo.empty() ) + { + return false; + } + return true; + } + else + { + EcuConfig ecu; + if ( !findTargetAddress( targetAddress, ecu ) ) + { + FWE_LOG_ERROR( "Unable to find address " + std::to_string( targetAddress ) ); + return false; + } + + EcuConnectionInfo phyInfo; + phyInfo.communicationParams = ecu; + if ( !openCANChannelPort( phyInfo, false ) ) + { + FWE_LOG_ERROR( "Could not open CAN channel" ); + return false; + } + + if ( !phyInfo.isotpSenderReceiver.sendPDU( sendPDU ) ) + { + FWE_LOG_ERROR( "Unable to send PDU" ); + return false; + } + + if ( !phyInfo.isotpSenderReceiver.receivePDU( phyInfo.data ) ) + { + FWE_LOG_ERROR( "Failed to receive PDU for Physical Address " + + std::to_string( phyInfo.communicationParams.physicalRequestID ) ); + return false; + } + + phyInfo.isotpSenderReceiver.disconnect(); + FWE_LOG_TRACE( "Received data: " + getStringFromBytes( phyInfo.data ) ); + UDSDTCInfo dtcInfo; + dtcInfo.targetAddress = targetAddress; + for ( uint32_t i = 2; i < phyInfo.data.size(); i++ ) + { + dtcInfo.dtcBuffer.push_back( phyInfo.data[i] ); + } + // Only send back dtc info if data was received + if ( !dtcInfo.dtcBuffer.empty() ) + { + response.dtcInfo.push_back( dtcInfo ); + response.result = 1; + } + return true; + } + return false; +} + +void +ExampleUDSInterface::addUdsDtcRequest( const UdsDtcRequest &request ) +{ + std::lock_guard lock( mQueryMutex ); + mDtcRequestQueue.push( request ); + mWait.notify(); +} + +void +ExampleUDSInterface::doWork( void *data ) +{ + ExampleUDSInterface *udsDataSource = static_cast( data ); + + while ( !udsDataSource->shouldStop() ) + { + udsDataSource->mWait.wait( Signal::WaitWithPredicate ); + + { + std::lock_guard lock( udsDataSource->mQueryMutex ); + while ( ( !udsDataSource->mDtcRequestQueue.empty() ) && ( !udsDataSource->shouldStop() ) ) + { + auto query = udsDataSource->mDtcRequestQueue.front(); + DTCResponse response; + response.token = query.token; + if ( udsDataSource->executeRequest( query.sendPDU, query.targetAddress, response ) ) + { + UdsDtcResponse responseToSend; + responseToSend.callback = query.callback; + responseToSend.response = response; + // Unblock request queue for incoming requests from callbacks + udsDataSource->mDtcResponseQueue.push( responseToSend ); + } + udsDataSource->mDtcRequestQueue.pop(); + } + } + + while ( ( !udsDataSource->mDtcResponseQueue.empty() ) && ( !udsDataSource->shouldStop() ) ) + { + auto query = udsDataSource->mDtcResponseQueue.front(); + query.callback( query.response ); + udsDataSource->mDtcResponseQueue.pop(); + } + } +} + +bool +ExampleUDSInterface::init( const std::vector &ecuConfigs ) +{ + if ( ecuConfigs.empty() ) + { + FWE_LOG_ERROR( "ECU configuration can not be empty" ); + return false; + } + mEcuConfig = ecuConfigs; + return true; +} + +bool +ExampleUDSInterface::start() +{ + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( false ); + if ( !mThread.create( doWork, this ) ) + { + FWE_LOG_TRACE( "ExampleUDSInterface Module Thread failed to start" ); + } + else + { + FWE_LOG_TRACE( "ExampleUDSInterface Module Thread started" ); + mThread.setThreadName( "fwDIExampleUDSInterface" ); + } + + return mThread.isActive() && mThread.isValid(); +} + +bool +ExampleUDSInterface::stop() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return true; + } + + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( true, std::memory_order_relaxed ); + FWE_LOG_TRACE( "ExampleUDSInterface Module Thread requested to stop" ); + mWait.notify(); + mThread.release(); + mShouldStop.store( false, std::memory_order_relaxed ); + FWE_LOG_TRACE( "Thread stopped" ); + return !mThread.isActive(); +} + +bool +ExampleUDSInterface::shouldStop() const +{ + return mShouldStop.load( std::memory_order_relaxed ); +} + +bool +ExampleUDSInterface::isAlive() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return false; + } + + return true; +} + +ExampleUDSInterface::~ExampleUDSInterface() +{ + // To make sure the thread stops during teardown of tests. + if ( isAlive() ) + { + stop(); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/ExampleUDSInterface.h b/src/ExampleUDSInterface.h new file mode 100644 index 00000000..0296aace --- /dev/null +++ b/src/ExampleUDSInterface.h @@ -0,0 +1,139 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "IRemoteDiagnostics.h" +#include "ISOTPOverCANSenderReceiver.h" +#include "Signal.h" +#include "Thread.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +struct EcuConfig +{ + std::string ecuName; + std::string canBus; + uint32_t physicalRequestID{ 0 }; + uint32_t physicalResponseID{ 0 }; + uint32_t functionalAddress{ 0 }; + int32_t targetAddress{ 0 }; +}; + +struct EcuConnectionInfo +{ + EcuConfig communicationParams; + ISOTPOverCANSenderReceiver isotpSenderReceiver; + std::vector data; +}; + +struct UdsDtcRequest +{ + int32_t targetAddress{ 0 }; + std::vector sendPDU; + std::string token; + UDSResponseCallback callback; +}; + +struct UdsDtcResponse +{ + UDSResponseCallback callback; + DTCResponse response; +}; + +class ExampleUDSInterface : public IRemoteDiagnostics +{ +public: + ExampleUDSInterface() = default; + ~ExampleUDSInterface() override; + ExampleUDSInterface( const ExampleUDSInterface & ) = delete; + ExampleUDSInterface &operator=( const ExampleUDSInterface & ) = delete; + ExampleUDSInterface( ExampleUDSInterface && ) = delete; + ExampleUDSInterface &operator=( ExampleUDSInterface && ) = delete; + + // Start the thread + bool start(); + // Stop the thread + bool stop(); + bool init( const std::vector &ecuConfigs ); + + /** + * @brief This function queues a new UDS DTC request for processing. + * + * @param request A const reference to a UdsDtcRequest object containing the details of the DTC request. + */ + void addUdsDtcRequest( const UdsDtcRequest &request ); + + void readDTCInfo( int32_t targetAddress, + UDSSubFunction subfn, + UDSStatusMask mask, + UDSResponseCallback callback, + const std::string &token ) override; + void readDTCInfoByDTCAndRecordNumber( int32_t targetAddress, + UDSSubFunction subfn, + uint32_t dtc, + uint8_t recordNumber, + UDSResponseCallback callback, + const std::string &token ) override; + +private: + // Intercepts stop signals. + bool shouldStop() const; + static void doWork( void *data ); + /** + * @brief Returns the health state of the cyclic thread + * @return True if successful. False otherwise. + */ + bool isAlive(); + + /** + * @brief Executes a UDS (Unified Diagnostic Services) request. + * + * This function sends a UDS request PDU (Protocol Data Unit) to a specified target address + * and processes the response. + * + * @param sendPDU A reference to a vector of uint8_t containing the request PDU to be sent. + * @param targetAddress An integer specifying the target address for the request. + * @param response A reference to an DTCResponse object to store the response data. + * @return int Returns a status code indicating the result of the request execution. + */ + bool executeRequest( std::vector &sendPDU, int32_t targetAddress, DTCResponse &response ); + + /** + * @brief Finds a ECU configuration for a given target address. + * + * @param target An integer representing the target ECU identifier. + * @param out A reference to an EcuConfig object where the found configuration will be stored. + * @return bool Returns true if the target address is found, false otherwise. + */ + bool findTargetAddress( int target, EcuConfig &out ); + + // Stop signal + Signal mWait; + Thread mThread; + mutable std::mutex mThreadMutex; + mutable std::mutex mQueryMutex; + std::atomic mShouldStop{ false }; + + std::queue mDtcRequestQueue; + std::queue mDtcResponseQueue; + + std::shared_ptr mClock = ClockHandler::getClock(); + + std::vector mEcuConfig; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/ExternalGpsSource.cpp b/src/ExternalGpsSource.cpp index f2eb7873..c77a9e6f 100644 --- a/src/ExternalGpsSource.cpp +++ b/src/ExternalGpsSource.cpp @@ -2,51 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 #include "ExternalGpsSource.h" -#include "CollectionInspectionAPITypes.h" #include "LoggingModule.h" -#include "QueueTypes.h" #include "SignalTypes.h" -#include #include +#include namespace Aws { namespace IoTFleetWise { -// NOLINT below due to C++17 warning of redundant declarations that are required to maintain C++14 compatibility -constexpr const char *ExternalGpsSource::CAN_CHANNEL_NUMBER; // NOLINT -constexpr const char *ExternalGpsSource::CAN_RAW_FRAME_ID; // NOLINT -constexpr const char *ExternalGpsSource::LATITUDE_START_BIT; // NOLINT -constexpr const char *ExternalGpsSource::LONGITUDE_START_BIT; // NOLINT -ExternalGpsSource::ExternalGpsSource( SignalBufferDistributorPtr signalBufferDistributor ) - : mSignalBufferDistributor{ std::move( signalBufferDistributor ) } +ExternalGpsSource::ExternalGpsSource( std::shared_ptr namedSignalDataSource, + std::string latitudeSignalName, + std::string longitudeSignalName ) + : mNamedSignalDataSource( std::move( namedSignalDataSource ) ) + , mLatitudeSignalName( std::move( latitudeSignalName ) ) + , mLongitudeSignalName( std::move( longitudeSignalName ) ) { } -bool -ExternalGpsSource::init( CANChannelNumericID canChannel, - CANRawFrameID canRawFrameId, - uint16_t latitudeStartBit, - uint16_t longitudeStartBit ) -{ - if ( canChannel == INVALID_CAN_SOURCE_NUMERIC_ID ) - { - return false; - } - mLatitudeStartBit = latitudeStartBit; - mLongitudeStartBit = longitudeStartBit; - mCanChannel = canChannel; - mCanRawFrameId = canRawFrameId; - setFilter( mCanChannel, mCanRawFrameId ); - return true; -} -const char * -ExternalGpsSource::getThreadName() -{ - return "ExternalGpsSrc"; -} - void ExternalGpsSource::setLocation( double latitude, double longitude ) { @@ -57,24 +31,10 @@ ExternalGpsSource::setLocation( double latitude, double longitude ) return; } FWE_LOG_TRACE( "Latitude: " + std::to_string( latitude ) + ", Longitude: " + std::to_string( longitude ) ); - auto latitudeSignalId = getSignalIdFromStartBit( mLatitudeStartBit ); - auto longitudeSignalId = getSignalIdFromStartBit( mLongitudeStartBit ); - if ( ( latitudeSignalId == INVALID_SIGNAL_ID ) || ( longitudeSignalId == INVALID_SIGNAL_ID ) ) - { - FWE_LOG_WARN( "Latitude or longitude not in decoder manifest" ); - return; - } - auto timestamp = mClock->systemTimeSinceEpochMs(); - CollectedSignalsGroup collectedSignalsGroup; - collectedSignalsGroup.push_back( CollectedSignal( latitudeSignalId, timestamp, latitude, SignalType::DOUBLE ) ); - collectedSignalsGroup.push_back( CollectedSignal( longitudeSignalId, timestamp, longitude, SignalType::DOUBLE ) ); - - mSignalBufferDistributor->push( CollectedDataFrame( std::move( collectedSignalsGroup ) ) ); -} - -void -ExternalGpsSource::pollData() -{ + std::vector> values; + values.emplace_back( std::make_pair( mLatitudeSignalName, DecodedSignalValue{ latitude, SignalType::DOUBLE } ) ); + values.emplace_back( std::make_pair( mLongitudeSignalName, DecodedSignalValue{ longitude, SignalType::DOUBLE } ) ); + mNamedSignalDataSource->ingestMultipleSignalValues( 0, values ); } bool diff --git a/src/ExternalGpsSource.h b/src/ExternalGpsSource.h index 51b2de4a..a766abeb 100644 --- a/src/ExternalGpsSource.h +++ b/src/ExternalGpsSource.h @@ -2,67 +2,39 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include "Clock.h" -#include "ClockHandler.h" -#include "CollectionInspectionAPITypes.h" -#include "CustomDataSource.h" -#include "SignalTypes.h" -#include +#include "NamedSignalDataSource.h" #include +#include namespace Aws { namespace IoTFleetWise { -/** - * To implement a custom data source create a new class and inherit from CustomDataSource - * then call setFilter() then start() and provide an implementation for pollData - */ -class ExternalGpsSource : public CustomDataSource +class ExternalGpsSource { public: /** - * @param signalBufferDistributor Signal buffer distributor + * @param namedSignalDataSource Named signal data source + * @param latitudeSignalName the signal name of the latitude signal + * @param longitudeSignalName the signal name of the longitude signal */ - ExternalGpsSource( SignalBufferDistributorPtr signalBufferDistributor ); - /** - * Initialize ExternalGpsSource and set filter for CustomDataSource - * - * @param canChannel the CAN channel used in the decoder manifest - * @param canRawFrameId the CAN message Id used in the decoder manifest - * @param latitudeStartBit the startBit used in the decoder manifest for the latitude signal - * @param longitudeStartBit the startBit used in the decoder manifest for the longitude signal - * - * @return on success true otherwise false - */ - bool init( CANChannelNumericID canChannel, - CANRawFrameID canRawFrameId, - uint16_t latitudeStartBit, - uint16_t longitudeStartBit ); + ExternalGpsSource( std::shared_ptr namedSignalDataSource, + std::string latitudeSignalName, + std::string longitudeSignalName ); void setLocation( double latitude, double longitude ); - static constexpr const char *CAN_CHANNEL_NUMBER = "canChannel"; - static constexpr const char *CAN_RAW_FRAME_ID = "canFrameId"; - static constexpr const char *LATITUDE_START_BIT = "latitudeStartBit"; - static constexpr const char *LONGITUDE_START_BIT = "longitudeStartBit"; - -protected: - void pollData() override; - const char *getThreadName() override; + static constexpr const char *LATITUDE_SIGNAL_NAME = "latitudeSignalName"; + static constexpr const char *LONGITUDE_SIGNAL_NAME = "longitudeSignalName"; private: static bool validLatitude( double latitude ); static bool validLongitude( double longitude ); - uint16_t mLatitudeStartBit = 0; - uint16_t mLongitudeStartBit = 0; - - SignalBufferDistributorPtr mSignalBufferDistributor; - std::shared_ptr mClock = ClockHandler::getClock(); - CANChannelNumericID mCanChannel{ INVALID_CAN_SOURCE_NUMERIC_ID }; - CANRawFrameID mCanRawFrameId{ 0 }; + std::shared_ptr mNamedSignalDataSource; + std::string mLatitudeSignalName; + std::string mLongitudeSignalName; }; } // namespace IoTFleetWise diff --git a/src/ICollectionScheme.h b/src/ICollectionScheme.h index ef6c8a96..14c358a5 100644 --- a/src/ICollectionScheme.h +++ b/src/ICollectionScheme.h @@ -16,6 +16,44 @@ namespace Aws namespace IoTFleetWise { +struct FetchInformation +{ + /** + * @brief ID of signal to be fetched. + */ + SignalID signalID{ 0U }; + + /** + * @brief AST root node to specify condition to fetch data (nullptr for time-based fetching). + */ + ExpressionNode *condition{ nullptr }; + + /** + * @brief Specify if data fetching should occur only on rising edge of condition (false to true). + */ + bool triggerOnlyOnRisingEdge{ false }; + + /** + * @brief Max number of action executions (per action?) can occur per interval (refer executionIntervalMs). + */ + uint64_t maxExecutionPerInterval{ 0U }; + + /** + * @brief Action execution period - between consecutive action executions (per action?). + */ + uint64_t executionPeriodMs{ 0U }; + + /** + * @brief Action execution interval (per action?) (refer maxExecutionPerInterval). + */ + uint64_t executionIntervalMs{ 0U }; + + /** + * @brief Actions to be executed on fetching. + */ + std::vector actions; +}; + struct SignalCollectionInfo { /** @@ -47,6 +85,14 @@ struct SignalCollectionInfo * condition logic with its associated fixed_window_period_ms. Default is false. */ bool isConditionOnlySignal{ false }; + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + /** + * @brief The Id of the partition where this signal should be stored. + * This Id will be used to index into the partition configuration array. + */ + uint32_t dataPartitionId{ 0 }; +#endif }; struct CanFrameCollectionInfo @@ -82,6 +128,7 @@ enum class ExpressionNodeType FLOAT = 0, SIGNAL, // Node_Signal_ID BOOLEAN, + STRING, OPERATOR_SMALLER, // NodeOperator OPERATOR_BIGGER, OPERATOR_SMALLER_EQUAL, @@ -96,6 +143,8 @@ enum class ExpressionNodeType OPERATOR_ARITHMETIC_MULTIPLY, OPERATOR_ARITHMETIC_DIVIDE, WINDOW_FUNCTION, // NodeFunction + CUSTOM_FUNCTION, + IS_NULL_FUNCTION, NONE }; @@ -116,6 +165,13 @@ struct ExpressionFunction * @brief Specify which window function to be used (in case ExpressionNodeType is WINDOW_FUNCTION). */ WindowFunction windowFunction{ WindowFunction::NONE }; + + /** + * @brief Specify custom function name and params (in case ExpressionNodeType is CUSTOM_FUNCTION). + */ + std::string customFunctionName; + std::vector customFunctionParams; + CustomFunctionInvocationID customFunctionInvocationId{}; }; // Instead of c style struct an object oriented interface could be implemented with different @@ -147,6 +203,11 @@ struct ExpressionNode */ bool booleanValue{ false }; + /** + * @brief Node Value is string + */ + std::string stringValue; + /** * @brief Unique Signal ID provided by Cloud */ @@ -207,6 +268,52 @@ struct S3UploadMetadata }; #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD +struct StorageOptions +{ + /** + * @brief The total amount of space allocated to this campaign including all overhead. uint64 can support up to 8GB. + */ + uint64_t maximumSizeInBytes{ 0 }; + + /** + * @brief Specifies where the data should be stored withing the device. + * Implementation is defined by the user who integrates FWE with their filesystem library. + */ + std::string storageLocation; + + /** + * @brief The minimum amount of time to keep data on disk after it is collected. + * When this TTL expires, data may be deleted, but it is not guaranteed to be deleted immediately after expiry. + * Can hold TTL more than 132 years. + */ + uint32_t minimumTimeToLiveInSeconds{ 0 }; +}; + +struct UploadOptions +{ + /** + * @brief Root condition node for the Abstract Syntax Tree. + */ + ExpressionNode *conditionTree{ nullptr }; +}; + +struct PartitionConfiguration +{ + /** + * @brief Optional Store and Forward storage options. + * If not specified, data in this partition will be uploaded in realtime. + */ + StorageOptions storageOptions; + + /** + * @brief Store and Forward upload options defines when the stored data may be uploaded. + * It is only used when spoolingMode=TO_DISK. May be non-null only when spoolingMode=TO_DISK. + */ + UploadOptions uploadOptions; +}; +#endif + /** * @brief ICollectionScheme is used to exchange CollectionScheme between components * @@ -247,12 +354,26 @@ class ICollectionScheme const S3UploadMetadata INVALID_S3_UPLOAD_METADATA = S3UploadMetadata(); #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + /** + * @brief Configuration of store and forward campaign. + */ + using StoreAndForwardConfig = std::vector; + const StoreAndForwardConfig INVALID_STORE_AND_FORWARD_CONFIG = std::vector(); +#endif + /** * @brief ExpressionNode_t is a vector that represents the AST Expression Tree per collectionScheme provided. */ using ExpressionNode_t = std::vector; const ExpressionNode_t INVALID_EXPRESSION_NODES = std::vector(); + /** + * @brief FetchInformation_t is a vector that represents all fetch informations in the CollectionScheme. + */ + using FetchInformation_t = std::vector; + const FetchInformation_t INVALID_FETCH_INFORMATIONS = std::vector(); + const uint64_t INVALID_COLLECTION_SCHEME_START_TIME = std::numeric_limits::max(); const uint64_t INVALID_COLLECTION_SCHEME_EXPIRY_TIME = std::numeric_limits::max(); const uint32_t INVALID_MINIMUM_PUBLISH_TIME = std::numeric_limits::max(); @@ -260,6 +381,9 @@ class ICollectionScheme const uint32_t INVALID_PRIORITY_LEVEL = std::numeric_limits::max(); const SyncID INVALID_COLLECTION_SCHEME_ID = SyncID(); const SyncID INVALID_DECODER_MANIFEST_ID = SyncID(); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + const std::string INVALID_CAMPAIGN_ARN = std::string(); +#endif virtual bool operator==( const ICollectionScheme &other ) const = 0; @@ -286,6 +410,15 @@ class ICollectionScheme */ virtual const SyncID &getCollectionSchemeID() const = 0; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + /** + * @brief Get the Amazon Resource Name of the campaign this collectionScheme is part of + * + * @return A string containing the Campaign Arn of this collectionScheme + */ + virtual const std::string &getCampaignArn() const = 0; +#endif + /** * @brief Get the associated Decoder Manifest ID of this collectionScheme * @@ -401,6 +534,30 @@ class ICollectionScheme virtual S3UploadMetadata getS3UploadMetadata() const = 0; #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + /** + * @brief Returns store and forward campaign configuration + * + * @return if not ready an empty vector + */ + virtual const StoreAndForwardConfig &getStoreAndForwardConfiguration() const = 0; +#endif + + /** + * @brief Get all expression nodes used for fetching conditions. Return empty vector if it is not ready. + */ + virtual const ExpressionNode_t &getAllExpressionNodesForFetchCondition() const = 0; + + /** + * @brief Get all expression nodes used for fetching actions. Return empty vector if it is not ready. + */ + virtual const ExpressionNode_t &getAllExpressionNodesForFetchAction() const = 0; + + /** + * @brief Get all fetch informations. Return empty vector if it is not ready. + */ + virtual const FetchInformation_t &getAllFetchInformations() const = 0; + virtual ~ICollectionScheme() = default; }; diff --git a/src/ICommandDispatcher.h b/src/ICommandDispatcher.h new file mode 100644 index 00000000..8b899e3b --- /dev/null +++ b/src/ICommandDispatcher.h @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "TimeTypes.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using CommandID = std::string; + +// 1:1 mapping with values from Protobuf enum Schemas::Commands::Status +enum class CommandStatus +{ + SUCCEEDED = 1, + EXECUTION_TIMEOUT = 2, + EXECUTION_FAILED = 4, + IN_PROGRESS = 10, +}; + +static inline std::string +commandStatusToString( CommandStatus status ) +{ + switch ( status ) + { + case CommandStatus::SUCCEEDED: + return "SUCCEEDED"; + case CommandStatus::EXECUTION_TIMEOUT: + return "EXECUTION_TIMEOUT"; + case CommandStatus::EXECUTION_FAILED: + return "EXECUTION_FAILED"; + case CommandStatus::IN_PROGRESS: + return "IN_PROGRESS"; + } + return "UNKNOWN " + std::to_string( static_cast( status ) ); +} + +using CommandReasonCode = uint32_t; +// clang-format off +static constexpr CommandReasonCode REASON_CODE_UNSPECIFIED = 0x00000000; +static constexpr CommandReasonCode REASON_CODE_IOTFLEETWISE_RANGE_START = 0x00000001; +static constexpr CommandReasonCode REASON_CODE_PRECONDITION_FAILED = 0x00000001; +static constexpr CommandReasonCode REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC = 0x00000002; +static constexpr CommandReasonCode REASON_CODE_NO_DECODING_RULES_FOUND = 0x00000003; +static constexpr CommandReasonCode REASON_CODE_COMMAND_REQUEST_PARSING_FAILED = 0x00000004; +static constexpr CommandReasonCode REASON_CODE_NO_COMMAND_DISPATCHER_FOUND = 0x00000005; +static constexpr CommandReasonCode REASON_CODE_STATE_TEMPLATE_OUT_OF_SYNC = 0x00000006; +static constexpr CommandReasonCode REASON_CODE_ARGUMENT_TYPE_MISMATCH = 0x00000007; +static constexpr CommandReasonCode REASON_CODE_NOT_SUPPORTED = 0x00000008; +static constexpr CommandReasonCode REASON_CODE_BUSY = 0x00000009; +static constexpr CommandReasonCode REASON_CODE_REJECTED = 0x0000000A; +static constexpr CommandReasonCode REASON_CODE_ACCESS_DENIED = 0x0000000B; +static constexpr CommandReasonCode REASON_CODE_ARGUMENT_OUT_OF_RANGE = 0x0000000C; +static constexpr CommandReasonCode REASON_CODE_INTERNAL_ERROR = 0x0000000D; +static constexpr CommandReasonCode REASON_CODE_UNAVAILABLE = 0x0000000E; +static constexpr CommandReasonCode REASON_CODE_WRITE_FAILED = 0x0000000F; +static constexpr CommandReasonCode REASON_CODE_STATE_TEMPLATE_ALREADY_ACTIVATED = 0x00000010; +static constexpr CommandReasonCode REASON_CODE_STATE_TEMPLATE_ALREADY_DEACTIVATED = 0x00000011; +static constexpr CommandReasonCode REASON_CODE_TIMED_OUT_BEFORE_DISPATCH = 0x00000012; +static constexpr CommandReasonCode REASON_CODE_NO_RESPONSE = 0x00000013; +static constexpr CommandReasonCode REASON_CODE_IOTFLEETWISE_RANGE_END = 0x0000FFFF; +static constexpr CommandReasonCode REASON_CODE_OEM_RANGE_START = 0x00010000; +static constexpr CommandReasonCode REASON_CODE_OEM_RANGE_END = 0x0001FFFF; +// clang-format on +using CommandReasonDescription = std::string; + +/** + * This callback interface is passed down to the command dispatcher as a means to provide the status + * of a command execution. The callback is thread-safe and can be called on the command dispatcher's + * thread. The callback can be called multiple times with the status IN_PROGRESS, and/or + * once with another terminal status value, such as SUCCESSFUL or EXECUTION_FAILED. + * @param status The command status + * @param reasonCode Reason code for the status + * @param reasonDescription Reason description + */ +using NotifyCommandStatusCallback = std::function; + +/** + * This class is the interface for Command Dispatcher. As Command Dispatcher instances are vehicle + * network / service dependent, it inherit from this class to provide an unified interface for + * application to dispatch commands. + */ +class ICommandDispatcher +{ +public: + virtual ~ICommandDispatcher() = default; + + /** + * @brief Initializer command dispatcher with its associated underlying vehicle network / service + * @return True if successful. False otherwise. + */ + virtual bool init() = 0; + + /** + * @brief set actuator value + * @param actuatorName Actuator name + * @param signalValue Signal value + * @param commandId Command ID + * @param issuedTimestampMs Timestamp of when the command was issued in the cloud in ms since + * epoch. Note: it is possible that commands are received in a different the order at edge to + * the order they were issued in the cloud. Depending on the application, this parameter can be + * used to drop outdated commands or buffer and sort them into the issuing order. + * @param executionTimeoutMs Relative execution timeout in ms since `issuedTimestampMs`. A value + * of zero means no timeout. + * @param notifyStatusCallback Callback to notify command status + */ + virtual void setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) = 0; + + /** + * @brief Gets the actuator names supported by the command dispatcher + * @todo The decoder manifest doesn't yet have an indication of whether a signal is + * READ/WRITE/READ_WRITE. Until it does this interface is needed to get the names of the + * actuators supported by the command dispatcher, so that for string signals, buffers can be + * pre-allocated in the RawDataManager by the CollectionSchemeManager when a new decoder + * manifest arrives. When the READ/WRITE/READ_WRITE usage of a signal is available this + * interface can be removed. + * @return List of actuator names + */ + virtual std::vector getActuatorNames() = 0; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/IConnectivityModule.h b/src/IConnectivityModule.h index 8e23d21c..fe5deacf 100644 --- a/src/IConnectivityModule.h +++ b/src/IConnectivityModule.h @@ -15,12 +15,6 @@ namespace Aws namespace IoTFleetWise { -enum class QoS -{ - AT_MOST_ONCE = 0, - AT_LEAST_ONCE = 1, -}; - /** * @brief called after the mqtt client is connected */ @@ -36,14 +30,10 @@ class IConnectivityModule /** * @brief create a new sender sharing the connection of this module * - * @param topicName the topic which this sender should publish to - * @param publishQoS the QoS level for the publish messages - * * @return a pointer to the newly created sender. A reference to the newly created sender is also hold inside this * module. */ - virtual std::shared_ptr createSender( const std::string &topicName, - QoS publishQoS = QoS::AT_MOST_ONCE ) = 0; + virtual std::shared_ptr createSender() = 0; /** * @brief create a new receiver sharing the connection of this module diff --git a/src/IDecoderDictionary.h b/src/IDecoderDictionary.h index 1f4293f3..aeb8ce58 100644 --- a/src/IDecoderDictionary.h +++ b/src/IDecoderDictionary.h @@ -112,5 +112,13 @@ struct ComplexDataDecoderDictionary : DecoderDictionary }; #endif +struct CustomDecoderDictionary : DecoderDictionary +{ + CustomDecoderDictionary() = default; + using CustomDecoderMethodType = + std::unordered_map>; + CustomDecoderMethodType customDecoderMethod; +}; + } // namespace IoTFleetWise } // namespace Aws diff --git a/src/IDecoderManifest.h b/src/IDecoderManifest.h index 3116604a..09a987b1 100644 --- a/src/IDecoderManifest.h +++ b/src/IDecoderManifest.h @@ -9,6 +9,7 @@ #include "SignalTypes.h" #include #include +#include #include namespace Aws @@ -130,6 +131,43 @@ struct PIDSignalDecoderFormat } }; +/* + * Custom signal decoder, which for example can be the fully-qualified-name of the signal + */ +using CustomSignalDecoder = std::string; +const CustomSignalDecoder INVALID_CUSTOM_SIGNAL_DECODER = {}; + +struct CustomSignalDecoderFormat +{ + InterfaceID mInterfaceId; + CustomSignalDecoder mDecoder; + + /** + * @brief Unique Signal ID provided by Cloud + */ + SignalID mSignalID{ 0x0 }; + + /** + * @brief The datatype of the signal. The default is double for backward compatibility + */ + SignalType mSignalType{ SignalType::DOUBLE }; + +public: + /** + * @brief Overload of the == operator + * @param other Other CustomSignalDecoderFormat object to compare + * @return true if ==, false otherwise + */ + bool + operator==( const CustomSignalDecoderFormat &other ) const + { + return ( mInterfaceId == other.mInterfaceId ) && ( mDecoder == other.mDecoder ); + } +}; + +using SignalIDToCustomSignalDecoderFormatMap = std::unordered_map; +using SignalIDToCustomSignalDecoderFormatMapPtr = std::shared_ptr; + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA /** * @brief Contains on ComplexSignal from the decoder manifest that can be used to decode big structured @@ -165,6 +203,11 @@ const PIDSignalDecoderFormat NOT_READY_PID_DECODER_FORMAT = PIDSignalDecoderForm */ const PIDSignalDecoderFormat NOT_FOUND_PID_DECODER_FORMAT = PIDSignalDecoderFormat(); +/** + * @brief Error code for custom signal decoder not found or not ready in decoder manifest + */ +const CustomSignalDecoderFormat INVALID_CUSTOM_SIGNAL_DECODER_FORMAT = CustomSignalDecoderFormat(); + /** * @brief IDecoderManifest is used to exchange DecoderManifest between components * @@ -243,6 +286,19 @@ class IDecoderManifest virtual ComplexDataElement getComplexDataType( ComplexDataTypeId typeId ) const = 0; #endif + /** + * @brief Get the custom decoder for this signal + * @param signalID the unique signalID + * @return invalid decoder if signal does not have a custom decoder + */ + virtual CustomSignalDecoderFormat getCustomSignalDecoderFormat( SignalID signalID ) const = 0; + + /** + * @brief Get custom signal decoder format map + * @return empty map if no map is present in the decoder manifest + */ + virtual SignalIDToCustomSignalDecoderFormatMapPtr getSignalIDToCustomSignalDecoderFormatMap() const = 0; + /** * @brief Used by the AWS IoT MQTT callback to copy data received from Cloud into this object without any further * processing to minimize time spent in callback context. diff --git a/src/IRemoteDiagnostics.h b/src/IRemoteDiagnostics.h new file mode 100644 index 00000000..c66f86fb --- /dev/null +++ b/src/IRemoteDiagnostics.h @@ -0,0 +1,122 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/*! UDSStatusMask DTC status mask for more detail please refer ISO 14229-1*/ +enum class UDSStatusMask +{ + TEST_FAILED = 0x01, + TEST_FAILED_THIS_OP_CYCLE = 0x02, + PENDING_DTC = 0x04, + CONFIRMED_DTC = 0x08, + TEST_NOT_COMPLETED_SINCE_LAST_CLEAR = 0x10, + TEST_FAILED_SINCE_LAST_CLEAR = 0x20, + TEST_NOT_COMPLETED_THIS_OP_CYCLE = 0x40, + WARNING_INDICATOR_REQUESTED = 0x80 +}; + +/*! UDSSubFunction UDS DTC subfunction; for more detail please refer ISO 14229-1*/ +// coverity[autosar_cpp14_a7_2_4_violation] Need to start from 1 +// coverity[misra_cpp_2008_rule_8_5_3_violation] Need to start from 1 +enum class UDSSubFunction +{ + NO_DTC_BY_STATUS_MASK = 0x01, + DTC_BY_STATUS_MASK, + DTC_SNAPSHOT_IDENTIFICATION, + DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER, + DTC_STORED_DATA_BY_RECORD_NUMBER, + DTC_EXT_DATA_RECORD_BY_DTC_NUMBER, + NO_DTC_BY_SEVERITY_MASK_RECORD, + DTC_BY_SEVERITY_MASK_RECORD, + SEVERITY_INFORMATION_OF_DTC, + SUPPORTED_DTCS, + FIRST_TEST_FAILED_DTC, + FIRST_CONFIRMED_DTC, + MOST_RECENT_TEST_FAILED_DTC, + MOST_RECENT_CONFIRMED_DTC, + MIRROR_MR_DTC_BY_STATUS_MASK, + MIRROR_MR_DTC_EXT_DATA_RECORD_BY_DTC, + NO_MIRROR_MR_DTC_BY_STATUS_MASK, + NO_EMISSION_OBD_DTC_BY_STATUS_MASK, + EMISSION_OBD_DTC_BY_STATUS_MASK, + DTC_FAULT_DETECTION_COUNTER, + DTC_WITH_PERMANENT_STATUS, + DTC_EXT_DATA_RECORD_BY_RECORD_NUMBER, + USER_DEF_MR_DTC_BY_STATUS_MASK, + USER_DEF_MR_DTC_SNAP_REC_BY_DTC, + USER_DEF_MR_DTC_EXT_DATA_REC_BY_DTC +}; + +// UDS query response for each ECU +struct UDSDTCInfo +{ + std::vector dtcBuffer; + int32_t targetAddress{ 0 }; +}; + +// Response format of UDS query for async requests +struct DTCResponse +{ + // Negative number means an error by processing (e.g. no response) + // Value 1 means successful processing + int8_t result{ -1 }; + std::vector dtcInfo; + std::string token; +}; + +// Callback format for UDS query results processing +using UDSResponseCallback = std::function; + +// This class is the interface for UDS queries +class IRemoteDiagnostics +{ +public: + virtual ~IRemoteDiagnostics() = default; + + /** + * @brief Asynchronously reads Diagnostic Trouble Code (DTC) information. + * + * @param targetAddress The target ECU address to query. + * @param subfn The UDS subfunction to use for the DTC read operation. + * @param mask The status mask for filtering DTCs. + * @param callback Function to be called when the operation completes. + * @param token Unique identifier for this query. + */ + virtual void readDTCInfo( int32_t targetAddress, + UDSSubFunction subfn, + UDSStatusMask mask, + UDSResponseCallback callback, + const std::string &token ) = 0; + /** + * @brief Asynchronously reads Diagnostic Trouble Code (DTC) snapshot or extended data + * information. + * + * @param targetAddress The target ECU address to query. + * @param subfn The UDS subfunction to use for the DTC read operation. + * @param dtc The specific Diagnostic Trouble Code to query. + * @param recordNumber The record number associated with the DTC. + * @param callback Function to be called when the operation completes. + * @param token Unique identifier for this query. + */ + virtual void readDTCInfoByDTCAndRecordNumber( int32_t targetAddress, + UDSSubFunction subfn, + uint32_t dtc, + uint8_t recordNumber, + UDSResponseCallback callback, + const std::string &token ) = 0; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/ISender.h b/src/ISender.h index 96c8b242..d76690a7 100644 --- a/src/ISender.h +++ b/src/ISender.h @@ -4,7 +4,10 @@ #pragma once #include "IConnectionTypes.h" +#include "TopicConfig.h" +#include #include +#include #include namespace Aws @@ -40,6 +43,12 @@ using OnDataSentCallback = std::function; */ using OnConnectionEstablishedCallback = std::function; +enum class QoS +{ + AT_MOST_ONCE = 0, + AT_LEAST_ONCE = 1, +}; + /** * @brief This interface will be used by all objects sending data to the cloud * @@ -73,6 +82,7 @@ class ISender * function call. It can be called from any thread because the function will if needed * copy the buffer. * + * @param topic the topic to send the data to. * @param buf pointer to raw data to send that needs to be at least size long. * The function does not care if the data is a c string, a json or a binary * data stream like proto buf. The data behind buf will not be modified. @@ -82,15 +92,17 @@ class ISender * @param callback callback that will be called when the operation completes (successfully or not). * IMPORTANT: The callback can be called by the same thread before sendBuffer even returns * or a separate thread, depending on whether the results are known synchronously or asynchronously. + * @param qos the QoS level for the publish messages */ - virtual void sendBuffer( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) = 0; - - virtual void sendBufferToTopic( const std::string &topic, - const std::uint8_t *buf, - size_t size, - OnDataSentCallback callback ) = 0; + virtual void sendBuffer( const std::string &topic, + const std::uint8_t *buf, + size_t size, + OnDataSentCallback callback, + QoS qos = QoS::AT_LEAST_ONCE ) = 0; virtual unsigned getPayloadCountSent() const = 0; + + virtual const TopicConfig &getTopicConfig() const = 0; }; } // namespace IoTFleetWise diff --git a/src/ISomeipInterfaceWrapper.h b/src/ISomeipInterfaceWrapper.h new file mode 100644 index 00000000..8fd5b6a4 --- /dev/null +++ b/src/ISomeipInterfaceWrapper.h @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +static inline std::string +commonapiCallStatusToString( CommonAPI::CallStatus callStatus ) +{ + switch ( callStatus ) + { + case CommonAPI::CallStatus::SUCCESS: + return "SUCCESS"; + case CommonAPI::CallStatus::OUT_OF_MEMORY: + return "OUT_OF_MEMORY"; + case CommonAPI::CallStatus::NOT_AVAILABLE: + return "NOT_AVAILABLE"; + case CommonAPI::CallStatus::CONNECTION_FAILED: + return "CONNECTION_FAILED"; + case CommonAPI::CallStatus::REMOTE_ERROR: + return "REMOTE_ERROR"; + case CommonAPI::CallStatus::UNKNOWN: + return "UNKNOWN"; + case CommonAPI::CallStatus::INVALID_VALUE: + return "INVALID_VALUE"; + case CommonAPI::CallStatus::SUBSCRIPTION_REFUSED: + return "SUBSCRIPTION_REFUSED"; + case CommonAPI::CallStatus::SERIALIZATION_ERROR: + return "SERIALIZATION_ERROR"; + default: + return "Unknown error: " + std::to_string( static_cast( callStatus ) ); + } +} + +static inline CommandStatus +commonapiCallStatusToCommandStatus( CommonAPI::CallStatus callStatus ) +{ + return ( callStatus == CommonAPI::CallStatus::SUCCESS ) ? CommandStatus::SUCCEEDED + : CommandStatus::EXECUTION_FAILED; +} + +static inline CommandReasonCode +commonapiCallStatusToReasonCode( CommonAPI::CallStatus callStatus ) +{ + return static_cast( callStatus ) + REASON_CODE_OEM_RANGE_START; +} + +static inline CommonAPI::Timeout_t +commonapiGetRemainingTimeout( Timestamp issuedTimestampMs, Timestamp executionTimeoutMs ) +{ + if ( executionTimeoutMs == 0 ) + { + // No timeout + return -1; // Defined in capicxx-core-runtime include/CommonAPI/Types.hpp + } + auto currentTimeMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + if ( ( issuedTimestampMs + executionTimeoutMs ) <= currentTimeMs ) + { + return 0; // Already timed-out + } + return static_cast( issuedTimestampMs + executionTimeoutMs - currentTimeMs ); +} + +// type for someip method wrapper +using SomeipMethodWrapperType = std::function; + +/** + * SomeipMethodInfo is a struct contains the information about method: input value type, method signature + */ +struct SomeipMethodInfo +{ + SignalType signalType; + SomeipMethodWrapperType methodWrapper; +}; + +class ISomeipInterfaceWrapper +{ +public: + virtual ~ISomeipInterfaceWrapper() = default; + virtual bool init() = 0; + virtual std::shared_ptr getProxy() const = 0; + virtual const std::unordered_map &getSupportedActuatorInfo() const = 0; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/IWaveGpsSource.cpp b/src/IWaveGpsSource.cpp index e182f001..e96688c4 100644 --- a/src/IWaveGpsSource.cpp +++ b/src/IWaveGpsSource.cpp @@ -2,122 +2,35 @@ // SPDX-License-Identifier: Apache-2.0 #include "IWaveGpsSource.h" -#include "CollectionInspectionAPITypes.h" #include "LoggingModule.h" -#include "QueueTypes.h" -#include +#include "SignalTypes.h" +#include "Thread.h" +#include #include #include #include #include -#include #include #include #include +#include namespace Aws { namespace IoTFleetWise { -// NOLINT below due to C++17 warning of redundant declarations that are required to maintain C++14 compatibility -constexpr const char *IWaveGpsSource::PATH_TO_NMEA; // NOLINT -constexpr const char *IWaveGpsSource::CAN_CHANNEL_NUMBER; // NOLINT -constexpr const char *IWaveGpsSource::CAN_RAW_FRAME_ID; // NOLINT -constexpr const char *IWaveGpsSource::LATITUDE_START_BIT; // NOLINT -constexpr const char *IWaveGpsSource::LONGITUDE_START_BIT; // NOLINT -const std::string DEFAULT_NMEA_SOURCE = "ttyUSB1"; -const std::string DEFAULT_PATH_TO_NMEA_SOURCE = "/dev/" + DEFAULT_NMEA_SOURCE; -const std::string SYS_USB_DEVICES_PATH = "/sys/bus/usb/devices"; -const std::string QUECTEL_VENDOR_ID = "2c7c"; - -std::string -IWaveGpsSource::getFileContents( const std::string &p ) -{ - constexpr auto NUM_CHARS = 1; - std::string ret; - std::ifstream fs{ p }; - // False alarm: uninit_use_in_call: Using uninitialized value "fs._M_streambuf_state" when calling "good". - // coverity[uninit_use_in_call : SUPPRESS] - while ( fs.good() ) - { - auto c = static_cast( fs.get() ); - ret.append( NUM_CHARS, c ); - } - - return ret; -} - -bool -IWaveGpsSource::detectQuectelDevice() -{ - if ( !boost::filesystem::exists( DEFAULT_PATH_TO_NMEA_SOURCE ) ) - { - return false; - } - for ( boost::filesystem::directory_iterator it( SYS_USB_DEVICES_PATH ); - it != boost::filesystem::directory_iterator(); - ++it ) - { - if ( ( !boost::filesystem::is_directory( *it ) ) || - ( !boost::filesystem::exists( it->path().string() + "/uevent" ) ) || - ( !boost::filesystem::exists( it->path().string() + "/" + DEFAULT_NMEA_SOURCE ) ) ) - { - continue; - } - auto result = getFileContents( it->path().string() + "/uevent" ); - if ( result.find( QUECTEL_VENDOR_ID ) != std::string::npos ) - { - return true; - } - } - return false; -} - -IWaveGpsSource::IWaveGpsSource( SignalBufferDistributorPtr signalBufferDistributor ) - : mSignalBufferDistributor{ std::move( signalBufferDistributor ) } -{ -} - -bool -IWaveGpsSource::init( const std::string &pathToNmeaSource, - CANChannelNumericID canChannel, - CANRawFrameID canRawFrameId, - uint16_t latitudeStartBit, - uint16_t longitudeStartBit ) -{ - if ( canChannel == INVALID_CAN_SOURCE_NUMERIC_ID ) - { - FWE_LOG_ERROR( "Invalid CAN channel" ); - return false; - } - // If no configuration was provided, auto-detect the GPS presence for backwards compatibility - if ( pathToNmeaSource.empty() ) - { - if ( !detectQuectelDevice() ) - { - FWE_LOG_TRACE( "No Quectel device detected" ); - return false; - } - FWE_LOG_INFO( "Quectel device detected at " + DEFAULT_PATH_TO_NMEA_SOURCE ); - mPathToNmeaSource = DEFAULT_PATH_TO_NMEA_SOURCE; - } - else - { - mPathToNmeaSource = pathToNmeaSource; - } - mLatitudeStartBit = latitudeStartBit; - mLongitudeStartBit = longitudeStartBit; - mCanChannel = canChannel; - mCanRawFrameId = canRawFrameId; - setFilter( mCanChannel, mCanRawFrameId ); - mCyclicLoggingTimer.reset(); - return true; -} -const char * -IWaveGpsSource::getThreadName() +IWaveGpsSource::IWaveGpsSource( std::shared_ptr namedSignalDataSource, + std::string pathToNmeaSource, + std::string latitudeSignalName, + std::string longitudeSignalName, + uint32_t pollIntervalMs ) + : mNamedSignalDataSource( std::move( namedSignalDataSource ) ) + , mPathToNmeaSource( std::move( pathToNmeaSource ) ) + , mLatitudeSignalName( std::move( latitudeSignalName ) ) + , mLongitudeSignalName( std::move( longitudeSignalName ) ) + , mPollIntervalMs( pollIntervalMs ) { - return "IWaveGpsSource"; } void @@ -160,20 +73,18 @@ IWaveGpsSource::pollData() i++; } - // If values were found pass them on as Signals similar to CAN Signals - if ( foundValid && mSignalBufferDistributor != nullptr ) + if ( foundValid ) { mValidCoordinateCounter++; - auto timestamp = mClock->systemTimeSinceEpochMs(); - - CollectedSignalsGroup collectedSignalsGroup; - collectedSignalsGroup.push_back( CollectedSignal( - getSignalIdFromStartBit( mLatitudeStartBit ), timestamp, lastValidLatitude, SignalType::DOUBLE ) ); - collectedSignalsGroup.push_back( CollectedSignal( - getSignalIdFromStartBit( mLongitudeStartBit ), timestamp, lastValidLongitude, SignalType::DOUBLE ) ); - mSignalBufferDistributor->push( CollectedDataFrame( collectedSignalsGroup ) ); + std::vector> values; + values.emplace_back( + std::make_pair( mLatitudeSignalName, DecodedSignalValue( lastValidLatitude, SignalType::DOUBLE ) ) ); + values.emplace_back( + std::make_pair( mLongitudeSignalName, DecodedSignalValue( lastValidLongitude, SignalType::DOUBLE ) ) ); + mNamedSignalDataSource->ingestMultipleSignalValues( 0, values ); } + if ( mCyclicLoggingTimer.getElapsedMs().count() > CYCLIC_LOG_PERIOD_MS ) { FWE_LOG_TRACE( "In the last " + std::to_string( CYCLIC_LOG_PERIOD_MS ) + " millisecond found " + @@ -268,24 +179,44 @@ IWaveGpsSource::extractLongAndLatitudeFromLine( bool IWaveGpsSource::connect() { + if ( mPollIntervalMs == 0 ) + { + FWE_LOG_ERROR( "Zero poll interval time" ); + return false; + } + mFileHandle = open( mPathToNmeaSource.c_str(), O_RDONLY | O_NOCTTY ); if ( mFileHandle == -1 ) { FWE_LOG_ERROR( "Could not open GPS NMEA file:" + mPathToNmeaSource ); return false; } + + mThread = std::thread( [this]() { + Thread::setCurrentThreadName( "IWaveGpsSource" ); + while ( !mShouldStop ) + { + pollData(); + std::this_thread::sleep_for( std::chrono::milliseconds( mPollIntervalMs ) ); + } + } ); return true; } -bool -IWaveGpsSource::disconnect() +IWaveGpsSource::~IWaveGpsSource() { + if ( mThread.joinable() ) + { + mShouldStop = true; + mThread.join(); + } if ( close( mFileHandle ) != 0 ) { - return false; + FWE_LOG_ERROR( "Could not close NMEA file" ); } mFileHandle = -1; - return true; + // coverity[cert_err50_cpp_violation] false positive - join is called to exit the previous thread + // coverity[autosar_cpp14_a15_5_2_violation] false positive - join is called to exit the previous thread } } // namespace IoTFleetWise diff --git a/src/IWaveGpsSource.h b/src/IWaveGpsSource.h index 96c2abf2..3ca3a45a 100644 --- a/src/IWaveGpsSource.h +++ b/src/IWaveGpsSource.h @@ -2,67 +2,52 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include "Clock.h" -#include "ClockHandler.h" -#include "CollectionInspectionAPITypes.h" -#include "CustomDataSource.h" -#include "SignalTypes.h" +#include "NamedSignalDataSource.h" #include "Timer.h" +#include #include #include #include +#include namespace Aws { namespace IoTFleetWise { -/** - * To implement a custom data source create a new class and inherit from CustomDataSource - * then call setFilter() then start() and provide an implementation for pollData - */ -class IWaveGpsSource : public CustomDataSource +class IWaveGpsSource { public: /** - * @param signalBufferDistributor Signal buffer distributor + * @param namedSignalDataSource Named signal data source + * @param pathToNmeaSource Path to the file/tty with the NMEA output with the GPS data + * @param latitudeSignalName the signal name of the latitude signal + * @param longitudeSignalName the signal name of the longitude signal + * @param pollIntervalMs the poll interval to read the GPS position */ - IWaveGpsSource( SignalBufferDistributorPtr signalBufferDistributor ); - /** - * Initialize IWaveGpsSource and set filter for CustomDataSource - * - * @param pathToNmeaSource Path to the file/tty with the NMEA output with the GPS data, or blank to auto-detect - * @param canChannel the CAN channel used in the decoder manifest - * @param canRawFrameId the CAN message Id used in the decoder manifest - * @param latitudeStartBit the startBit used in the decoder manifest for the latitude signal - * @param longitudeStartBit the startBit used in the decoder manifest for the longitude signal - * - * @return on success true otherwise false - */ - bool init( const std::string &pathToNmeaSource, - CANChannelNumericID canChannel, - CANRawFrameID canRawFrameId, - uint16_t latitudeStartBit, - uint16_t longitudeStartBit ); + IWaveGpsSource( std::shared_ptr namedSignalDataSource, + std::string pathToNmeaSource, + std::string latitudeSignalName, + std::string longitudeSignalName, + uint32_t pollIntervalMs ); + ~IWaveGpsSource(); + + IWaveGpsSource( const IWaveGpsSource & ) = delete; + IWaveGpsSource &operator=( const IWaveGpsSource & ) = delete; + IWaveGpsSource( IWaveGpsSource && ) = delete; + IWaveGpsSource &operator=( IWaveGpsSource && ) = delete; bool connect(); - bool disconnect(); static constexpr const char *PATH_TO_NMEA = "nmeaFilePath"; - static constexpr const char *CAN_CHANNEL_NUMBER = "canChannel"; - static constexpr const char *CAN_RAW_FRAME_ID = "canFrameId"; - static constexpr const char *LATITUDE_START_BIT = "latitudeStartBit"; - static constexpr const char *LONGITUDE_START_BIT = "longitudeStartBit"; - -protected: - void pollData() override; - const char *getThreadName() override; + static constexpr const char *LATITUDE_SIGNAL_NAME = "latitudeSignalName"; + static constexpr const char *LONGITUDE_SIGNAL_NAME = "longitudeSignalName"; + static constexpr const char *POLL_INTERVAL_MS = "pollIntervalMs"; private: + void pollData(); static bool validLatitude( double latitude ); static bool validLongitude( double longitude ); - static std::string getFileContents( const std::string &p ); - static bool detectQuectelDevice(); /** * The NMEA protocol provides the position in $GPGGA in the following format @@ -80,19 +65,18 @@ class IWaveGpsSource : public CustomDataSource static const uint32_t CYCLIC_LOG_PERIOD_MS = 10000; static const uint32_t MAX_BYTES_READ_PER_POLL = 2048; - uint16_t mLatitudeStartBit = 0; - uint16_t mLongitudeStartBit = 0; - int mFileHandle = -1; - SignalBufferDistributorPtr mSignalBufferDistributor; - std::shared_ptr mClock = ClockHandler::getClock(); Timer mCyclicLoggingTimer; uint32_t mGpggaLineCounter = 0; uint32_t mValidCoordinateCounter = 0; + std::shared_ptr mNamedSignalDataSource; std::string mPathToNmeaSource; - CANChannelNumericID mCanChannel{ INVALID_CAN_SOURCE_NUMERIC_ID }; - CANRawFrameID mCanRawFrameId{}; + std::string mLatitudeSignalName; + std::string mLongitudeSignalName; + uint32_t mPollIntervalMs; char mBuffer[MAX_BYTES_READ_PER_POLL]{}; + std::thread mThread; + std::atomic mShouldStop{}; }; } // namespace IoTFleetWise diff --git a/src/InspectionMatrixExtractor.cpp b/src/InspectionMatrixExtractor.cpp index 0ad048b1..c4ca1a6e 100644 --- a/src/InspectionMatrixExtractor.cpp +++ b/src/InspectionMatrixExtractor.cpp @@ -16,6 +16,86 @@ namespace Aws namespace IoTFleetWise { +void +CollectionSchemeManager::updateRawDataBufferConfigStringSignals( + std::unordered_map &updatedSignals ) +{ + if ( mDecoderManifest == nullptr ) + { + return; + } + if ( isCollectionSchemesInSyncWithDm() ) + { + // Iterate through enabled collectionScheme lists to locate the string signals to be collected + for ( auto it = mEnabledCollectionSchemeMap.begin(); it != mEnabledCollectionSchemeMap.end(); ++it ) + { + const auto &collectionSchemePtr = it->second; + for ( const auto &signalInfo : collectionSchemePtr->getCollectSignals() ) + { + SignalID signalId = signalInfo.signalID; + RawData::SignalUpdateConfig signalConfig; + signalConfig.typeId = signalId; + + auto networkType = mDecoderManifest->getNetworkProtocol( signalId ); + if ( networkType != VehicleDataSourceProtocol::CUSTOM_DECODING ) + { + continue; + } + + auto customSignalDecoderFormat = mDecoderManifest->getCustomSignalDecoderFormat( signalId ); + if ( customSignalDecoderFormat == INVALID_CUSTOM_SIGNAL_DECODER_FORMAT ) + { + continue; + } + + if ( customSignalDecoderFormat.mSignalType != SignalType::STRING ) + { + continue; + } + + signalConfig.interfaceId = customSignalDecoderFormat.mInterfaceId; + signalConfig.messageId = customSignalDecoderFormat.mDecoder; + + updatedSignals[signalId] = signalConfig; + } + } + } +#ifdef FWE_FEATURE_REMOTE_COMMANDS + /* TODO: Here we should iterate over all signals from all network interface types looking + for string signals that are WRITE or READ_WRITE and add them. + But the READ/WRITE/READ_WRITE indication for each signal is not yet available, so until then + we only support custom decoded signals with the decoder id being the actuator name. */ + auto customSignalDecoderFormatMap = mDecoderManifest->getSignalIDToCustomSignalDecoderFormatMap(); + if ( mGetActuatorNamesCallback && customSignalDecoderFormatMap ) + { + auto actuatorNames = mGetActuatorNamesCallback(); + for ( const auto &interface : actuatorNames ) + { + for ( const auto &actuatorName : interface.second ) + { + for ( const auto &customSignalDecoderFormat : *customSignalDecoderFormatMap ) + { + if ( ( interface.first != customSignalDecoderFormat.second.mInterfaceId ) || + ( actuatorName != customSignalDecoderFormat.second.mDecoder ) ) + { + continue; + } + if ( customSignalDecoderFormat.second.mSignalType == SignalType::STRING ) + { + auto signalId = customSignalDecoderFormat.first; + updatedSignals[signalId] = + RawData::SignalUpdateConfig{ signalId, + customSignalDecoderFormat.second.mInterfaceId, + customSignalDecoderFormat.second.mDecoder }; + } + break; + } + } + } + } +#endif +} + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA void CollectionSchemeManager::updateRawDataBufferConfigComplexSignals( @@ -81,6 +161,9 @@ CollectionSchemeManager::addConditionData( const ICollectionSchemePtr &collectio conditionData.metadata.priority = collectionScheme->getPriority(); conditionData.metadata.decoderID = collectionScheme->getDecoderManifestID(); conditionData.metadata.collectionSchemeID = collectionScheme->getCollectionSchemeID(); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + conditionData.metadata.campaignArn = collectionScheme->getCampaignArn(); +#endif /* * use for loop to copy signalInfo and CANframe over to avoid error or memory issue @@ -100,30 +183,28 @@ CollectionSchemeManager::addConditionData( const ICollectionSchemePtr &collectio conditionData.signals.emplace_back( inspectionSignal ); } + const std::vector &collectionCANFrames = collectionScheme->getCollectRawCanFrames(); + for ( uint32_t i = 0; i < collectionCANFrames.size(); i++ ) { - const std::vector &collectionCANFrames = collectionScheme->getCollectRawCanFrames(); - for ( uint32_t i = 0; i < collectionCANFrames.size(); i++ ) + InspectionMatrixCanFrameCollectionInfo CANFrame = {}; + CANFrame.frameID = collectionCANFrames[i].frameID; + CANFrame.channelID = mCANIDTranslator.getChannelNumericID( collectionCANFrames[i].interfaceID ); + CANFrame.sampleBufferSize = collectionCANFrames[i].sampleBufferSize; + CANFrame.minimumSampleIntervalMs = collectionCANFrames[i].minimumSampleIntervalMs; + if ( CANFrame.channelID == INVALID_CAN_SOURCE_NUMERIC_ID ) { - InspectionMatrixCanFrameCollectionInfo CANFrame = {}; - CANFrame.frameID = collectionCANFrames[i].frameID; - CANFrame.channelID = mCANIDTranslator.getChannelNumericID( collectionCANFrames[i].interfaceID ); - CANFrame.sampleBufferSize = collectionCANFrames[i].sampleBufferSize; - CANFrame.minimumSampleIntervalMs = collectionCANFrames[i].minimumSampleIntervalMs; - if ( CANFrame.channelID == INVALID_CAN_SOURCE_NUMERIC_ID ) - { - FWE_LOG_WARN( "Invalid Interface ID provided: " + collectionCANFrames[i].interfaceID ); - } - else - { - conditionData.canFrames.emplace_back( CANFrame ); - } + FWE_LOG_WARN( "Invalid Interface ID provided: " + collectionCANFrames[i].interfaceID ); + } + else + { + conditionData.canFrames.emplace_back( CANFrame ); } - - conditionData.minimumPublishIntervalMs = collectionScheme->getMinimumPublishIntervalMs(); - conditionData.afterDuration = collectionScheme->getAfterDurationMs(); - conditionData.includeActiveDtcs = collectionScheme->isActiveDTCsIncluded(); - conditionData.triggerOnlyOnRisingEdge = collectionScheme->isTriggerOnlyOnRisingEdge(); } + + conditionData.minimumPublishIntervalMs = collectionScheme->getMinimumPublishIntervalMs(); + conditionData.afterDuration = collectionScheme->getAfterDurationMs(); + conditionData.includeActiveDtcs = collectionScheme->isActiveDTCsIncluded(); + conditionData.triggerOnlyOnRisingEdge = collectionScheme->isTriggerOnlyOnRisingEdge(); } static void @@ -149,6 +230,23 @@ buildExpressionNodeMapAndVector( const ExpressionNode *expressionNode, isStaticCondition = false; } + if ( expressionNode->nodeType == ExpressionNodeType::CUSTOM_FUNCTION ) + { + // Custom functions should always be reevaluated + alwaysEvaluateCondition = true; + for ( const auto ¶m : expressionNode->function.customFunctionParams ) + { + buildExpressionNodeMapAndVector( + param, expressionNodeToIndexMap, expressionNodes, index, isStaticCondition, alwaysEvaluateCondition ); + } + } + + if ( expressionNode->nodeType == ExpressionNodeType::IS_NULL_FUNCTION ) + { + // Null function should always be reevaluated + alwaysEvaluateCondition = true; + } + buildExpressionNodeMapAndVector( expressionNode->left, expressionNodeToIndexMap, expressionNodes, @@ -164,11 +262,13 @@ buildExpressionNodeMapAndVector( const ExpressionNode *expressionNode, } void -CollectionSchemeManager::matrixExtractor( const std::shared_ptr &inspectionMatrix ) +CollectionSchemeManager::matrixExtractor( const std::shared_ptr &inspectionMatrix, + const std::shared_ptr &fetchMatrix ) { std::map expressionNodeToIndexMap; std::vector expressionNodes; uint32_t index = 0U; + FetchRequestID fetchRequestID = 0U; if ( !isCollectionSchemesInSyncWithDm() ) { @@ -185,6 +285,145 @@ CollectionSchemeManager::matrixExtractor( const std::shared_ptrgetCondition() ); + + ConditionWithCollectedData &conditionWithCollectedData = inspectionMatrix->conditions.back(); + + // extract FetchInformation + const std::vector &fetchInformations = collectionScheme->getAllFetchInformations(); + + for ( const auto &fetchInformation : fetchInformations ) + { + bool isValid = true; + + auto itFetchRequests = + fetchMatrix->fetchRequests.emplace( fetchRequestID, std::vector() ).first; + std::vector &fetchRequests = itFetchRequests->second; + + SignalID signalID = fetchInformation.signalID; + + for ( const auto &action : fetchInformation.actions ) + { + if ( action->nodeType != ExpressionNodeType::CUSTOM_FUNCTION ) + { + FWE_LOG_WARN( "Ignored fetch information due to unsupported action " + "(only custom functions are allowed)" ); + isValid = false; + break; + } + + fetchRequests.emplace_back(); + + FetchRequest &fetchRequest = fetchRequests.back(); + + fetchRequest.signalID = signalID; + fetchRequest.functionName = action->function.customFunctionName; + + for ( const auto ¶m : action->function.customFunctionParams ) + { + fetchRequest.args.emplace_back(); + + InspectionValue &arg = fetchRequest.args.back(); + + if ( param->nodeType == ExpressionNodeType::BOOLEAN ) + { + arg = param->booleanValue; + } + else if ( param->nodeType == ExpressionNodeType::FLOAT ) + { + arg = param->floatingValue; + } + else if ( param->nodeType == ExpressionNodeType::STRING ) + { + arg = param->stringValue; + } + else + { + FWE_LOG_WARN( "Ignored fetch information due to unsupported action arguments " + "(only boolean, double and string value are allowed)" ); + isValid = false; + break; + } + } + + if ( !isValid ) + { + break; + } + } + + if ( ( fetchInformation.condition == nullptr ) && ( fetchInformation.executionPeriodMs == 0 ) ) + { + FWE_LOG_WARN( "Ignored fetch information due to unsupported time based fetch configuration" ); + isValid = false; + } + + if ( !isValid ) + { + // invalid FetchInformation => remove key fetchRequestID from map fetchMatrix->fetchRequests + fetchMatrix->fetchRequests.erase( itFetchRequests ); + continue; + } + + if ( fetchInformation.condition == nullptr ) + { + // time-based fetch configuration + PeriodicalFetchParameters &periodicalFetchParameters = + fetchMatrix->periodicalFetchRequestSetup[fetchRequestID]; + + periodicalFetchParameters.fetchFrequencyMs = fetchInformation.executionPeriodMs; + // TODO: below parameters are not yet supported by the cloud and are ignored on edge + periodicalFetchParameters.maxExecutionCount = fetchInformation.maxExecutionPerInterval; + periodicalFetchParameters.maxExecutionCountResetPeriodMs = fetchInformation.executionIntervalMs; + } + else + { + // condition-based fetch configuration + ConditionForFetch conditionForFetch; + + conditionForFetch.condition = fetchInformation.condition; + conditionForFetch.triggerOnlyOnRisingEdge = fetchInformation.triggerOnlyOnRisingEdge; + conditionForFetch.fetchRequestID = fetchRequestID; + + conditionWithCollectedData.fetchConditions.emplace_back( conditionForFetch ); + + bool alwaysEvaluateCondition = false; + buildExpressionNodeMapAndVector( fetchInformation.condition, + expressionNodeToIndexMap, + expressionNodes, + index, + conditionWithCollectedData.isStaticCondition, + alwaysEvaluateCondition ); + } + + for ( auto &signal : conditionWithCollectedData.signals ) + { + if ( signal.signalID == signalID ) + { + signal.fetchRequestIDs.push_back( fetchRequestID ); + } + } + + fetchRequestID++; + } + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + for ( const auto &partition : collectionScheme->getStoreAndForwardConfiguration() ) + { + + bool alwaysEvaluateCondition = false; + ConditionForForward conditionForForward; + + conditionForForward.condition = partition.uploadOptions.conditionTree; + conditionWithCollectedData.forwardConditions.emplace_back( conditionForForward ); + + buildExpressionNodeMapAndVector( conditionForForward.condition, + expressionNodeToIndexMap, + expressionNodes, + index, + conditionWithCollectedData.isStaticCondition, + alwaysEvaluateCondition ); + } +#endif } // re-build all ExpressionNodes (for storage) and set ExpressionNode pointer addresses appropriately @@ -197,9 +436,23 @@ CollectionSchemeManager::matrixExtractor( const std::shared_ptrexpressionNodeStorage[i].nodeType = expressionNodes[i]->nodeType; inspectionMatrix->expressionNodeStorage[i].floatingValue = expressionNodes[i]->floatingValue; inspectionMatrix->expressionNodeStorage[i].booleanValue = expressionNodes[i]->booleanValue; + inspectionMatrix->expressionNodeStorage[i].stringValue = expressionNodes[i]->stringValue; inspectionMatrix->expressionNodeStorage[i].signalID = expressionNodes[i]->signalID; inspectionMatrix->expressionNodeStorage[i].function.windowFunction = expressionNodes[i]->function.windowFunction; + inspectionMatrix->expressionNodeStorage[i].function.customFunctionName = + expressionNodes[i]->function.customFunctionName; + inspectionMatrix->expressionNodeStorage[i].function.customFunctionInvocationId = + expressionNodes[i]->function.customFunctionInvocationId; + + for ( const auto ¶m : expressionNodes[i]->function.customFunctionParams ) + { + uint32_t paramIndex = expressionNodeToIndexMap[param]; + + inspectionMatrix->expressionNodeStorage[i].function.customFunctionParams.push_back( + &inspectionMatrix->expressionNodeStorage[paramIndex] ); + } + if ( expressionNodes[i]->left != nullptr ) { uint32_t leftIndex = expressionNodeToIndexMap[expressionNodes[i]->left]; @@ -221,6 +474,13 @@ CollectionSchemeManager::matrixExtractor( const std::shared_ptrexpressionNodeStorage[conditionIndex]; } + + for ( auto &conditionForFetch : conditionWithCollectedData.fetchConditions ) + { + uint32_t conditionIndex = expressionNodeToIndexMap[conditionForFetch.condition]; + + conditionForFetch.condition = &inspectionMatrix->expressionNodeStorage[conditionIndex]; + } } } @@ -253,5 +513,46 @@ CollectionSchemeManager::inspectionMatrixUpdater( const std::shared_ptr &fetchMatrix ) +{ + mFetchMatrixChangeListeners.notify( fetchMatrix ); +} + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +std::shared_ptr +CollectionSchemeManager::lastKnownStateExtractor() +{ + auto extractedStateTemplates = std::make_shared(); + + for ( auto &stateTemplate : mStateTemplates ) + { + if ( stateTemplate.second->decoderManifestID != mCurrentDecoderManifestID ) + { + FWE_LOG_INFO( "Decoder manifest out of sync: " + stateTemplate.second->decoderManifestID + " vs. " + + mCurrentDecoderManifestID ); + continue; + } + + // Intentionally copy it because we will need to modify the signals + auto newStateTemplate = std::make_shared( *stateTemplate.second ); + for ( auto &signal : newStateTemplate->signals ) + { + signal.signalType = getSignalType( signal.signalID ); + } + + extractedStateTemplates->emplace_back( newStateTemplate ); + } + + return extractedStateTemplates; +} + +void +CollectionSchemeManager::lastKnownStateUpdater( std::shared_ptr stateTemplates ) +{ + mStateTemplatesChangeListeners.notify( stateTemplates ); +} +#endif + } // namespace IoTFleetWise } // namespace Aws diff --git a/src/IoTFleetWiseEngine.cpp b/src/IoTFleetWiseEngine.cpp index d6a35059..19cccd27 100644 --- a/src/IoTFleetWiseEngine.cpp +++ b/src/IoTFleetWiseEngine.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #ifdef FWE_FEATURE_GREENGRASSV2 #include "AwsGreengrassV2ConnectivityModule.h" @@ -48,6 +47,28 @@ #include "DataSenderIonWriter.h" #include "VisionSystemDataSender.h" #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS +#include "CommandResponseDataSender.h" +#endif +#ifdef FWE_FEATURE_SOMEIP +#include "ExampleSomeipInterfaceWrapper.h" +#include +#include +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateDataSender.h" +#include "LastKnownStateInspector.h" +#endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include "RateLimiter.h" +#include "StreamForwarder.h" +#endif +#ifdef FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES +#include "CustomFunctionMath.h" +#endif +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE +#include +#endif namespace Aws { @@ -58,17 +79,36 @@ static constexpr uint64_t DEFAULT_RETRY_UPLOAD_PERSISTED_INTERVAL_MS = 10000; static const std::string CAN_INTERFACE_TYPE = "canInterface"; static const std::string EXTERNAL_CAN_INTERFACE_TYPE = "externalCanInterface"; static const std::string OBD_INTERFACE_TYPE = "obdInterface"; +static const std::string NAMED_SIGNAL_INTERFACE_TYPE = "namedSignalInterface"; #ifdef FWE_FEATURE_ROS2 static const std::string ROS2_INTERFACE_TYPE = "ros2Interface"; #endif +#ifdef FWE_FEATURE_SOMEIP +static const std::string SOMEIP_TO_CAN_BRIDGE_INTERFACE_TYPE = "someipToCanBridgeInterface"; +static const std::string SOMEIP_COLLECTION_INTERFACE_TYPE = "someipCollectionInterface"; +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS +#ifdef FWE_FEATURE_SOMEIP +static const std::string SOMEIP_COMMAND_INTERFACE_TYPE = "someipCommandInterface"; +#endif +static const std::string CAN_COMMAND_INTERFACE_TYPE = "canCommandInterface"; +static const std::unordered_map + EXAMPLE_CAN_INTERFACE_SUPPORTED_ACTUATOR_MAP = { + { "Vehicle.actuator6", { 0x00000123, 0x00000456, SignalType::INT32 } }, + { "Vehicle.actuator7", { 0x80000789, 0x80000ABC, SignalType::DOUBLE } }, +}; +#endif #ifdef FWE_FEATURE_IWAVE_GPS -static const std::string CONFIG_SECTION_IWAVE_GPS = "iWaveGpsExample"; +static const std::string IWAVE_GPS_INTERFACE_TYPE = "iWaveGpsInterface"; #endif #ifdef FWE_FEATURE_EXTERNAL_GPS -static const std::string CONFIG_SECTION_EXTERNAL_GPS = "externalGpsExample"; +static const std::string EXTERNAL_GPS_INTERFACE_TYPE = "externalGpsInterface"; #endif #ifdef FWE_FEATURE_AAOS_VHAL -static const std::string CONFIG_SECTION_AAOS_VHAL = "aaosVhalExample"; +static const std::string AAOS_VHAL_INTERFACE_TYPE = "aaosVhalInterface"; +#endif +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE +static const std::string UDS_DTC_INTERFACE = "exampleUDSInterface"; #endif namespace @@ -145,6 +185,28 @@ IoTFleetWiseEngine::~IoTFleetWiseEngine() setLogForwarding( nullptr ); } +#ifdef FWE_FEATURE_SOMEIP +static std::shared_ptr +createExampleSomeipInterfaceWrapper( const std::string &applicationName, + const std::string &exampleInstance, + std::shared_ptr rawBufferManager, + bool subscribeToLongRunningCommandStatus ) +{ + return std::make_shared( + "local", + exampleInstance, + applicationName, + []( std::string domain, + std::string instance, + std::string connection ) -> std::shared_ptr> { + return CommonAPI::Runtime::get()->buildProxy( + domain, instance, connection ); + }, + std::move( rawBufferManager ), + subscribeToLongRunningCommandStatus ); +} +#endif + bool IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesystem::path &configFileDirectoryPath ) { @@ -174,6 +236,10 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys else { FWE_LOG_INFO( "Persistency feature is disabled in the configuration." ); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + FWE_LOG_INFO( "Disabling Store and Forward feature as persistency is disabled." ); + mStoreAndForwardEnabled = false; +#endif } /*************************Payload Manager and Persistency library bootstrap end************/ @@ -183,45 +249,15 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys auto networkInterface = config["networkInterfaces"][i]; auto networkInterfaceType = networkInterface["type"].asStringRequired(); if ( ( networkInterfaceType == CAN_INTERFACE_TYPE ) || - ( networkInterfaceType == EXTERNAL_CAN_INTERFACE_TYPE ) ) + ( networkInterfaceType == EXTERNAL_CAN_INTERFACE_TYPE ) +#ifdef FWE_FEATURE_SOMEIP + || ( networkInterfaceType == SOMEIP_TO_CAN_BRIDGE_INTERFACE_TYPE ) +#endif + ) { mCANIDTranslator.add( networkInterface["interfaceId"].asStringRequired() ); } } -#ifdef FWE_FEATURE_IWAVE_GPS - if ( config["staticConfig"].isMember( CONFIG_SECTION_IWAVE_GPS ) ) - { - mCANIDTranslator.add( config["staticConfig"][CONFIG_SECTION_IWAVE_GPS][IWaveGpsSource::CAN_CHANNEL_NUMBER] - .asStringRequired() ); - } - else - { - mCANIDTranslator.add( "IWAVE-GPS-CAN" ); - } -#endif -#ifdef FWE_FEATURE_EXTERNAL_GPS - if ( config["staticConfig"].isMember( CONFIG_SECTION_EXTERNAL_GPS ) ) - { - mCANIDTranslator.add( - config["staticConfig"][CONFIG_SECTION_EXTERNAL_GPS][ExternalGpsSource::CAN_CHANNEL_NUMBER] - .asStringRequired() ); - } - else - { - mCANIDTranslator.add( "EXTERNAL-GPS-CAN" ); - } -#endif -#ifdef FWE_FEATURE_AAOS_VHAL - if ( config["staticConfig"].isMember( CONFIG_SECTION_AAOS_VHAL ) ) - { - mCANIDTranslator.add( config["staticConfig"][CONFIG_SECTION_AAOS_VHAL][AaosVhalSource::CAN_CHANNEL_NUMBER] - .asStringRequired() ); - } - else - { - mCANIDTranslator.add( "AAOS-VHAL-CAN" ); - } -#endif /*************************CAN InterfaceID to InternalID Translator end*********/ /**************************Connectivity bootstrap begin*******************************/ @@ -251,6 +287,14 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys auto mqttConfig = config["staticConfig"]["mqttConnection"]; auto clientId = mqttConfig["clientId"].asStringRequired(); std::string connectionType = mqttConfig["connectionType"].asStringOptional().get_value_or( "iotCore" ); + TopicConfigArgs topicConfigArgs; + topicConfigArgs.iotFleetWisePrefix = mqttConfig["iotFleetWiseTopicPrefix"].asStringOptional(); + topicConfigArgs.commandsPrefix = mqttConfig["commandsTopicPrefix"].asStringOptional(); + topicConfigArgs.deviceShadowPrefix = mqttConfig["deviceShadowTopicPrefix"].asStringOptional(); + topicConfigArgs.jobsPrefix = mqttConfig["jobsTopicPrefix"].asStringOptional(); + topicConfigArgs.metricsTopic = mqttConfig["metricsUploadTopic"].asStringOptional().get_value_or( "" ); + topicConfigArgs.logsTopic = mqttConfig["loggingUploadTopic"].asStringOptional().get_value_or( "" ); + mTopicConfig = std::make_unique( clientId, topicConfigArgs ); if ( connectionType == "iotCore" ) { @@ -324,7 +368,7 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys MQTT_SESSION_EXPIRY_INTERVAL_SECONDS ); mConnectivityModule = std::make_shared( - rootCA, clientId, std::move( builderWrapper ), mqttConnectionConfig ); + rootCA, clientId, std::move( builderWrapper ), *mTopicConfig, mqttConnectionConfig ); #ifdef FWE_FEATURE_S3 if ( config["staticConfig"].isMember( "credentialsProvider" ) ) @@ -344,7 +388,7 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys else if ( connectionType == "iotGreengrassV2" ) { FWE_LOG_INFO( "ConnectionType is iotGreengrassV2" ); - mConnectivityModule = std::make_shared( bootstrapPtr ); + mConnectivityModule = std::make_shared( bootstrapPtr, *mTopicConfig ); #ifdef FWE_FEATURE_S3 mAwsCredentialsProvider = std::make_shared(); #endif @@ -356,38 +400,78 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys return false; } - // Only CAN data channel needs a payloadManager object for persistency and compression support, - // for other components this will be nullptr - mSenderVehicleData = mConnectivityModule->createSender( mqttConfig["canDataTopic"].asStringRequired() ); + mReceiverCollectionSchemeList = mConnectivityModule->createReceiver( mTopicConfig->collectionSchemesTopic ); + mReceiverDecoderManifest = mConnectivityModule->createReceiver( mTopicConfig->decoderManifestTopic ); +#ifdef FWE_FEATURE_STORE_AND_FORWARD + if ( mStoreAndForwardEnabled ) + { + // Receivers to receive Store and Forward Data Upload Requests + mReceiverIotJob = mConnectivityModule->createReceiver( mTopicConfig->jobNotificationTopic ); + mReceiverJobDocumentAccepted = + mConnectivityModule->createReceiver( mTopicConfig->getJobExecutionAcceptedTopic ); + mReceiverJobDocumentRejected = + mConnectivityModule->createReceiver( mTopicConfig->getJobExecutionRejectedTopic ); + mReceiverPendingJobsAccepted = + mConnectivityModule->createReceiver( mTopicConfig->getPendingJobExecutionsAcceptedTopic ); + mReceiverPendingJobsRejected = + mConnectivityModule->createReceiver( mTopicConfig->getPendingJobExecutionsRejectedTopic ); + mReceiverUpdateIotJobStatusAccepted = + mConnectivityModule->createReceiver( mTopicConfig->updateJobExecutionAcceptedTopic ); + mReceiverUpdateIotJobStatusRejected = + mConnectivityModule->createReceiver( mTopicConfig->updateJobExecutionRejectedTopic ); + mReceiverCanceledIoTJobs = + mConnectivityModule->createReceiver( mTopicConfig->jobCancellationInProgressTopic ); + } +#endif - mReceiverCollectionSchemeList = - mConnectivityModule->createReceiver( mqttConfig["collectionSchemeListTopic"].asStringRequired() ); + mMqttSender = mConnectivityModule->createSender(); + +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::shared_ptr receiverCommandRequest; + std::shared_ptr receiverRejectedCommandResponse; + std::shared_ptr receiverAcceptedCommandResponse; + receiverCommandRequest = mConnectivityModule->createReceiver( mTopicConfig->commandRequestTopic ); + // The accepted/rejected messages are always sent regardless of whether we are subscribing to the topics or + // not. So even if we don't need to receive them, we subscribe to them just to ensure we don't log any + // error. + receiverAcceptedCommandResponse = + mConnectivityModule->createReceiver( mTopicConfig->commandResponseAcceptedTopic ); + receiverRejectedCommandResponse = + mConnectivityModule->createReceiver( mTopicConfig->commandResponseRejectedTopic ); +#endif - mReceiverDecoderManifest = - mConnectivityModule->createReceiver( mqttConfig["decoderManifestTopic"].asStringRequired() ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + std::shared_ptr receiverLastKnownStateConfig = + mConnectivityModule->createReceiver( mTopicConfig->lastKnownStateConfigTopic ); +#endif - /* - * Over this sender, metrics like performance (resident ram pages, cpu time spent in threads) - * and tracing metrics like internal variables and time spent in specially instrumented functions - * spent are uploaded in json format over mqtt. - */ - auto metricsUploadTopic = mqttConfig["metricsUploadTopic"].asStringOptional().get_value_or( "" ); - if ( !metricsUploadTopic.empty() ) +#ifdef FWE_FEATURE_SOMEIP + if ( !config["staticConfig"].isMember( "deviceShadowOverSomeip" ) ) { - mSenderMetrics = mConnectivityModule->createSender( metricsUploadTopic ); + FWE_LOG_TRACE( "DeviceShadowOverSomeip is disabled as no deviceShadowOverSomeip member in staticConfig" ); } - /* - * Over this sender, log messages that are currently logged to STDOUT are uploaded in json - * format over MQTT. - */ - auto loggingUploadTopic = mqttConfig["loggingUploadTopic"].asStringOptional().get_value_or( "" ); - if ( !loggingUploadTopic.empty() ) + else { - mSenderLogs = mConnectivityModule->createSender( loggingUploadTopic ); + std::shared_ptr receiverDeviceShadow = + mConnectivityModule->createReceiver( mTopicConfig->deviceShadowPrefix + "#" ); + mDeviceShadowOverSomeip = std::make_shared( mMqttSender ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + receiverDeviceShadow->subscribeToDataReceived( std::bind( + &DeviceShadowOverSomeip::onDataReceived, mDeviceShadowOverSomeip.get(), std::placeholders::_1 ) ); + mDeviceShadowOverSomeipInstanceName = + config["staticConfig"]["deviceShadowOverSomeip"]["someipInstance"].asStringOptional().get_value_or( + "commonapi.DeviceShadowOverSomeipInterface" ); + if ( !CommonAPI::Runtime::get()->registerService( + "local", + mDeviceShadowOverSomeipInstanceName, + mDeviceShadowOverSomeip, + config["staticConfig"]["deviceShadowOverSomeip"]["someipApplicationName"].asStringRequired() ) ) + { + FWE_LOG_ERROR( "Failed to register DeviceShadowOverSomeip service" ); + return false; + } } - - // Create an ISender for sending Checkins - mSenderCheckin = mConnectivityModule->createSender( mqttConfig["checkinTopic"].asStringRequired() ); +#endif boost::optional rawDataBufferManagerConfig; auto rawDataBufferJsonConfig = config["staticConfig"]["visionSystemDataCollection"]["rawDataBuffer"]; @@ -456,8 +540,7 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys // loggingUploadMaxWaitBeforeUploadMs // profilerPrefix mRemoteProfiler = std::make_unique( - mSenderMetrics, - mSenderLogs, + mMqttSender, config["staticConfig"]["remoteProfilerDefaultValues"]["metricsUploadIntervalMs"].asU32Required(), config["staticConfig"]["remoteProfilerDefaultValues"]["loggingUploadMaxWaitBeforeUploadMs"] .asU32Required(), @@ -502,22 +585,55 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys payloadConfigCompressed["payloadSizeLimitMinPercent"].asU32Optional().get_value_or( 70 ), payloadConfigCompressed["payloadSizeLimitMaxPercent"].asU32Optional().get_value_or( 90 ), payloadConfigCompressed["transmitThresholdAdaptPercent"].asU32Optional().get_value_or( 10 ) }; - auto telemetryDataSender = std::make_shared( mSenderVehicleData, - dataSenderProtoWriter, - payloadAdaptionConfigUncompressed, - payloadAdaptionConfigCompressed ); + auto telemetryDataSender = std::make_shared( + mMqttSender, dataSenderProtoWriter, payloadAdaptionConfigUncompressed, payloadAdaptionConfigCompressed ); std::unordered_map> dataSenders; dataSenders[SenderDataType::TELEMETRY] = telemetryDataSender; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + if ( mStoreAndForwardEnabled ) + { + const auto persistencyPath = config["staticConfig"]["persistency"]["persistencyPath"].asStringRequired(); + + mStreamManager = std::make_shared( + persistencyPath, + dataSenderProtoWriter, + config["staticConfig"]["publishToCloudParameters"]["maxPublishMessageCount"].asU32Required() ); + auto rateLimiter = std::make_shared( + config["staticConfig"]["storeAndForward"]["forwardMaxTokens"].asU32Optional().get_value_or( + DEFAULT_MAX_TOKENS ), + config["staticConfig"]["storeAndForward"]["forwardTokenRefillsPerSecond"].asU32Optional().get_value_or( + DEFAULT_TOKEN_REFILLS_PER_SECOND ) ); + mStreamForwarder = std::make_shared( + mStreamManager, telemetryDataSender, rateLimiter ); + + // Start the forwarder + if ( !mStreamForwarder->start() ) + { + FWE_LOG_ERROR( "Failed to init and start the Stream Forwarder" ); + return false; + } + } +#endif + // Init and start the Inspection Engine - mCollectionInspectionEngine = std::make_shared(); + auto minFetchTriggerIntervalMs = + config["staticConfig"]["internalParameters"]["minFetchTriggerIntervalMs"].asU32Optional().get_value_or( + MIN_FETCH_TRIGGER_MS ); + mCollectionInspectionEngine = std::make_shared( minFetchTriggerIntervalMs ); mCollectionInspectionWorkerThread = std::make_shared( *mCollectionInspectionEngine ); if ( ( !mCollectionInspectionWorkerThread->init( signalBuffer, mCollectedDataReadyToPublish, config["staticConfig"]["threadIdleTimes"]["inspectionThreadIdleTimeMs"].asU32Required(), - mRawBufferManager ) ) || + mRawBufferManager +#ifdef FWE_FEATURE_STORE_AND_FORWARD + , + mStreamForwarder, + mStreamManager +#endif + ) ) || ( !mCollectionInspectionWorkerThread->start() ) ) { FWE_LOG_ERROR( "Failed to init and start the Inspection Engine" ); @@ -528,6 +644,31 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys mCollectionInspectionWorkerThread.get() ) ); /*************************Inspection Engine bootstrap end***********************************/ + /*************************Store and Forward IoT Jobs bootstrap begin************************/ +#ifdef FWE_FEATURE_STORE_AND_FORWARD + if ( mStoreAndForwardEnabled ) + { + mIoTJobsDataRequestHandler = + std::make_unique( mMqttSender, + mReceiverIotJob, + mReceiverJobDocumentAccepted, + mReceiverJobDocumentRejected, + mReceiverPendingJobsAccepted, + mReceiverPendingJobsRejected, + mReceiverUpdateIotJobStatusAccepted, + mReceiverUpdateIotJobStatusRejected, + mReceiverCanceledIoTJobs, + mStreamManager, + mStreamForwarder, + clientId ); + + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mConnectivityModule->subscribeToConnectionEstablished( + std::bind( &IoTJobsDataRequestHandler::onConnectionEstablished, mIoTJobsDataRequestHandler.get() ) ); + } +#endif + /*************************Store and Forward IoT Jobs bootstrap end**************************/ + /*************************DataSender bootstrap begin*********************************/ #ifdef FWE_FEATURE_VISION_SYSTEM_DATA std::shared_ptr ionWriter; @@ -563,10 +704,43 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys dataSenders[SenderDataType::VISION_SYSTEM] = visionSystemDataSender; } #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + mCommandResponses = std::make_shared( + config["staticConfig"]["internalParameters"]["readyToPublishCommandResponsesBufferSize"] + .asSizeOptional() + .get_value_or( 100 ), + "Command Responses", + TraceAtomicVariable::QUEUE_PENDING_COMMAND_RESPONSES ); + size_t maxConcurrentCommandRequests = + config["staticConfig"]["internalParameters"]["maxConcurrentCommandRequests"].asSizeOptional().get_value_or( + 100 ); + mActuatorCommandManager = std::make_shared( + mCommandResponses, maxConcurrentCommandRequests, mRawBufferManager ); + + dataSenders[SenderDataType::COMMAND_RESPONSE] = std::make_shared( mMqttSender ); +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + mLastKnownStateDataReadyToPublish = std::make_shared( + config["staticConfig"]["internalParameters"]["readyToPublishDataBufferSize"].asSizeRequired(), + "LastKnownState data", + TraceAtomicVariable::QUEUE_LAST_KNOWN_STATE_INSPECTION_TO_SENDER ); + dataSenders[SenderDataType::LAST_KNOWN_STATE] = std::make_shared( + mMqttSender, + config["staticConfig"]["publishToCloudParameters"]["maxPublishLastKnownStateMessageCount"] + .asU32Optional() + .get_value_or( 1000 ) ); +#endif mDataSenderManager = - std::make_shared( std::move( dataSenders ), mSenderVehicleData, mPayloadManager ); - std::vector> dataToSendQueues = { mCollectedDataReadyToPublish }; + std::make_shared( std::move( dataSenders ), mMqttSender, mPayloadManager ); + std::vector> dataToSendQueues = { +#ifdef FWE_FEATURE_REMOTE_COMMANDS + mCommandResponses, +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + mLastKnownStateDataReadyToPublish, +#endif + mCollectedDataReadyToPublish }; mDataSenderManagerWorkerThread = std::make_shared( mConnectivityModule, mDataSenderManager, persistencyUploadRetryIntervalMs, dataToSendQueues ); if ( !mDataSenderManagerWorkerThread->start() ) @@ -578,6 +752,12 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda mCollectedDataReadyToPublish->subscribeToNewDataAvailable( std::bind( &DataSenderManagerWorkerThread::onDataReadyToPublish, mDataSenderManagerWorkerThread.get() ) ); + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mLastKnownStateDataReadyToPublish->subscribeToNewDataAvailable( + std::bind( &DataSenderManagerWorkerThread::onDataReadyToPublish, mDataSenderManagerWorkerThread.get() ) ); +#endif /*************************DataSender bootstrap end*********************************/ /*************************CollectionScheme Ingestion bootstrap begin*********************************/ @@ -585,9 +765,13 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys // CollectionScheme Ingestion module executes in the context for the offboardconnectivity thread. Upcoming // messages are expected to come either on the decoder manifest topic or the collectionScheme topic or both // ( eventually ). - mSchemaPtr = - std::make_shared( mReceiverDecoderManifest, mReceiverCollectionSchemeList, mSenderCheckin ); - + mSchemaPtr = std::make_shared( mReceiverDecoderManifest, mReceiverCollectionSchemeList, mMqttSender ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + if ( receiverLastKnownStateConfig != nullptr ) + { + mLastKnownStateSchema = std::make_unique( receiverLastKnownStateConfig ); + } +#endif /*****************************CollectionScheme Management bootstrap begin*****************************/ // Allow CollectionSchemeManagement to send checkins through the Schema Object Callback @@ -598,7 +782,17 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys // Create and connect the CollectionScheme Manager mCollectionSchemeManagerPtr = std::make_shared( - mPersistDecoderManifestCollectionSchemesAndData, mCANIDTranslator, mCheckinSender, mRawBufferManager ); + mPersistDecoderManifestCollectionSchemesAndData, + mCANIDTranslator, + mCheckinSender, + mRawBufferManager +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , + [this]() -> std::unordered_map> { + return mActuatorCommandManager->getActuatorNames(); + } +#endif + ); // Make sure the CollectionScheme Ingestion can notify the CollectionScheme Manager about the arrival // of new artifacts over the offboardconnectivity receiver. @@ -610,6 +804,16 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys mSchemaPtr->subscribeToDecoderManifestUpdate( std::bind( &CollectionSchemeManager::onDecoderManifestUpdate, mCollectionSchemeManagerPtr.get(), std::placeholders::_1 ) ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + if ( mLastKnownStateSchema != nullptr ) + { + mLastKnownStateSchema->subscribeToLastKnownStateReceived( + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + std::bind( &CollectionSchemeManager::onStateTemplatesChanged, + mCollectionSchemeManagerPtr.get(), + std::placeholders::_1 ) ); + } +#endif // Make sure the CollectionScheme Manager can notify the Inspection Engine about the availability of // a new set of collection CollectionSchemes. @@ -641,7 +845,28 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys std::placeholders::_2 ) ); } #endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + if ( mStoreAndForwardEnabled ) + { + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToCollectionSchemeListChange( + std::bind( &Aws::IoTFleetWise::Store::StreamManager::onChangeCollectionSchemeList, + mStreamManager.get(), + std::placeholders::_1 ) ); + } +#endif + /*************************DataFetchManager bootstrap begin*********************************/ + mDataFetchManager = std::make_shared(); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionInspectionEngine->subscribeToFetchConditionEvaluationUpdate( + std::bind( &DataFetchManager::onFetchRequest, + mDataFetchManager.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToFetchMatrixChange( + std::bind( &DataFetchManager::onChangeFetchMatrix, mDataFetchManager.get(), std::placeholders::_1 ) ); /********************************Data source bootstrap start*******************************/ auto obdOverCANModuleInit = false; @@ -742,6 +967,105 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys std::placeholders::_1, std::placeholders::_2 ) ); } + else if ( interfaceType == NAMED_SIGNAL_INTERFACE_TYPE ) + { + if ( mNamedSignalDataSource != nullptr ) + { + continue; + } + mNamedSignalDataSource = + std::make_shared( interfaceId, signalBufferDistributor ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &NamedSignalDataSource::onChangeOfActiveDictionary, + mNamedSignalDataSource.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + } +#ifdef FWE_FEATURE_SOMEIP + else if ( interfaceType == SOMEIP_COLLECTION_INTERFACE_TYPE ) + { + if ( mSomeipDataSource != nullptr ) + { + continue; + } + // coverity[autosar_cpp14_a20_8_4_violation] Shared pointer interface required for unit testing + auto namedSignalDataSource = + std::make_shared( interfaceId, signalBufferDistributor ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &NamedSignalDataSource::onChangeOfActiveDictionary, + namedSignalDataSource.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + auto someipCollectionInterfaceConfig = networkInterfaceConfig[SOMEIP_COLLECTION_INTERFACE_TYPE]; + mSomeipDataSource = std::make_unique( + createExampleSomeipInterfaceWrapper( + someipCollectionInterfaceConfig["someipApplicationName"].asStringRequired(), + someipCollectionInterfaceConfig["someipInstance"].asStringOptional().get_value_or( + "commonapi.ExampleSomeipInterface" ), + mRawBufferManager, + false ), + std::move( namedSignalDataSource ), + mRawBufferManager, + someipCollectionInterfaceConfig["cyclicUpdatePeriodMs"].asU32Required() ); + if ( !mSomeipDataSource->init() ) + { + FWE_LOG_ERROR( "Failed to initialize SOME/IP data source" ); + return false; + } + } +#endif +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE + else if ( interfaceType == UDS_DTC_INTERFACE ) + { + FWE_LOG_INFO( "UDS Template DTC Interface Type received" ); + mDiagnosticNamedSignalDataSource = + std::make_shared( interfaceId, signalBufferDistributor ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &NamedSignalDataSource::onChangeOfActiveDictionary, + mDiagnosticNamedSignalDataSource.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + std::vector remoteDiagnosticInterfaceConfig; + auto ecuConfiguration = networkInterfaceConfig[UDS_DTC_INTERFACE]; + for ( unsigned j = 0; j < networkInterfaceConfig[UDS_DTC_INTERFACE]["configs"].getArraySizeRequired(); + j++ ) + { + auto ecu = ecuConfiguration["configs"][j]; + auto canInterface = ecu["can"]; + EcuConfig ecuConfig; + ecuConfig.ecuName = static_cast( ecu["name"].asStringRequired() ); + ecuConfig.canBus = static_cast( canInterface["interfaceName"].asStringRequired() ); + try + { + ecuConfig.targetAddress = + static_cast( std::stoi( ecu["targetAddress"].asStringRequired(), nullptr, 0 ) ); + ecuConfig.physicalRequestID = static_cast( + std::stoi( canInterface["physicalRequestID"].asStringRequired(), nullptr, 0 ) ); + ecuConfig.physicalResponseID = static_cast( + std::stoi( canInterface["physicalResponseID"].asStringRequired(), nullptr, 0 ) ); + ecuConfig.functionalAddress = static_cast( + std::stoi( canInterface["functionalAddress"].asStringRequired(), nullptr, 0 ) ); + } + catch ( const std::invalid_argument &err ) + { + FWE_LOG_ERROR( "Could not parse received remote diagnostics interface configuration: " + + std::string( err.what() ) ); + return false; + } + remoteDiagnosticInterfaceConfig.emplace_back( ecuConfig ); + } + mExampleDiagnosticInterface = std::make_shared(); + if ( ( !mExampleDiagnosticInterface->init( remoteDiagnosticInterfaceConfig ) ) || + ( !mExampleDiagnosticInterface->start() ) ) + { + FWE_LOG_ERROR( "Failed to initialize the Template Interface" ); + return false; + } + } +#endif #ifdef FWE_FEATURE_ROS2 else if ( interfaceType == ROS2_INTERFACE_TYPE ) { @@ -760,169 +1084,326 @@ IoTFleetWiseEngine::connect( const Json::Value &jsonConfig, const boost::filesys std::placeholders::_1, std::placeholders::_2 ) ); } +#endif +#ifdef FWE_FEATURE_SOMEIP + else if ( interfaceType == SOMEIP_TO_CAN_BRIDGE_INTERFACE_TYPE ) + { + auto canChannelId = mCANIDTranslator.getChannelNumericID( interfaceId ); + auto someipToCanBridgeConfig = networkInterfaceConfig[SOMEIP_TO_CAN_BRIDGE_INTERFACE_TYPE]; + auto bridge = std::make_unique( + static_cast( someipToCanBridgeConfig["someipServiceId"].asU32FromStringRequired() ), + static_cast( someipToCanBridgeConfig["someipInstanceId"].asU32FromStringRequired() ), + static_cast( someipToCanBridgeConfig["someipEventId"].asU32FromStringRequired() ), + static_cast( someipToCanBridgeConfig["someipEventGroupId"].asU32FromStringRequired() ), + someipToCanBridgeConfig["someipApplicationName"].asStringRequired(), + canChannelId, + *mCANDataConsumer, + []( std::string name ) -> std::shared_ptr { + return vsomeip::runtime::get()->create_application( name ); + }, + []( std::string name ) { + vsomeip::runtime::get()->remove_application( name ); + } ); + if ( !bridge->init() ) + { + FWE_LOG_ERROR( "Failed to initialize SomeipToCanBridge" ); + return false; + } + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &SomeipToCanBridge::onChangeOfActiveDictionary, + bridge.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + mSomeipToCanBridges.push_back( std::move( bridge ) ); + } +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS +#ifdef FWE_FEATURE_SOMEIP + else if ( interfaceType == SOMEIP_COMMAND_INTERFACE_TYPE ) + { + if ( mExampleSomeipCommandDispatcher != nullptr ) + { + continue; + } + auto exampleSomeipInterfaceWrapper = createExampleSomeipInterfaceWrapper( + networkInterfaceConfig[SOMEIP_COMMAND_INTERFACE_TYPE]["someipApplicationName"].asStringRequired(), + networkInterfaceConfig[SOMEIP_COMMAND_INTERFACE_TYPE]["someipInstance"] + .asStringOptional() + .get_value_or( "commonapi.ExampleSomeipInterface" ), + mRawBufferManager, + true ); + mExampleSomeipCommandDispatcher = + std::make_shared( exampleSomeipInterfaceWrapper ); + if ( !mActuatorCommandManager->registerDispatcher( interfaceId, mExampleSomeipCommandDispatcher ) ) + { + return false; + } + } +#endif + else if ( interfaceType == CAN_COMMAND_INTERFACE_TYPE ) + { + if ( mCanCommandDispatcher != nullptr ) + { + continue; + } + mCanCommandDispatcher = std::make_shared( + EXAMPLE_CAN_INTERFACE_SUPPORTED_ACTUATOR_MAP, + networkInterfaceConfig[CAN_COMMAND_INTERFACE_TYPE]["interfaceName"].asStringRequired(), + mRawBufferManager ); + if ( !mActuatorCommandManager->registerDispatcher( interfaceId, mCanCommandDispatcher ) ) + { + return false; + } + } +#endif +#ifdef FWE_FEATURE_AAOS_VHAL + else if ( interfaceType == AAOS_VHAL_INTERFACE_TYPE ) + { + if ( mAaosVhalSource != nullptr ) + { + continue; + } + mAaosVhalSource = std::make_shared( interfaceId, signalBufferDistributor ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &AaosVhalSource::onChangeOfActiveDictionary, + mAaosVhalSource.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + } +#endif +#ifdef FWE_FEATURE_IWAVE_GPS + else if ( interfaceType == IWAVE_GPS_INTERFACE_TYPE ) + { + if ( mIWaveGpsSource != nullptr ) + { + continue; + } + // coverity[autosar_cpp14_a20_8_4_violation] Shared pointer interface required for unit testing + auto namedSignalDataSource = + std::make_shared( interfaceId, signalBufferDistributor ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &NamedSignalDataSource::onChangeOfActiveDictionary, + namedSignalDataSource.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + auto iwaveGpsConfig = networkInterfaceConfig[IWAVE_GPS_INTERFACE_TYPE]; + mIWaveGpsSource = std::make_shared( + namedSignalDataSource, + iwaveGpsConfig[IWaveGpsSource::PATH_TO_NMEA].asStringRequired(), + iwaveGpsConfig[IWaveGpsSource::LATITUDE_SIGNAL_NAME].asStringRequired(), + iwaveGpsConfig[IWaveGpsSource::LONGITUDE_SIGNAL_NAME].asStringRequired(), + iwaveGpsConfig[IWaveGpsSource::POLL_INTERVAL_MS].asU32Required() ); + if ( !mIWaveGpsSource->connect() ) + { + FWE_LOG_ERROR( "IWaveGps initialization failed" ); + return false; + } + } +#endif +#ifdef FWE_FEATURE_EXTERNAL_GPS + else if ( interfaceType == EXTERNAL_GPS_INTERFACE_TYPE ) + { + if ( mExternalGpsSource != nullptr ) + { + continue; + } + // coverity[autosar_cpp14_a20_8_4_violation] Shared pointer interface required for unit testing + auto namedSignalDataSource = + std::make_shared( interfaceId, signalBufferDistributor ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( + std::bind( &NamedSignalDataSource::onChangeOfActiveDictionary, + namedSignalDataSource.get(), + std::placeholders::_1, + std::placeholders::_2 ) ); + auto externalGpsConfig = networkInterfaceConfig[EXTERNAL_GPS_INTERFACE_TYPE]; + mExternalGpsSource = std::make_shared( + namedSignalDataSource, + externalGpsConfig[ExternalGpsSource::LATITUDE_SIGNAL_NAME].asStringRequired(), + externalGpsConfig[ExternalGpsSource::LONGITUDE_SIGNAL_NAME].asStringRequired() ); + } #endif else { FWE_LOG_ERROR( interfaceType + " is not supported" ); } } - - /********************************Data source bootstrap end*******************************/ - - // For asynchronous connect the call needs to be done after all senders and receivers are - // created and all receiver listeners are subscribed. - if ( !mConnectivityModule->connect() ) +#ifdef FWE_FEATURE_UDS_DTC + mDiagnosticDataSource = std::make_shared( mDiagnosticNamedSignalDataSource, + mRawBufferManager +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE + , + mExampleDiagnosticInterface +#endif + ); + if ( !mDiagnosticDataSource->start() ) { + FWE_LOG_ERROR( "Failed to start the Remote Diagnostics Data Source" ); return false; } + mDataFetchManager->registerCustomFetchFunction( "DTC_QUERY", + std::bind( &RemoteDiagnosticDataSource::DTC_QUERY, + mDiagnosticDataSource.get(), + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3 ) ); +#endif - if ( ( mRemoteProfiler != nullptr ) && ( !mRemoteProfiler->start() ) ) - { - FWE_LOG_WARN( "Failed to start the Remote Profiler - No remote profiling available until FWE restart" ); - } - - if ( !mCheckinSender->start() ) - { - FWE_LOG_ERROR( "Failed to start the Checkin thread" ); - return false; - } + /********************************Data source bootstrap end*******************************/ - // Only start the CollectionSchemeManager after all listeners have subscribed, otherwise - // they will not be notified of the initial decoder manifest and collection schemes that are - // read from persistent memory: - if ( !mCollectionSchemeManagerPtr->connect() ) - { - FWE_LOG_ERROR( "Failed to start the CollectionScheme Manager" ); - return false; - } - /****************************CollectionScheme Manager bootstrap end*************************/ + /*******************************Custom function setup begin******************************/ +#ifdef FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES + mCollectionInspectionEngine->registerCustomFunction( "abs", { CustomFunctionMath::absFunc, nullptr, nullptr } ); + mCollectionInspectionEngine->registerCustomFunction( "min", { CustomFunctionMath::minFunc, nullptr, nullptr } ); + mCollectionInspectionEngine->registerCustomFunction( "max", { CustomFunctionMath::maxFunc, nullptr, nullptr } ); + mCollectionInspectionEngine->registerCustomFunction( "pow", { CustomFunctionMath::powFunc, nullptr, nullptr } ); + mCollectionInspectionEngine->registerCustomFunction( "log", { CustomFunctionMath::logFunc, nullptr, nullptr } ); + mCollectionInspectionEngine->registerCustomFunction( "ceil", + { CustomFunctionMath::ceilFunc, nullptr, nullptr } ); + mCollectionInspectionEngine->registerCustomFunction( "floor", + { CustomFunctionMath::floorFunc, nullptr, nullptr } ); + + mCustomFunctionMultiRisingEdgeTrigger = + std::make_unique( mNamedSignalDataSource, mRawBufferManager ); + mCollectionInspectionEngine->registerCustomFunction( + "MULTI_RISING_EDGE_TRIGGER", + { [this]( auto invocationId, const auto &args ) -> CustomFunctionInvokeResult { + return mCustomFunctionMultiRisingEdgeTrigger->invoke( invocationId, args ); + }, + [this]( const auto &collectedSignalIds, auto timestamp, auto &collectedData ) { + mCustomFunctionMultiRisingEdgeTrigger->conditionEnd( collectedSignalIds, timestamp, collectedData ); + }, + [this]( auto invocationId ) { + mCustomFunctionMultiRisingEdgeTrigger->cleanup( invocationId ); + } } ); +#endif + /********************************Custom function setup end*******************************/ -#ifdef FWE_FEATURE_IWAVE_GPS - /********************************IWave GPS Example NMEA reader *********************************/ - mIWaveGpsSource = std::make_shared( signalBufferDistributor ); - std::string pathToNmeaSource; - CANChannelNumericID canChannel{}; - CANRawFrameID canRawFrameId{}; - uint16_t latitudeStartBit{}; - uint16_t longitudeStartBit{}; - if ( config["staticConfig"].isMember( CONFIG_SECTION_IWAVE_GPS ) ) - { - FWE_LOG_TRACE( "Found '" + CONFIG_SECTION_IWAVE_GPS + "' section in config file" ); - const auto iwaveConfig = config["staticConfig"][CONFIG_SECTION_IWAVE_GPS]; - pathToNmeaSource = iwaveConfig[IWaveGpsSource::PATH_TO_NMEA].asStringRequired(); - canChannel = mCANIDTranslator.getChannelNumericID( - iwaveConfig[IWaveGpsSource::CAN_CHANNEL_NUMBER].asStringRequired() ); - canRawFrameId = iwaveConfig[IWaveGpsSource::CAN_RAW_FRAME_ID].asU32FromStringRequired(); - latitudeStartBit = - static_cast( iwaveConfig[IWaveGpsSource::LATITUDE_START_BIT].asU32FromStringRequired() ); - longitudeStartBit = - static_cast( iwaveConfig[IWaveGpsSource::LONGITUDE_START_BIT].asU32FromStringRequired() ); - } - else - { - // If not config available, autodetect the presence of the iWave by passing a blank source: - pathToNmeaSource = ""; - canChannel = mCANIDTranslator.getChannelNumericID( "IWAVE-GPS-CAN" ); - // Default to these values: - canRawFrameId = 1; - latitudeStartBit = 32; - longitudeStartBit = 0; - } - if ( mIWaveGpsSource->init( pathToNmeaSource, canChannel, canRawFrameId, latitudeStartBit, longitudeStartBit ) ) +#ifdef FWE_FEATURE_REMOTE_COMMANDS + /********************************Remote commands bootstrap begin***************************/ + if ( receiverCommandRequest ) { - if ( !mIWaveGpsSource->connect() ) + mCommandSchema = + std::make_unique( receiverCommandRequest, mCommandResponses, mRawBufferManager ); + if ( receiverRejectedCommandResponse != nullptr ) { - FWE_LOG_ERROR( "IWaveGps initialization failed" ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + receiverRejectedCommandResponse->subscribeToDataReceived( + std::bind( &CommandSchema::onRejectedCommandResponseReceived, std::placeholders::_1 ) ); + } + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCommandResponses->subscribeToNewDataAvailable( std::bind( + &DataSenderManagerWorkerThread::onDataReadyToPublish, mDataSenderManagerWorkerThread.get() ) ); + if ( !mActuatorCommandManager->start() ) + { + FWE_LOG_ERROR( "Failed to init and start the Command Manager" ); return false; } // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda - mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( - std::bind( &IWaveGpsSource::onChangeOfActiveDictionary, - mIWaveGpsSource.get(), + mCollectionSchemeManagerPtr->subscribeToCustomSignalDecoderFormatMapChange( + std::bind( &ActuatorCommandManager::onChangeOfCustomSignalDecoderFormatMap, + mActuatorCommandManager.get(), std::placeholders::_1, std::placeholders::_2 ) ); - mIWaveGpsSource->start(); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + mCommandSchema->subscribeToActuatorCommandRequestReceived( + std::bind( &ActuatorCommandManager::onReceivingCommandRequest, + mActuatorCommandManager.get(), + std::placeholders::_1 ) ); } - /********************************IWave GPS Example NMEA reader end******************************/ + + /********************************Remote commands bootstrap end*****************************/ #endif -#ifdef FWE_FEATURE_EXTERNAL_GPS - /********************************External GPS Example NMEA reader *********************************/ - mExternalGpsSource = std::make_shared( signalBufferDistributor ); - bool externalGpsInitSuccessful = false; - if ( config["staticConfig"].isMember( CONFIG_SECTION_EXTERNAL_GPS ) ) +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + /********************************Last known state bootstrap begin**************************/ + if ( receiverCommandRequest && receiverLastKnownStateConfig ) { - FWE_LOG_TRACE( "Found '" + CONFIG_SECTION_EXTERNAL_GPS + "' section in config file" ); - auto externalGpsConfig = config["staticConfig"][CONFIG_SECTION_EXTERNAL_GPS]; - externalGpsInitSuccessful = mExternalGpsSource->init( - mCANIDTranslator.getChannelNumericID( - externalGpsConfig[ExternalGpsSource::CAN_CHANNEL_NUMBER].asStringRequired() ), - externalGpsConfig[ExternalGpsSource::CAN_RAW_FRAME_ID].asU32FromStringRequired(), - static_cast( - externalGpsConfig[ExternalGpsSource::LATITUDE_START_BIT].asU32FromStringRequired() ), - static_cast( - externalGpsConfig[ExternalGpsSource::LONGITUDE_START_BIT].asU32FromStringRequired() ) ); + auto lastKnownStateInspector = std::make_unique( + mCommandResponses, mPersistDecoderManifestCollectionSchemesAndData ); + auto lastKnownStateSignalBuffer = + std::make_shared( signalBufferSize, + "LKS Signal Buffer", + TraceAtomicVariable::QUEUE_CONSUMER_TO_LAST_KNOWN_STATE_INSPECTION, + // Notify listeners when 10% of the buffer is full so that we don't + // let it grow too much. + signalBufferSize / 10 ); + + signalBufferDistributor->registerQueue( lastKnownStateSignalBuffer ); + mLastKnownStateWorkerThread = std::make_shared( + lastKnownStateSignalBuffer, + mLastKnownStateDataReadyToPublish, + std::move( lastKnownStateInspector ), + config["staticConfig"]["threadIdleTimes"]["lastKnownStateThreadIdleTimeMs"] + .asU32Optional() + .get_value_or( 0 ) ); + mCollectionSchemeManagerPtr->subscribeToStateTemplatesChange( + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + std::bind( &LastKnownStateWorkerThread::onStateTemplatesChanged, + mLastKnownStateWorkerThread.get(), + std::placeholders::_1 ) ); + mCommandSchema->subscribeToLastKnownStateCommandRequestReceived( + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + std::bind( &LastKnownStateWorkerThread::onNewCommandReceived, + mLastKnownStateWorkerThread.get(), + std::placeholders::_1 ) ); + // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda + lastKnownStateSignalBuffer->subscribeToNewDataAvailable( + std::bind( &LastKnownStateWorkerThread::onNewDataAvailable, mLastKnownStateWorkerThread.get() ) ); + + if ( !mLastKnownStateWorkerThread->start() ) + { + FWE_LOG_ERROR( "Failed to init and start the Last Known State Inspection Engine" ); + return false; + } } - else + else if ( receiverLastKnownStateConfig == nullptr ) { - // If not config available default to this values - externalGpsInitSuccessful = - mExternalGpsSource->init( mCANIDTranslator.getChannelNumericID( "EXTERNAL-GPS-CAN" ), 1, 32, 0 ); + FWE_LOG_INFO( "Disabling LastKnownState because LastKnownState topics are not configured" ); } - if ( externalGpsInitSuccessful ) + else if ( receiverCommandRequest == nullptr ) { - // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda - mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( - std::bind( &ExternalGpsSource::onChangeOfActiveDictionary, - mExternalGpsSource.get(), - std::placeholders::_1, - std::placeholders::_2 ) ); - mExternalGpsSource->start(); + FWE_LOG_WARN( "Disabling LastKnownState because command topics are not configured" ); } - else + /********************************Last known state bootstrap end****************************/ +#endif + + // For asynchronous connect the call needs to be done after all senders and receivers are + // created and all receiver listeners are subscribed. + if ( !mConnectivityModule->connect() ) { - FWE_LOG_ERROR( "ExternalGpsSource initialization failed" ); return false; } - /********************************External GPS Example NMEA reader end******************************/ -#endif -#ifdef FWE_FEATURE_AAOS_VHAL - /********************************AAOS VHAL Example reader *********************************/ - mAaosVhalSource = std::make_shared( signalBufferDistributor ); - bool aaosVhalInitSuccessful = false; - if ( config["staticConfig"].isMember( CONFIG_SECTION_AAOS_VHAL ) ) + if ( ( mRemoteProfiler != nullptr ) && ( !mRemoteProfiler->start() ) ) { - FWE_LOG_TRACE( "Found '" + CONFIG_SECTION_AAOS_VHAL + "' section in config file" ); - auto aaosConfig = config["staticConfig"][CONFIG_SECTION_AAOS_VHAL]; - aaosVhalInitSuccessful = - mAaosVhalSource->init( mCANIDTranslator.getChannelNumericID( - aaosConfig[AaosVhalSource::CAN_CHANNEL_NUMBER].asStringRequired() ), - aaosConfig[AaosVhalSource::CAN_RAW_FRAME_ID].asU32FromStringRequired() ); + FWE_LOG_WARN( "Failed to start the Remote Profiler - No remote profiling available until FWE restart" ); } - else + + if ( !mCheckinSender->start() ) { - // If not config available default to this values - aaosVhalInitSuccessful = - mAaosVhalSource->init( mCANIDTranslator.getChannelNumericID( "AAOS-VHAL-CAN" ), 1 ); + FWE_LOG_ERROR( "Failed to start the Checkin thread" ); + return false; } - if ( aaosVhalInitSuccessful ) + + // Only start the CollectionSchemeManager after all listeners have subscribed, otherwise + // they will not be notified of the initial decoder manifest and collection schemes that are + // read from persistent memory: + if ( !mCollectionSchemeManagerPtr->connect() ) { - // coverity[autosar_cpp14_a18_9_1_violation] std::bind is easier to maintain than extra lambda - mCollectionSchemeManagerPtr->subscribeToActiveDecoderDictionaryChange( - std::bind( &AaosVhalSource::onChangeOfActiveDictionary, - mAaosVhalSource.get(), - std::placeholders::_1, - std::placeholders::_2 ) ); - mAaosVhalSource->start(); + FWE_LOG_ERROR( "Failed to start the CollectionScheme Manager" ); + return false; } - else + /****************************CollectionScheme Manager bootstrap end*************************/ + + if ( !mDataFetchManager->start() ) { - FWE_LOG_ERROR( "AaosVhalExample initialization failed" ); + FWE_LOG_ERROR( "Failed to start the DataFetchManager" ); return false; } - /********************************AAOS VHAL Example reader end******************************/ -#endif mPrintMetricsCyclicPeriodMs = config["staticConfig"]["internalParameters"]["metricsCyclicPrintIntervalMs"].asU32Optional().get_value_or( @@ -943,22 +1424,13 @@ bool IoTFleetWiseEngine::disconnect() { #ifdef FWE_FEATURE_AAOS_VHAL - if ( mAaosVhalSource ) - { - mAaosVhalSource->stop(); - } + mAaosVhalSource.reset(); #endif #ifdef FWE_FEATURE_EXTERNAL_GPS - if ( mExternalGpsSource ) - { - mExternalGpsSource->stop(); - } + mExternalGpsSource.reset(); #endif #ifdef FWE_FEATURE_IWAVE_GPS - if ( mIWaveGpsSource ) - { - mIWaveGpsSource->stop(); - } + mIWaveGpsSource.reset(); #endif #ifdef FWE_FEATURE_ROS2 if ( mROS2DataSource ) @@ -966,6 +1438,26 @@ IoTFleetWiseEngine::disconnect() mROS2DataSource->disconnect(); } #endif +#ifdef FWE_FEATURE_SOMEIP + for ( auto &bridge : mSomeipToCanBridges ) + { + bridge->disconnect(); + } + mSomeipDataSource.reset(); + mExampleSomeipCommandDispatcher.reset(); + if ( mDeviceShadowOverSomeip ) + { + if ( !CommonAPI::Runtime::get()->unregisterService( + "local", + v1::commonapi::DeviceShadowOverSomeipInterface::getInterface(), + mDeviceShadowOverSomeipInstanceName ) ) + { + FWE_LOG_ERROR( "Failed to unregister DeviceShadowOverSomeip service" ); + return false; + } + } + mDeviceShadowOverSomeip.reset(); +#endif if ( mOBDOverCANModule ) { @@ -1016,6 +1508,60 @@ IoTFleetWiseEngine::disconnect() return false; } +#ifdef FWE_FEATURE_REMOTE_COMMANDS + if ( mActuatorCommandManager != nullptr ) + { + if ( !mActuatorCommandManager->stop() ) + { + FWE_LOG_ERROR( "Could not stop the ActuatorCommandManager" ); + return false; + } + } +#endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + // iot jobs depends on stream forwarder, + // so only stop forwarder after connectivity module is disconnected + if ( mStreamForwarder ) + { + if ( !mStreamForwarder->stop() ) + { + FWE_LOG_ERROR( "Could not stop the SteamForwarder" ); + return false; + } + } +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + if ( ( mLastKnownStateWorkerThread != nullptr ) && ( !mLastKnownStateWorkerThread->stop() ) ) + { + FWE_LOG_ERROR( "Could not stop the Last Known State Inspection Thread" ); + return false; + } +#endif + if ( !mDataFetchManager->stop() ) + { + FWE_LOG_ERROR( "Could not stop the DataFetchManager" ); + return false; + } +#ifdef FWE_FEATURE_UDS_DTC + if ( mDiagnosticDataSource != nullptr ) + { + if ( !mDiagnosticDataSource->stop() ) + { + FWE_LOG_ERROR( "Could not stop DiagnosticDataSource" ); + return false; + } + } +#endif +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE + if ( mExampleDiagnosticInterface != nullptr ) + { + if ( !mExampleDiagnosticInterface->stop() ) + { + FWE_LOG_ERROR( "Could not stop DiagnosticInterface" ); + return false; + } + } +#endif if ( !mDataSenderManagerWorkerThread->stop() ) { FWE_LOG_ERROR( "Could not stop the DataSenderManager" ); @@ -1169,6 +1715,29 @@ IoTFleetWiseEngine::ingestExternalCANMessage( const InterfaceID &interfaceId, mExternalCANDataSource->ingestMessage( canChannelId, timestamp, messageId, data ); } +void +IoTFleetWiseEngine::ingestSignalValueByName( Timestamp timestamp, + const std::string &name, + const DecodedSignalValue &value ) +{ + if ( mNamedSignalDataSource == nullptr ) + { + return; + } + mNamedSignalDataSource->ingestSignalValue( timestamp, name, value ); +} + +void +IoTFleetWiseEngine::ingestMultipleSignalValuesByName( + Timestamp timestamp, const std::vector> &values ) +{ + if ( mNamedSignalDataSource == nullptr ) + { + return; + } + mNamedSignalDataSource->ingestMultipleSignalValues( timestamp, values ); +} + #ifdef FWE_FEATURE_EXTERNAL_GPS void IoTFleetWiseEngine::setExternalGpsLocation( double latitude, double longitude ) @@ -1207,7 +1776,7 @@ IoTFleetWiseEngine::setVehicleProperty( uint32_t signalId, const DecodedSignalVa std::string IoTFleetWiseEngine::getStatusSummary() { - if ( mConnectivityModule == nullptr || mCollectionSchemeManagerPtr == nullptr || mSenderVehicleData == nullptr || + if ( mConnectivityModule == nullptr || mCollectionSchemeManagerPtr == nullptr || mMqttSender == nullptr || mOBDOverCANModule == nullptr ) { return ""; @@ -1231,7 +1800,7 @@ IoTFleetWiseEngine::getStatusSummary() } status += "\n"; - status += "Payloads sent: " + std::to_string( mSenderVehicleData->getPayloadCountSent() ) + "\n\n"; + status += "Payloads sent: " + std::to_string( mMqttSender->getPayloadCountSent() ) + "\n\n"; return status; } diff --git a/src/IoTFleetWiseEngine.h b/src/IoTFleetWiseEngine.h index 038564b5..1700096d 100644 --- a/src/IoTFleetWiseEngine.h +++ b/src/IoTFleetWiseEngine.h @@ -13,6 +13,7 @@ #include "CollectionInspectionEngine.h" #include "CollectionInspectionWorkerThread.h" #include "CollectionSchemeManager.h" +#include "DataFetchManager.h" #include "DataSenderManager.h" #include "DataSenderManagerWorkerThread.h" #include "DataSenderTypes.h" @@ -20,6 +21,7 @@ #include "IConnectivityModule.h" #include "IReceiver.h" #include "ISender.h" +#include "NamedSignalDataSource.h" #include "OBDDataTypes.h" #include "OBDOverCANModule.h" #include "PayloadManager.h" @@ -31,6 +33,7 @@ #include "Thread.h" #include "TimeTypes.h" #include "Timer.h" +#include "TopicConfig.h" #include #include #include @@ -38,8 +41,15 @@ #include #include #include +#include #include +#ifdef FWE_FEATURE_UDS_DTC +#include "RemoteDiagnosticDataSource.h" +#endif +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE +#include "ExampleUDSInterface.h" +#endif #ifdef FWE_FEATURE_AAOS_VHAL #include "AaosVhalSource.h" #include @@ -67,6 +77,29 @@ #ifdef FWE_FEATURE_ROS2 #include "ROS2DataSource.h" #endif +#ifdef FWE_FEATURE_SOMEIP +#include "DeviceShadowOverSomeip.h" +#include "SomeipCommandDispatcher.h" +#include "SomeipDataSource.h" +#include "SomeipToCanBridge.h" +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS +#include "ActuatorCommandManager.h" +#include "CanCommandDispatcher.h" +#include "CommandSchema.h" +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateSchema.h" +#include "LastKnownStateWorkerThread.h" +#endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include "IoTJobsDataRequestHandler.h" +#include "StreamForwarder.h" +#include "StreamManager.h" +#endif +#ifdef FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES +#include "CustomFunctionMultiRisingEdgeTrigger.h" +#endif namespace Aws { @@ -120,6 +153,18 @@ class IoTFleetWiseEngine uint32_t messageId, const std::vector &data ); + /** @brief Ingest a signal value by name + * @param timestamp Timestamp of signal value in milliseconds since epoch, or zero if unknown. + * @param name Signal name + * @param value Signal value */ + void ingestSignalValueByName( Timestamp timestamp, const std::string &name, const DecodedSignalValue &value ); + + /** @brief Ingest multiple signal values by name + * @param timestamp Timestamp of signal values in milliseconds since epoch, or zero if unknown. + * @param values Signal values */ + void ingestMultipleSignalValuesByName( Timestamp timestamp, + const std::vector> &values ); + #ifdef FWE_FEATURE_EXTERNAL_GPS /** * @brief Sets the location for the ExternalGpsSource @@ -185,13 +230,22 @@ class IoTFleetWiseEngine std::vector> mCANDataSources; std::unique_ptr mExternalCANDataSource; std::unique_ptr mCANDataConsumer; + std::shared_ptr mNamedSignalDataSource; + std::unique_ptr mTopicConfig; std::shared_ptr mConnectivityModule; - std::shared_ptr mSenderVehicleData; - std::shared_ptr mSenderCheckin; + std::shared_ptr mMqttSender; std::shared_ptr mReceiverCollectionSchemeList; std::shared_ptr mReceiverDecoderManifest; std::shared_ptr mPayloadManager; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + bool mStoreAndForwardEnabled = true; + std::shared_ptr mStreamManager; + std::shared_ptr mStreamForwarder; +#endif +#ifdef FWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES + std::unique_ptr mCustomFunctionMultiRisingEdgeTrigger; +#endif std::shared_ptr mSchemaPtr; std::shared_ptr mCheckinSender; @@ -204,9 +258,38 @@ class IoTFleetWiseEngine std::shared_ptr mDataSenderManager; std::shared_ptr mDataSenderManagerWorkerThread; + std::shared_ptr mDataFetchManager; + std::unique_ptr mRemoteProfiler; - std::shared_ptr mSenderMetrics; - std::shared_ptr mSenderLogs; +#ifdef FWE_FEATURE_UDS_DTC_EXAMPLE + std::shared_ptr mExampleDiagnosticInterface; +#endif +#ifdef FWE_FEATURE_UDS_DTC + std::shared_ptr mDiagnosticDataSource; + std::shared_ptr mDiagnosticNamedSignalDataSource; +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::shared_ptr mCommandResponses; + std::shared_ptr mActuatorCommandManager; + std::unique_ptr mCommandSchema; + std::shared_ptr mCanCommandDispatcher; +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + std::shared_ptr mLastKnownStateDataReadyToPublish; + std::unique_ptr mLastKnownStateSchema; + std::shared_ptr mLastKnownStateWorkerThread; +#endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + std::shared_ptr mReceiverIotJob; + std::shared_ptr mReceiverJobDocumentAccepted; + std::shared_ptr mReceiverJobDocumentRejected; + std::shared_ptr mReceiverPendingJobsAccepted; + std::shared_ptr mReceiverPendingJobsRejected; + std::shared_ptr mReceiverUpdateIotJobStatusAccepted; + std::shared_ptr mReceiverUpdateIotJobStatusRejected; + std::shared_ptr mReceiverCanceledIoTJobs; + std::unique_ptr mIoTJobsDataRequestHandler; +#endif #ifdef FWE_FEATURE_S3 std::shared_ptr mAwsCredentialsProvider; std::shared_ptr mTransferManagerExecutor; @@ -228,6 +311,15 @@ class IoTFleetWiseEngine #ifdef FWE_FEATURE_ROS2 std::shared_ptr mROS2DataSource; #endif +#ifdef FWE_FEATURE_SOMEIP + std::unique_ptr mSomeipDataSource; + std::vector> mSomeipToCanBridges; + // Create one for each SOME/IP interface + std::shared_ptr mExampleSomeipCommandDispatcher; + + std::shared_ptr mDeviceShadowOverSomeip; + std::string mDeviceShadowOverSomeipInstanceName; +#endif }; } // namespace IoTFleetWise diff --git a/src/IoTJobsDataRequestHandler.cpp b/src/IoTJobsDataRequestHandler.cpp new file mode 100644 index 00000000..f8f618ae --- /dev/null +++ b/src/IoTJobsDataRequestHandler.cpp @@ -0,0 +1,878 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "IoTJobsDataRequestHandler.h" +#include "IConnectionTypes.h" +#include "LoggingModule.h" +#include "TopicConfig.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +IoTJobsDataRequestHandler::IoTJobsDataRequestHandler( + std::shared_ptr mqttSender, + std::shared_ptr receiveIotJobs, + std::shared_ptr receiveAcceptedJobDocuments, + std::shared_ptr receiveRejectedJobDocuments, + std::shared_ptr receiveAcceptedPendingJobs, + std::shared_ptr receiveRejectedPendingJobs, + std::shared_ptr receiveAcceptedUpdateJobs, + std::shared_ptr receiveRejectedUpdateJobs, + std::shared_ptr receiveCanceledJobs, + std::shared_ptr streamManager, + std::shared_ptr streamForwarder, + std::string thingName ) + : mThingName( std::move( thingName ) ) + , mMqttSender( std::move( mqttSender ) ) + , mStreamManager( std::move( streamManager ) ) + , mStreamForwarder( std::move( streamForwarder ) ) +{ + mStreamForwarder->registerJobCompletionCallback( [this]( const Store::CampaignID &campaignId ) { + onJobUploadCompleted( campaignId ); + } ); + + // register the listeners + receiveIotJobs->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onIotJobReceived( receivedMessage.buf, receivedMessage.size ); + } ); + receiveAcceptedJobDocuments->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onIotJobDocumentAccepted( receivedMessage.buf, receivedMessage.size ); + } ); + receiveRejectedJobDocuments->subscribeToDataReceived( []( const ReceivedConnectivityMessage &receivedMessage ) { + onIotJobDocumentRejected( receivedMessage.buf, receivedMessage.size ); + } ); + receiveAcceptedPendingJobs->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onPendingJobsAccepted( receivedMessage.buf, receivedMessage.size ); + } ); + receiveRejectedPendingJobs->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onPendingJobsRejected( receivedMessage.buf, receivedMessage.size ); + } ); + receiveAcceptedUpdateJobs->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onUpdateJobStatusAccepted( receivedMessage.buf, receivedMessage.size ); + } ); + receiveRejectedUpdateJobs->subscribeToDataReceived( []( const ReceivedConnectivityMessage &receivedMessage ) { + onUpdateJobStatusRejected( receivedMessage.buf, receivedMessage.size ); + } ); + receiveCanceledJobs->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onCanceledJobReceived( receivedMessage.buf, receivedMessage.size ); + } ); +} + +void +IoTJobsDataRequestHandler::onConnectionEstablished() +{ + + // wait until connected to check to see if any jobs were QUEUED or IN_PROGRESS + sendGetPendingExecutions(); +} + +void +IoTJobsDataRequestHandler::onPendingJobsAccepted( const uint8_t *buf, size_t size ) +{ + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + Json::Reader reader; + Json::Value pendingJobs; + + if ( reader.parse( std::string( buf, buf + size ), pendingJobs ) ) + { + auto inProgressJobs = pendingJobs["inProgressJobs"]; + + if ( !inProgressJobs.empty() ) + { + // Jobs were IN_PROGRESS when FWE restarted. Need to start data upload again + if ( !inProgressJobs.isArray() ) + { + FWE_LOG_ERROR( "Expected inProgressJobs Jobs to be a list in GetPendingJobExecutions response" ); + return; + } + + for ( Json::Value::iterator it = inProgressJobs.begin(); it != inProgressJobs.end(); it++ ) + { + Json::Value job = *it; + + if ( job["jobId"].empty() ) + { + FWE_LOG_ERROR( "No jobId in the pending job execution summary" ); + continue; + } + + if ( !job["jobId"].isString() ) + { + FWE_LOG_ERROR( "The jobId received is not a string" ); + continue; + } + + std::string jobId = job["jobId"].asString(); + + if ( jobId.empty() ) + { + // Note: Need this empty check since Json::Value [] operator will + // "Access an object value by name, create a null member if it does not exist." + FWE_LOG_ERROR( "The jobId received is an empty string" ); + continue; + } + + bool isJobUploading = false; + { + std::lock_guard lock( mCampaignMutex ); + isJobUploading = mJobToStatus.find( jobId ) != mJobToStatus.end() && + ( mJobToStatus[jobId] == jobStatus::IN_PROGRESS ); + } + if ( isJobUploading ) + { + // Since this jobId is already uploading data, there is no need to request the job doc + FWE_LOG_TRACE( jobId + " is already uploading data" ); + continue; + } + + Json::StreamWriterBuilder builder; + Json::Value requestJobDoc; + requestJobDoc["jobId"] = jobId; + requestJobDoc["thingName"] = mThingName; + requestJobDoc["includeJobDocument"] = true; + + const std::string data = Json::writeString( builder, requestJobDoc ); + // Publishing is async and non-blocking + std::string topic = mMqttSender->getTopicConfig().getJobExecutionTopic( jobId ); + mMqttSender->sendBuffer( topic, + reinterpret_cast( data.c_str() ), + data.size(), + [topic]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully sent message on topic " + topic ); + } + else + { + FWE_LOG_ERROR( "Send error " + + std::to_string( static_cast( result ) ) ); + } + } ); + } + } + + auto queuedJobs = pendingJobs["queuedJobs"]; + + if ( !queuedJobs.empty() ) + { + // Jobs were QUEUED when FWE reconnected or restarted + // The onIotJobReceived handler won't receive a message unless a "job is added to or removed from the list + // of pending job executions for a thing or the first job execution in the list changes" + // Thus, we need to process the queued jobs + + if ( !queuedJobs.isArray() ) + { + FWE_LOG_ERROR( "Expected queuedJobs Jobs to be a list in GetPendingJobExecutions response" ); + return; + } + + for ( Json::Value::iterator it = queuedJobs.begin(); it != queuedJobs.end(); it++ ) + { + Json::Value job = *it; + + if ( job["jobId"].empty() ) + { + FWE_LOG_ERROR( "No jobId in the pending job execution summary" ); + continue; + } + + if ( !job["jobId"].isString() ) + { + FWE_LOG_ERROR( "The jobId received is not a string" ); + continue; + } + + std::string jobId = job["jobId"].asString(); + + if ( jobId.empty() ) + { + // This should never happen since IoT Jobs should never send us "" as a jobId + FWE_LOG_ERROR( "The jobId received is an empty string" ); + continue; + } + + Json::StreamWriterBuilder builder; + Json::Value requestJobDoc; + requestJobDoc["jobId"] = jobId; + requestJobDoc["thingName"] = mThingName; + requestJobDoc["includeJobDocument"] = true; + + const std::string data = Json::writeString( builder, requestJobDoc ); + // Publishing is async and non-blocking + std::string topic = mMqttSender->getTopicConfig().getJobExecutionTopic( jobId ); + mMqttSender->sendBuffer( topic, + reinterpret_cast( data.c_str() ), + data.size(), + [topic]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully sent message on topic " + topic ); + } + else + { + FWE_LOG_ERROR( "Send error " + + std::to_string( static_cast( result ) ) ); + } + } ); + } + } + } +} + +void +IoTJobsDataRequestHandler::onPendingJobsRejected( const uint8_t *buf, size_t size ) +{ + FWE_LOG_ERROR( "GetPendingJobExecutions request was rejected" ); + + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + FWE_LOG_INFO( "Retrying GetPendingJobExecutions request" ); + sendGetPendingExecutions(); +} + +void +IoTJobsDataRequestHandler::onIotJobReceived( const uint8_t *buf, size_t size ) +{ + // Check for an empty input data + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + Json::Reader reader; + Json::Value newJobs; + if ( reader.parse( std::string( buf, buf + size ), newJobs ) ) + { + if ( newJobs["jobs"].empty() ) + { + FWE_LOG_TRACE( "No Jobs received in Store and Forward data upload request" ); + return; + } + + auto listJobs = newJobs["jobs"]["QUEUED"]; + + if ( listJobs.empty() ) + { + FWE_LOG_ERROR( "No QUEUED Jobs received in Store and Forward data upload request" ); + return; + } + + FWE_LOG_INFO( "Received Store and Forward data upload request" ); + + if ( !listJobs.isArray() ) + { + FWE_LOG_ERROR( "Expected QUEUED Jobs to be a list in the Store and Forward data upload request" ); + return; + } + + for ( Json::Value::iterator it = listJobs.begin(); it != listJobs.end(); it++ ) + { + Json::Value job = *it; + + if ( job["jobId"].empty() ) + { + FWE_LOG_ERROR( "No jobId received in Store and Forward data upload request" ); + continue; + } + + if ( !job["jobId"].isString() ) + { + FWE_LOG_ERROR( "The jobId received is not a string" ); + continue; + } + + std::string jobId = job["jobId"].asString(); + + if ( jobId.empty() ) + { + // This should never happen since IoT Jobs should never send us "" as a jobId + FWE_LOG_ERROR( "The jobId received is an empty string" ); + continue; + } + + Json::StreamWriterBuilder builder; + Json::Value requestJobDoc; + requestJobDoc["jobId"] = jobId; + requestJobDoc["thingName"] = mThingName; + requestJobDoc["includeJobDocument"] = true; + + const std::string data = Json::writeString( builder, requestJobDoc ); + // Publishing is async and non-blocking + std::string topic = mMqttSender->getTopicConfig().getJobExecutionTopic( jobId ); + mMqttSender->sendBuffer( topic, + reinterpret_cast( data.c_str() ), + data.size(), + [topic]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully sent message on topic " + topic ); + } + else + { + FWE_LOG_ERROR( "Send error " + + std::to_string( static_cast( result ) ) ); + } + } ); + } + } + else + { + FWE_LOG_ERROR( "Received Store and Forward data upload request, but unable to parse received Job(s)" ); + } +} + +void +IoTJobsDataRequestHandler::onIotJobDocumentAccepted( const uint8_t *buf, size_t size ) +{ + // Check for an empty input data + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + FWE_LOG_INFO( "Received Store and Forward data upload request document" ); + + Json::Reader reader; + Json::Value jobDocument; + if ( reader.parse( std::string( buf, buf + size ), jobDocument ) ) + { + std::string campaignArn; + uint64_t endTime = 0; + std::string jobId; + + auto execution = jobDocument["execution"]; + + if ( execution.empty() ) + { + FWE_LOG_ERROR( "Null or empty execution field in received upload request document" ); + return; + } + + if ( execution["jobId"].empty() ) + { + FWE_LOG_ERROR( "Null or empty jobId in received upload request document" ); + return; + } + + if ( !execution["jobId"].isString() ) + { + FWE_LOG_ERROR( "The jobId in the upload request document is not a string" ); + return; + } + + jobId = execution["jobId"].asString(); + + if ( jobId.empty() ) + { + // This should never happen since IoT Jobs should never send us "" as a jobId + FWE_LOG_ERROR( "The jobId received in the upload request document is an empty string" ); + return; + } + + if ( execution["jobDocument"].empty() ) + { + FWE_LOG_ERROR( "Null or empty jobDocument field in received upload request document" ); + return; + } + + auto jobDoc = execution["jobDocument"]; + + if ( jobDoc["parameters"].empty() ) + { + FWE_LOG_ERROR( "Null or empty parameters field in received job document" ); + return; + } + + if ( jobDoc["parameters"]["campaignArn"].empty() || ( !jobDoc["parameters"]["campaignArn"].isString() ) ) + { + FWE_LOG_ERROR( "Invalid campaignArn value in received job document" ); + return; + } + + campaignArn = jobDoc["parameters"]["campaignArn"].asString(); + + if ( ( !jobDoc["parameters"]["endTime"].empty() ) && jobDoc["parameters"]["endTime"].isString() ) + { + // Specifying an endTime in the Job Document is optional + endTime = convertEndTimeToMS( jobDocument["execution"]["jobDocument"]["parameters"]["endTime"].asString() ); + } + + Json::StreamWriterBuilder builder; + Json::Value updateJobExecution; + updateJobExecution["status"] = "IN_PROGRESS"; + // setting clientToken as the jobId so that we can access the jobId in the rejected topic + updateJobExecution["clientToken"] = jobId; + + if ( execution["status"] == "IN_PROGRESS" ) + { + // Job is already IN_PROGRESS meaning data upload was once happening + // Restart data upload if campaignArn is still valid + bool isJobUploading = false; + { + std::lock_guard lock( mCampaignMutex ); + isJobUploading = mJobToStatus.find( jobId ) != mJobToStatus.end() && + ( mJobToStatus[jobId] == jobStatus::IN_PROGRESS ); + } + if ( isJobUploading ) + { + FWE_LOG_TRACE( jobId + " is already uploading data" ) + return; + } + + if ( !mStreamManager->hasCampaign( campaignArn ) ) + { + FWE_LOG_ERROR( "CampaignArn value in the received job document does not match the arn of a " + "store-and-forward campaign" ); + updateJobExecution["status"] = "REJECTED"; + + { + std::lock_guard lock( mCampaignMutex ); + mJobToStatus[jobId] = jobStatus::REJECTED; + } + + // Need to update job since something went wrong and we no longer have the specified s&f campaign + const std::string data = Json::writeString( builder, updateJobExecution ); + // Publishing is async and non-blocking + std::string topic = mMqttSender->getTopicConfig().updateJobExecutionTopic( jobId ); + mMqttSender->sendBuffer( topic, + reinterpret_cast( data.c_str() ), + data.size(), + [topic]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully sent message on topic " + topic ); + } + else + { + FWE_LOG_ERROR( "Send error " + + std::to_string( static_cast( result ) ) ); + } + } ); + } + else + { + mStreamForwarder->beginJobForward( campaignArn, endTime ); + std::lock_guard lock( mCampaignMutex ); + mJobToStatus[jobId] = jobStatus::IN_PROGRESS; + mJobToCampaignId[jobId] = campaignArn; + } + } + else + { + // Job is NOT already IN_PROGRESS + if ( !mStreamManager->hasCampaign( campaignArn ) ) + { + FWE_LOG_ERROR( "CampaignArn value in the received job document does not match the arn of a " + "store-and-forward campaign" ); + updateJobExecution["status"] = "REJECTED"; + std::lock_guard lock( mCampaignMutex ); + mJobToStatus[jobId] = jobStatus::REJECTED; + } + else + { + mStreamForwarder->beginJobForward( campaignArn, endTime ); + std::lock_guard lock( mCampaignMutex ); + mJobToStatus[jobId] = jobStatus::IN_PROGRESS; + mJobToCampaignId[jobId] = campaignArn; + } + + const std::string data = Json::writeString( builder, updateJobExecution ); + // Publishing is async and non-blocking + std::string topic = mMqttSender->getTopicConfig().updateJobExecutionTopic( jobId ); + mMqttSender->sendBuffer( topic, + reinterpret_cast( data.c_str() ), + data.size(), + [topic]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully sent message on topic " + topic ); + } + else + { + FWE_LOG_ERROR( "Send error " + + std::to_string( static_cast( result ) ) ); + } + } ); + } + } + else + { + FWE_LOG_ERROR( "Unable to parse received Store and Forward data upload request document" ); + } +} + +void +IoTJobsDataRequestHandler::onIotJobDocumentRejected( const uint8_t *buf, size_t size ) +{ + FWE_LOG_ERROR( "DescribeJobExecution request was rejected" ); + + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + Json::Reader reader; + Json::Value rejectedJobDocument; + if ( reader.parse( std::string( buf, buf + size ), rejectedJobDocument ) ) + { + std::string jobId; + + auto execution = rejectedJobDocument["execution"]; + + if ( execution.empty() ) + { + FWE_LOG_ERROR( "Null or empty execution field in received upload request document" ); + FWE_LOG_INFO( "Not retrying DescribeJobExecution request" ); + return; + } + + if ( execution["jobId"].empty() ) + { + FWE_LOG_ERROR( "Null or empty jobId in received upload request document" ); + FWE_LOG_INFO( "Not retrying DescribeJobExecution request" ); + return; + } + + if ( !execution["jobId"].isString() ) + { + FWE_LOG_ERROR( "The jobId in the upload request document is not a string" ); + FWE_LOG_INFO( "Not retrying DescribeJobExecution request" ); + return; + } + + jobId = execution["jobId"].asString(); + + if ( jobId.empty() ) + { + // This should never happen since IoT Jobs should never send us "" as a jobId + FWE_LOG_ERROR( "The jobId received is an empty string" ); + } + + FWE_LOG_INFO( "Not retrying DescribeJobExecution request for jobId: " + jobId ); + } + else + { + FWE_LOG_INFO( "Not retrying DescribeJobExecution request" ); + } +} + +void +IoTJobsDataRequestHandler::onUpdateJobStatusAccepted( const uint8_t *buf, size_t size ) +{ + FWE_LOG_TRACE( "UpdateJobExecution request was accepted" ); + + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + Json::Reader reader; + Json::Value acceptedUpdateJob; + + if ( reader.parse( std::string( buf, buf + size ), acceptedUpdateJob ) ) + { + + if ( acceptedUpdateJob["clientToken"].empty() ) + { + FWE_LOG_ERROR( "Null or empty clientToken in the accepted update request message" ); + return; + } + + if ( !acceptedUpdateJob["clientToken"].isString() ) + { + FWE_LOG_ERROR( "The clientToken in the accepted upload request message is not a string" ); + return; + } + + std::string jobId = acceptedUpdateJob["clientToken"].asString(); + + if ( jobId.empty() ) + { + // checking that the jobId is not "" + FWE_LOG_ERROR( "The jobId in the accepted upload request message is empty" ); + return; + } + + std::lock_guard lock( mCampaignMutex ); + if ( mJobToStatus.find( jobId ) != mJobToStatus.end() && + ( ( mJobToStatus[jobId] == jobStatus::SUCCEEDED ) || ( mJobToStatus[jobId] == jobStatus::REJECTED ) ) ) + { + // if the job has reached a terminal state, then remove it from mJobToStatus + mJobToStatus.erase( jobId ); + } + } +} + +void +IoTJobsDataRequestHandler::onUpdateJobStatusRejected( const uint8_t *buf, size_t size ) +{ + FWE_LOG_ERROR( "UpdateJobExecution request was rejected" ); + + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + Json::Reader reader; + Json::Value rejectedUpdateJob; + + if ( reader.parse( std::string( buf, buf + size ), rejectedUpdateJob ) ) + { + + if ( rejectedUpdateJob["clientToken"].empty() ) + { + FWE_LOG_ERROR( "Null or empty clientToken in the rejected update request message" ); + FWE_LOG_INFO( "Not retrying UpdateJobExecution request" ); + return; + } + + if ( !rejectedUpdateJob["clientToken"].isString() ) + { + FWE_LOG_ERROR( "The clientToken in the rejected upload request message is not a string" ); + FWE_LOG_INFO( "Not retrying UpdateJobExecution request" ); + return; + } + + std::string jobId = rejectedUpdateJob["clientToken"].asString(); + + if ( jobId.empty() ) + { + // checking that the jobId is not "" + FWE_LOG_ERROR( "The jobId in the rejected upload request message is empty" ); + } + + FWE_LOG_INFO( "Not retrying UpdateJobExecution request for JobId: " + jobId ); + } + else + { + FWE_LOG_INFO( "Not retrying UpdateJobExecution request" ); + } +} + +void +IoTJobsDataRequestHandler::onCanceledJobReceived( const uint8_t *buf, size_t size ) +{ + FWE_LOG_INFO( "Received a canceled job notification" ); + + if ( ( buf == nullptr ) || ( size == 0 ) ) + { + FWE_LOG_ERROR( "Received empty IoT Job data from Cloud" ); + return; + } + + Json::Reader reader; + Json::Value canceledJob; + + if ( reader.parse( std::string( buf, buf + size ), canceledJob ) ) + { + std::string jobId; + + if ( canceledJob["jobId"].empty() ) + { + FWE_LOG_ERROR( "Null or empty jobId in the received cancel request message" ); + return; + } + + if ( !canceledJob["jobId"].isString() ) + { + FWE_LOG_ERROR( "The jobId in the cancel request message is not a string" ); + return; + } + + jobId = canceledJob["jobId"].asString(); + + if ( jobId.empty() ) + { + // This should never happen since IoT Jobs should never send us "" as a jobId + FWE_LOG_ERROR( "The jobId received is an empty string" ); + return; + } + + std::unique_lock lock( mCampaignMutex ); + + if ( mJobToStatus.find( jobId ) == mJobToStatus.end() ) + { + lock.unlock(); + FWE_LOG_ERROR( "The jobId of the received canceled job is not running on this device" ) + return; + } + + if ( mJobToCampaignId.find( jobId ) == mJobToCampaignId.end() ) + { + lock.unlock(); + FWE_LOG_ERROR( "The jobId of the received canceled job is not running on this device" ) + return; + } + + // Stop uploading data + auto campaignId = mJobToCampaignId[jobId]; + + lock.unlock(); + + auto pIDs = mStreamManager->getPartitionIdsFromCampaign( campaignId ); + for ( auto pID : pIDs ) + { + mStreamForwarder->cancelForward( + campaignId, pID, Aws::IoTFleetWise::Store::StreamForwarder::Source::IOT_JOB ); + } + + lock.lock(); + mJobToStatus.erase( jobId ); + // we don't want to erase the jobId to CampaignId until after we cancelForward + mJobToCampaignId.erase( jobId ); + lock.unlock(); + + // we cannot update the job status to "CANCELED" since this will return an InvalidStateTransition code + } +} + +void +IoTJobsDataRequestHandler::onJobUploadCompleted( Store::CampaignID campaignId ) +{ + FWE_LOG_INFO( "Received notification that a job has completed uploading data" ); + + std::set jobIds; + { + std::lock_guard lock( mCampaignMutex ); + auto it = mJobToCampaignId.begin(); + while ( it != mJobToCampaignId.end() ) + { + if ( it->second == campaignId ) + { + mJobToStatus[it->first] = jobStatus::SUCCEEDED; + jobIds.insert( it->first ); + it = mJobToCampaignId.erase( it ); + } + else + { + it++; + } + } + } + + if ( jobIds.empty() ) + { + FWE_LOG_ERROR( "No jobId corresponds to campaign " + campaignId ); + return; + } + + for ( const auto &jobId : jobIds ) + { + Json::StreamWriterBuilder builder; + Json::Value updateJobExecution; + updateJobExecution["status"] = "SUCCEEDED"; + // setting clientToken as the jobId so that we can access the jobId in the rejected topic + updateJobExecution["clientToken"] = jobId; + + const std::string data = Json::writeString( builder, updateJobExecution ); + // Publishing is async and non-blocking + std::string topic = mMqttSender->getTopicConfig().updateJobExecutionTopic( jobId ); + mMqttSender->sendBuffer( + topic, reinterpret_cast( data.c_str() ), data.size(), [topic]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully sent message on topic " + topic ); + } + else + { + FWE_LOG_ERROR( "Send error " + std::to_string( static_cast( result ) ) ); + } + } ); + } +} + +std::string +IoTJobsDataRequestHandler::random_string( size_t length ) +{ + const std::string ALPHANUMERIC = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + std::random_device random_device; + std::mt19937 engine( random_device() ); + std::uniform_int_distribution distribution( 0, ALPHANUMERIC.size() - 1 ); + std::string output; + for ( size_t i = 0; i < length; i++ ) + { + // coverity[cert_str53_cpp_violation] Accessing element of string "ALPHANUMERIC" with index + // "distribution(engine)" without range check. + auto index = distribution( engine ); + if ( index < ALPHANUMERIC.length() ) + { + output += ALPHANUMERIC[index]; + } + } + return output; +} + +void +IoTJobsDataRequestHandler::sendGetPendingExecutions() +{ + Json::StreamWriterBuilder builder; + Json::Value pendingJobs; + pendingJobs["clientToken"] = random_string( RANDOM_STRING_SIZE ); + const std::string data = Json::writeString( builder, pendingJobs ); + mMqttSender->sendBuffer( mMqttSender->getTopicConfig().getPendingJobExecutionsTopic, + reinterpret_cast( data.c_str() ), + data.size(), + []( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Successfully requested pending jobs" ); + } + else + { + FWE_LOG_ERROR( "Send error " + std::to_string( static_cast( result ) ) ); + } + } ); +} + +uint64_t +IoTJobsDataRequestHandler::convertEndTimeToMS( const std::string &iso8601 ) +{ + std::tm tmTime = {}; + std::stringstream ss( iso8601 ); + ss >> std::get_time( &tmTime, "%Y-%m-%dT%H:%M:%SZ" ); + + if ( ss.fail() ) + { + FWE_LOG_WARN( "Malformed IoT Job endTime: " + iso8601 + ". Not setting endTime" ); + return 0; + } + + boost::posix_time::ptime time = boost::posix_time::ptime_from_tm( tmTime ); + boost::posix_time::ptime epoch( boost::gregorian::date( 1970, 1, 1 ) ); + boost::posix_time::time_duration duration = time - epoch; + return static_cast( duration.total_milliseconds() ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/IoTJobsDataRequestHandler.h b/src/IoTJobsDataRequestHandler.h new file mode 100644 index 00000000..87f9d941 --- /dev/null +++ b/src/IoTJobsDataRequestHandler.h @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "IReceiver.h" +#include "ISender.h" +#include "StreamForwarder.h" +#include "StreamManager.h" +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief This class handles the receipt of Data Upload Request Jobs from the Cloud. + */ +class IoTJobsDataRequestHandler +{ +public: + /** + * @brief Construct the IoTJobsDataRequestHandler which can receive Jobs from the Cloud. + * @param mqttSender Sender to send any IoT Job requests + * @param receiveIotJobs Receiver to receive IoT Jobs + * @param receiveAcceptedJobDocuments Receiver to receive accepted IoT Job documents + * @param receiveRejectedJobDocuments Receiver to receive rejected IoT Job document requests + * @param receiveAcceptedPendingJobs Receiver to receive accepted pending IoT Jobs + * @param receiveRejectedPendingJobs Receiver to receive rejected pending IoT Job requests + * @param receiveAcceptedUpdateJobs Receiver to receive accepted IoT Job execution updates + * @param receiveRejectedUpdateJobs Receiver to receive rejected IoT Job execution updates + * @param receiveCanceledJobs Receiver to receive IoT Jobs that were canceled in the cloud + * @param streamManager Contains information about active collection schemes + * @param streamForwarder Contains functions to upload data + * @param thingName ThingName + */ + IoTJobsDataRequestHandler( std::shared_ptr mqttSender, + std::shared_ptr receiveIotJobs, + std::shared_ptr receiveAcceptedJobDocuments, + std::shared_ptr receiveRejectedJobDocuments, + std::shared_ptr receiveAcceptedPendingJobs, + std::shared_ptr receiveRejectedPendingJobs, + std::shared_ptr receiveAcceptedUpdateJobs, + std::shared_ptr receiveRejectedUpdateJobs, + std::shared_ptr receiveCanceledJobs, + std::shared_ptr streamManager, + std::shared_ptr streamForwarder, + std::string thingName ); + + static uint64_t convertEndTimeToMS( const std::string &iso8601 ); + + /** + * @brief Callback to be called when the connection is successful and all senders and receivers are ready + */ + void onConnectionEstablished(); + +private: + void onIotJobReceived( const uint8_t *buf, size_t size ); + void onIotJobDocumentAccepted( const uint8_t *buf, size_t size ); + static void onIotJobDocumentRejected( const uint8_t *buf, size_t size ); + void onPendingJobsAccepted( const uint8_t *buf, size_t size ); + void onPendingJobsRejected( const uint8_t *buf, size_t size ); + void onUpdateJobStatusAccepted( const uint8_t *buf, size_t size ); + static void onUpdateJobStatusRejected( const uint8_t *buf, size_t size ); + void onCanceledJobReceived( const uint8_t *buf, size_t size ); + void onJobUploadCompleted( Store::CampaignID campaignId ); + + static std::string random_string( size_t length ); + + void sendGetPendingExecutions(); + + constexpr static size_t RANDOM_STRING_SIZE = 10; + + enum struct jobStatus + { + QUEUED, + IN_PROGRESS, + FAILED, + SUCCEEDED, + CANCELED, + TIMED_OUT, + REJECTED, + REMOVED, + }; + + std::string mThingName; + std::shared_ptr mMqttSender; + std::shared_ptr mStreamManager; + std::shared_ptr mStreamForwarder; + std::map mJobToStatus; + std::map mJobToCampaignId; + + std::mutex mCampaignMutex; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateDataSender.cpp b/src/LastKnownStateDataSender.cpp new file mode 100644 index 00000000..65d32605 --- /dev/null +++ b/src/LastKnownStateDataSender.cpp @@ -0,0 +1,204 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateDataSender.h" +#include "CollectionInspectionAPITypes.h" +#include "IConnectionTypes.h" +#include "LastKnownStateTypes.h" +#include "LoggingModule.h" +#include "SignalTypes.h" +#include "TopicConfig.h" +#include "TraceModule.h" +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +LastKnownStateDataSender::LastKnownStateDataSender( std::shared_ptr lastKnownStateSender, + unsigned maxMessagesPerPayload ) + : mLastKnownStateSender( std::move( lastKnownStateSender ) ) + , mMaxMessagesPerPayload( maxMessagesPerPayload ) +{ +} + +void +LastKnownStateDataSender::processData( std::shared_ptr data, OnDataProcessedCallback callback ) +{ + if ( data == nullptr ) + { + FWE_LOG_WARN( "Nothing to send as the input is empty" ); + return; + } + + auto collectedData = std::dynamic_pointer_cast( data ); + if ( collectedData == nullptr ) + { + FWE_LOG_WARN( "Nothing to send as the input is not a valid LastKnownStateCollectedData" ); + return; + } + + if ( mLastKnownStateSender == nullptr ) + { + FWE_LOG_ERROR( "No sender for LastKnownState data provided" ); + return; + } + + resetProto( collectedData->triggerTime ); + + std::stringstream logMessageIds; + + for ( auto &stateTemplateCollectedSignals : collectedData->stateTemplateCollectedSignals ) + { + logMessageIds << stateTemplateCollectedSignals.stateTemplateSyncId << " "; + + auto capturedStateTemplateSignals = mProtoMsg.add_captured_state_template_signals(); + capturedStateTemplateSignals->set_state_template_sync_id( stateTemplateCollectedSignals.stateTemplateSyncId ); + auto capturedSignalsProto = capturedStateTemplateSignals->mutable_captured_signals(); + + FWE_LOG_INFO( "Ready to send state template data with ID: " + + stateTemplateCollectedSignals.stateTemplateSyncId ); + for ( auto &signal : stateTemplateCollectedSignals.signals ) + { + Schemas::LastKnownState::CapturedSignal signalProto; + signalProto.set_signal_id( signal.signalID ); + + auto signalValueWrapper = signal.value; + + switch ( signalValueWrapper.getType() ) + { + case SignalType::UINT8: + signalProto.set_uint8_value( signalValueWrapper.value.uint8Val ); + break; + case SignalType::INT8: + signalProto.set_int8_value( signalValueWrapper.value.int8Val ); + break; + case SignalType::UINT16: + signalProto.set_uint16_value( signalValueWrapper.value.uint16Val ); + break; + case SignalType::INT16: + signalProto.set_int16_value( signalValueWrapper.value.int16Val ); + break; + case SignalType::UINT32: + signalProto.set_uint32_value( signalValueWrapper.value.uint32Val ); + break; + case SignalType::INT32: + signalProto.set_int32_value( signalValueWrapper.value.int32Val ); + break; + case SignalType::UINT64: + signalProto.set_uint64_value( signalValueWrapper.value.uint64Val ); + break; + case SignalType::INT64: + signalProto.set_int64_value( signalValueWrapper.value.int64Val ); + break; + case SignalType::FLOAT: + signalProto.set_float_value( signalValueWrapper.value.floatVal ); + break; + case SignalType::DOUBLE: + signalProto.set_double_value( signalValueWrapper.value.doubleVal ); + break; + case SignalType::BOOLEAN: + signalProto.set_boolean_value( signalValueWrapper.value.boolVal ); + break; + default: + FWE_LOG_ERROR( + "Skipping value for signal: " + std::to_string( signal.signalID ) + + " with unsupported type: " + std::to_string( static_cast( signalValueWrapper.getType() ) ) ); + continue; + } + + capturedSignalsProto->Add( std::move( signalProto ) ); + mMessageCount++; + if ( mMessageCount >= mMaxMessagesPerPayload ) + { + sendProto( logMessageIds, callback ); + resetProto( collectedData->triggerTime ); + logMessageIds.clear(); + capturedStateTemplateSignals = mProtoMsg.add_captured_state_template_signals(); + capturedStateTemplateSignals->set_state_template_sync_id( + stateTemplateCollectedSignals.stateTemplateSyncId ); + capturedSignalsProto = capturedStateTemplateSignals->mutable_captured_signals(); + } + } + } + + if ( mMessageCount > 0U ) + { + sendProto( logMessageIds, callback ); + } +} + +void +LastKnownStateDataSender::resetProto( Timestamp triggerTime ) +{ + mMessageCount = 0; + mProtoMsg.Clear(); + mProtoMsg.set_collection_event_time_ms_epoch( triggerTime ); +} + +void +LastKnownStateDataSender::sendProto( std::stringstream &logMessageIds, + const Aws::IoTFleetWise::OnDataProcessedCallback &callback ) +{ + auto protoOutput = std::make_shared(); + + // Note: a class member is used to store the serialized proto output to avoid heap fragmentation + if ( !mProtoMsg.SerializeToString( protoOutput.get() ) ) + { + FWE_LOG_ERROR( "Serialization failed for state template data with IDs: " + logMessageIds.str() ); + return; + } + + auto compressedProtoOutput = std::make_shared(); + if ( snappy::Compress( protoOutput->data(), protoOutput->size(), compressedProtoOutput.get() ) == 0U ) + { + FWE_LOG_ERROR( "Data cannot be uploaded due to compression failure for state template data with IDs: " + + logMessageIds.str() ); + return; + } + protoOutput = compressedProtoOutput; + + mLastKnownStateSender->sendBuffer( + mLastKnownStateSender->getTopicConfig().lastKnownStateDataTopic, + reinterpret_cast( protoOutput->data() ), + protoOutput->size(), + [stateTemplateSyncIds = logMessageIds.str(), payloadSize = protoOutput->size(), callback]( + ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_INFO( "A LastKnownState payload of size: " + std::to_string( payloadSize ) + + " bytes has been uploaded for IDs: " + stateTemplateSyncIds ); + + TraceModule::get().incrementVariable( TraceVariable::MQTT_LAST_KNOWN_STATE_MESSAGE_SENT_OUT ); + callback( true, nullptr ); + } + else + { + FWE_LOG_ERROR( "Failed to send state template data with IDs: " + stateTemplateSyncIds + + " with error: " + std::to_string( static_cast( result ) ) ); + TraceModule::get().incrementVariable( TraceVariable::MQTT_LAST_KNOWN_STATE_MESSAGE_FAILED_TO_BE_SENT ); + callback( false, nullptr ); + } + } ); +} + +void +LastKnownStateDataSender::processPersistedData( std::istream &data, + const Json::Value &metadata, + OnPersistedDataProcessedCallback callback ) +{ + static_cast( data ); + static_cast( metadata ); + static_cast( callback ); + + FWE_LOG_WARN( "Upload of persisted data is not supported for LastKnownState" ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateDataSender.h b/src/LastKnownStateDataSender.h new file mode 100644 index 00000000..21a543ae --- /dev/null +++ b/src/LastKnownStateDataSender.h @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "DataSenderTypes.h" +#include "ISender.h" +#include "TimeTypes.h" +#include "last_known_state_data.pb.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Sends LastKnownState data + */ +class LastKnownStateDataSender : public DataSender +{ + +public: + LastKnownStateDataSender( std::shared_ptr lastKnownStateSender, unsigned maxMessagesPerPayload ); + + ~LastKnownStateDataSender() override = default; + + /** + * @brief Process last known state and prepare data for upload + */ + void processData( std::shared_ptr data, OnDataProcessedCallback callback ) override; + + void processPersistedData( std::istream &data, + const Json::Value &metadata, + OnPersistedDataProcessedCallback callback ) override; + +private: + /** + * @brief member variable used to hold the protobuf data and minimize heap fragmentation + */ + Schemas::LastKnownState::LastKnownStateData mProtoMsg; + std::shared_ptr mLastKnownStateSender; + + unsigned mMaxMessagesPerPayload{ 0 }; // max number of messages that can be sent to cloud at one time + unsigned mMessageCount{ 0 }; // current number of messages in the payload + + void resetProto( Timestamp triggerTime ); + + void sendProto( std::stringstream &logMessageIds, const Aws::IoTFleetWise::OnDataProcessedCallback &callback ); +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateIngestion.cpp b/src/LastKnownStateIngestion.cpp new file mode 100644 index 00000000..d9c6d489 --- /dev/null +++ b/src/LastKnownStateIngestion.cpp @@ -0,0 +1,154 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateIngestion.h" +#include "LoggingModule.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +LastKnownStateIngestion::LastKnownStateIngestion() + : mStateTemplatesDiff( std::make_shared() ) +{ +} + +LastKnownStateIngestion::~LastKnownStateIngestion() +{ + // delete any global objects that were allocated by the Protocol Buffer library + google::protobuf::ShutdownProtobufLibrary(); +} + +bool +LastKnownStateIngestion::isReady() const +{ + return mReady; +} + +bool +LastKnownStateIngestion::copyData( const std::uint8_t *inputBuffer, const size_t size ) +{ + if ( isReady() ) + { + return false; + } + + // It is possible that the data is empty, if, for example, the list of state templates is empty. + if ( ( inputBuffer == nullptr ) || ( size == 0 ) ) + { + mProtoBinaryData.clear(); + FWE_LOG_TRACE( "Received empty data" ); + return true; + } + + // We have to guard against document sizes that are too large + if ( size > LAST_KNOWN_STATE_BYTE_SIZE_LIMIT ) + { + FWE_LOG_ERROR( "LastKnownState binary too big. Size: " + std::to_string( size ) + + " limit: " + std::to_string( LAST_KNOWN_STATE_BYTE_SIZE_LIMIT ) ); + return false; + } + + mProtoBinaryData.assign( inputBuffer, inputBuffer + size ); + + mReady = false; + + FWE_LOG_TRACE( "Copy of LastKnownState data success" ); + return true; +} + +bool +LastKnownStateIngestion::build() +{ + // Verify we have not accidentally linked against a version of the library which is incompatible with the version of + // the headers we compiled with. + GOOGLE_PROTOBUF_VERIFY_VERSION; + + if ( isReady() ) + { + return false; + } + + if ( !mProtoStateTemplates.ParseFromArray( mProtoBinaryData.data(), static_cast( mProtoBinaryData.size() ) ) ) + { + FWE_LOG_ERROR( "Failed to parse LastKnownState proto" ); + return false; + } + + auto decoderManifestId = mProtoStateTemplates.decoder_manifest_sync_id(); + if ( decoderManifestId.empty() ) + { + FWE_LOG_ERROR( "Missing decoder manifest ID" ); + return false; + } + mStateTemplatesDiff->version = mProtoStateTemplates.version(); + + std::copy( mProtoStateTemplates.state_template_sync_ids_to_remove().begin(), + mProtoStateTemplates.state_template_sync_ids_to_remove().end(), + std::back_inserter( mStateTemplatesDiff->stateTemplatesToRemove ) ); + + for ( auto &protoStateTemplateInformation : mProtoStateTemplates.state_templates_to_add() ) + { + auto stateTemplateId = protoStateTemplateInformation.state_template_sync_id(); + if ( stateTemplateId.empty() ) + { + FWE_LOG_ERROR( "State template does not have a valid ID" ); + continue; + } + FWE_LOG_INFO( "Building LastKnownState with ID: " + stateTemplateId ); + + auto stateTemplate = std::make_shared(); + stateTemplate->id = stateTemplateId; + stateTemplate->decoderManifestID = decoderManifestId; + + if ( protoStateTemplateInformation.has_periodic_update_strategy() ) + { + auto periodicUpdateStrategy = protoStateTemplateInformation.periodic_update_strategy(); + stateTemplate->updateStrategy = LastKnownStateUpdateStrategy::PERIODIC; + stateTemplate->periodMs = periodicUpdateStrategy.period_ms(); + for ( auto &signalID : protoStateTemplateInformation.signal_ids() ) + { + stateTemplate->signals.emplace_back( LastKnownStateSignalInformation{ signalID } ); + } + } + else if ( protoStateTemplateInformation.has_on_change_update_strategy() ) + { + stateTemplate->updateStrategy = LastKnownStateUpdateStrategy::ON_CHANGE; + for ( auto &signalID : protoStateTemplateInformation.signal_ids() ) + { + stateTemplate->signals.emplace_back( LastKnownStateSignalInformation{ signalID } ); + } + } + else + { + FWE_LOG_ERROR( "Invalid LastKnownState update strategy for state template " + stateTemplate->id ); + continue; + } + + mStateTemplatesDiff->stateTemplatesToAdd.emplace_back( stateTemplate ); + } + + if ( mStateTemplatesDiff->stateTemplatesToAdd.empty() && mStateTemplatesDiff->stateTemplatesToRemove.empty() ) + { + FWE_LOG_ERROR( "LastKnownState message has no state templates" ); + return false; + } + + FWE_LOG_TRACE( "LastKnownState build succeeded" ); + + mReady = true; + return true; +} + +std::shared_ptr +LastKnownStateIngestion::getStateTemplatesDiff() const +{ + return mStateTemplatesDiff; +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateIngestion.h b/src/LastKnownStateIngestion.h new file mode 100644 index 00000000..a57cca8b --- /dev/null +++ b/src/LastKnownStateIngestion.h @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "LastKnownStateTypes.h" +#include "state_templates.pb.h" +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Setting a LastKnownState proto byte size limit for file received from Cloud + */ +constexpr size_t LAST_KNOWN_STATE_BYTE_SIZE_LIMIT = 128000000; + +class LastKnownStateIngestion +{ +public: + LastKnownStateIngestion(); + + virtual ~LastKnownStateIngestion(); + + LastKnownStateIngestion( const LastKnownStateIngestion & ) = delete; + LastKnownStateIngestion &operator=( const LastKnownStateIngestion & ) = delete; + LastKnownStateIngestion( LastKnownStateIngestion && ) = delete; + LastKnownStateIngestion &operator=( LastKnownStateIngestion && ) = delete; + + bool isReady() const; + + virtual bool build(); + + virtual std::shared_ptr getStateTemplatesDiff() const; + + bool copyData( const std::uint8_t *inputBuffer, const size_t size ); + + virtual inline const std::vector & + getData() const + { + return mProtoBinaryData; + } + +private: + /** + * @brief The protobuf message that will hold the deserialized proto. + */ + Schemas::LastKnownState::StateTemplates mProtoStateTemplates; + + /** + * @brief This vector will store the binary data copied from the IReceiver callback. + */ + std::vector mProtoBinaryData; + + /** + * @brief Flag which is true if proto binary data is processed into readable data structures. + */ + bool mReady{ false }; + + /** + * @brief List of structs containing information about state templates + */ + std::shared_ptr mStateTemplatesDiff; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateInspector.cpp b/src/LastKnownStateInspector.cpp new file mode 100644 index 00000000..916661ff --- /dev/null +++ b/src/LastKnownStateInspector.cpp @@ -0,0 +1,704 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateInspector.h" +#include "ICommandDispatcher.h" +#include "QueueTypes.h" +#include "TraceModule.h" +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +LastKnownStateInspector::LastKnownStateInspector( std::shared_ptr commandResponses, + std::shared_ptr schemaPersistency ) + : mCommandResponses( std::move( commandResponses ) ) + , mSchemaPersistency( std::move( schemaPersistency ) ) +{ + restorePersistedMetadata(); +} + +template +void +LastKnownStateInspector::addSignalBuffer( const LastKnownStateSignalInformation &signalIn ) +{ + SignalHistoryBuffer *signalHistoryBufferPtr = nullptr; + signalHistoryBufferPtr = getSignalHistoryBufferPtr( signalIn.signalID ); + if ( signalHistoryBufferPtr == nullptr ) + { + FWE_LOG_WARN( "Unable to retrieve signal history buffer for signal " + std::to_string( signalIn.signalID ) ); + return; + } + signalHistoryBufferPtr->mSize = MAX_SIGNAL_HISTORY_BUFFER_SIZE; +} + +void +LastKnownStateInspector::onStateTemplatesChanged( std::shared_ptr stateTemplates ) +{ + if ( stateTemplates->empty() ) + { + FWE_LOG_INFO( "No state template available" ); + clearUnused( {} ); + return; + } + + // If there is no change in the templates, return early: + if ( mStateTemplates.size() == stateTemplates->size() ) + { + bool difference = false; + for ( const auto &stateTemplate : *stateTemplates ) + { + if ( mStateTemplates.find( stateTemplate->id ) == mStateTemplates.end() ) + { + difference = true; + break; + } + } + if ( !difference ) + { + return; + } + } + + clearUnused( *stateTemplates ); + auto currentTime = mClock->timeSinceEpoch(); + for ( const auto &stateTemplateInfo : *stateTemplates ) + { + bool activated = false; + Timestamp deactivateAfterMonotonicTimeMs = 0; + extractMetadataFields( stateTemplateInfo->id, currentTime, &activated, &deactivateAfterMonotonicTimeMs ); + mStateTemplates.emplace( stateTemplateInfo->id, + StateTemplate{ // Pointers and references into this memory are maintained so + // hold a shared_ptr to it so it does not get deleted + stateTemplateInfo, + activated, + false, // sendSnapshot + deactivateAfterMonotonicTimeMs, + {}, + {}, + {} } ); + + FWE_LOG_TRACE( "Iterating over signals for template " + stateTemplateInfo->id ); + for ( const auto &signalInfo : stateTemplateInfo->signals ) + { + FWE_LOG_TRACE( "Processing signal " + std::to_string( signalInfo.signalID ) ); + if ( signalInfo.signalID == INVALID_SIGNAL_ID ) + { + FWE_LOG_ERROR( "A SignalID with value " + std::to_string( INVALID_SIGNAL_ID ) + " is not allowed" ); + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::STATE_TEMPLATE_ERROR ); + return; + } + auto signalID = signalInfo.signalID; + mSignalToBufferTypeMap.insert( { signalID, signalInfo.signalType } ); + + switch ( signalInfo.signalType ) + { + case SignalType::UINT8: + addSignalBuffer( signalInfo ); + break; + case SignalType::INT8: + addSignalBuffer( signalInfo ); + break; + case SignalType::UINT16: + addSignalBuffer( signalInfo ); + break; + case SignalType::INT16: + addSignalBuffer( signalInfo ); + break; + case SignalType::UINT32: + addSignalBuffer( signalInfo ); + break; + case SignalType::INT32: + addSignalBuffer( signalInfo ); + break; + case SignalType::UINT64: + addSignalBuffer( signalInfo ); + break; + case SignalType::INT64: + addSignalBuffer( signalInfo ); + break; + case SignalType::FLOAT: + addSignalBuffer( signalInfo ); + break; + case SignalType::DOUBLE: + addSignalBuffer( signalInfo ); + break; + case SignalType::BOOLEAN: + addSignalBuffer( signalInfo ); + break; + default: + FWE_LOG_WARN( "Unsupported data type for signal with ID " + std::to_string( signalInfo.signalID ) ); + break; + } + } + } + static_cast( preAllocateBuffers() ); + + FWE_LOG_INFO( "Updated Last Known State Inspection Matrix" ); +} + +void +LastKnownStateInspector::extractMetadataFields( const SyncID &stateTemplateId, + const TimePoint ¤tTime, + bool *activated, + Timestamp *deactivateAfterMonotonicTimeMs ) +{ + auto &persistedMetadata = mPersistedMetadata["stateTemplates"][stateTemplateId]; + if ( !persistedMetadata.isObject() ) + { + return; + } + + if ( persistedMetadata["activated"].isBool() ) + { + *activated = persistedMetadata["activated"].asBool(); + } + else + { + FWE_LOG_WARN( "Invalid persisted metadata for state template with ID: " + stateTemplateId + + ". Field 'activated' not present." ); + } + + if ( persistedMetadata["deactivateAfterSystemTimeMs"].isUInt64() ) + { + auto deactivateAfterSystemTimeMs = persistedMetadata["deactivateAfterSystemTimeMs"].asUInt64(); + if ( deactivateAfterSystemTimeMs > currentTime.systemTimeMs ) + { + *deactivateAfterMonotonicTimeMs = + currentTime.monotonicTimeMs + ( deactivateAfterSystemTimeMs - currentTime.systemTimeMs ); + } + } + else + { + FWE_LOG_WARN( "Invalid persisted metadata for state template with ID: " + stateTemplateId + + ". Field 'deactivateAfterMonotonicTimeMs' not present." ); + } +} + +void +LastKnownStateInspector::onNewCommandReceived( const LastKnownStateCommandRequest &lastKnownStateCommandRequest ) +{ + auto it = mStateTemplates.find( lastKnownStateCommandRequest.stateTemplateID ); + if ( it == mStateTemplates.end() ) + { + FWE_LOG_WARN( "Received a command for missing state template with ID: " + + lastKnownStateCommandRequest.stateTemplateID ); + // coverity[check_return] + mCommandResponses->push( + std::make_shared( lastKnownStateCommandRequest.commandID, + CommandStatus::EXECUTION_FAILED, + REASON_CODE_STATE_TEMPLATE_OUT_OF_SYNC, + "Received a command for missing state template." ) ); + return; + } + + auto &stateTemplate = it->second; + CommandReasonCode reasonCode = REASON_CODE_UNSPECIFIED; + std::string reasonDescription; + switch ( lastKnownStateCommandRequest.operation ) + { + case LastKnownStateOperation::ACTIVATE: { + if ( stateTemplate.activated ) + { + FWE_LOG_INFO( "Updating already activated state template with ID: " + + lastKnownStateCommandRequest.stateTemplateID ); + reasonCode = REASON_CODE_STATE_TEMPLATE_ALREADY_ACTIVATED; + reasonDescription = REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_ACTIVATED; + } + else + { + FWE_LOG_INFO( "Activating state template with ID: " + lastKnownStateCommandRequest.stateTemplateID ); + } + + stateTemplate.activated = true; + stateTemplate.sendSnapshot = true; + // We want the periodic update start from the time the command was received, so we need to + // set the lastTriggerTime to the command received time. + stateTemplate.timeBasedCondition.lastTriggerTime = lastKnownStateCommandRequest.receivedTime; + + Timestamp deactivateAfterSystemTimeMs = 0; + if ( lastKnownStateCommandRequest.deactivateAfterSeconds == 0 ) + { + stateTemplate.deactivateAfterMonotonicTimeMs = 0; + } + else + { + stateTemplate.deactivateAfterMonotonicTimeMs = + lastKnownStateCommandRequest.receivedTime.monotonicTimeMs + + ( static_cast( lastKnownStateCommandRequest.deactivateAfterSeconds ) * 1000 ); + deactivateAfterSystemTimeMs = + lastKnownStateCommandRequest.receivedTime.systemTimeMs + + ( static_cast( lastKnownStateCommandRequest.deactivateAfterSeconds ) * 1000 ); + } + PersistedStateTemplateMetadata metadata; + metadata.stateTemplateId = lastKnownStateCommandRequest.stateTemplateID; + metadata.activated = stateTemplate.activated; + metadata.deactivateAfterSystemTimeMs = deactivateAfterSystemTimeMs; + updatePersistedMetadata( metadata ); + break; + } + case LastKnownStateOperation::DEACTIVATE: + if ( stateTemplate.activated ) + { + deactivateStateTemplate( stateTemplate ); + } + else + { + FWE_LOG_INFO( "Received request to deactivate state template with ID: " + + lastKnownStateCommandRequest.stateTemplateID + + " which is already deactivated. Ignoring it." ); + reasonCode = REASON_CODE_STATE_TEMPLATE_ALREADY_DEACTIVATED; + reasonDescription = REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_DEACTIVATED; + } + break; + case LastKnownStateOperation::FETCH_SNAPSHOT: + FWE_LOG_INFO( "Scheduling a snapshot for state template with ID: " + + lastKnownStateCommandRequest.stateTemplateID ); + stateTemplate.sendSnapshot = true; + break; + default: + FWE_LOG_ERROR( + "Unsupported operation: " + std::to_string( static_cast( lastKnownStateCommandRequest.operation ) ) + + " for Last Known State command with ID: " + lastKnownStateCommandRequest.commandID ); + mCommandResponses->push( std::make_shared( + lastKnownStateCommandRequest.commandID, CommandStatus::EXECUTION_FAILED, REASON_CODE_NOT_SUPPORTED, "" ) ); + return; + } + + // coverity[check_return] + mCommandResponses->push( std::make_shared( + lastKnownStateCommandRequest.commandID, CommandStatus::SUCCEEDED, reasonCode, reasonDescription ) ); +} + +bool +LastKnownStateInspector::preAllocateBuffers() +{ + // Allocate size + size_t usedBytes = 0; + + // Allocate Signal Buffer + for ( auto &bufferVector : mSignalBuffers ) + { + auto signalID = bufferVector.first; + auto signalTypeIterator = mSignalToBufferTypeMap.find( signalID ); + if ( signalTypeIterator != mSignalToBufferTypeMap.end() ) + { + auto signalType = signalTypeIterator->second; + switch ( signalType ) + { + case SignalType::UINT8: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::INT8: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::UINT16: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::INT16: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::UINT32: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::INT32: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::UINT64: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::INT64: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::FLOAT: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::DOUBLE: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + case SignalType::BOOLEAN: + if ( !allocateBuffer( signalID, usedBytes, sizeof( struct SignalSample ) ) ) + { + return false; + } + break; + default: + FWE_LOG_WARN( "Unknown type :" + std::to_string( static_cast( signalType ) ) ); + break; + } + } + else + { + FWE_LOG_WARN( "Fail to allocate buffer for Signal with ID " + std::to_string( signalID ) + + " due to missing signal type" ); + return false; + } + } + TraceModule::get().setVariable( TraceVariable::LAST_KNOWN_STATE_SIGNAL_HISTORY_BUFFER_SIZE, usedBytes ); + return true; +} + +template +bool +LastKnownStateInspector::allocateBuffer( SignalID signalID, size_t &usedBytes, size_t signalSampleSize ) +{ + SignalHistoryBuffer *signalHistoryBufferPtr = nullptr; + signalHistoryBufferPtr = getSignalHistoryBufferPtr( signalID ); + + if ( signalHistoryBufferPtr != nullptr ) + { + auto &buffer = *signalHistoryBufferPtr; + uint64_t requiredBytes = buffer.mSize * static_cast( signalSampleSize ); + if ( usedBytes + requiredBytes > MAX_SAMPLE_MEMORY ) + { + FWE_LOG_WARN( "The requested " + std::to_string( buffer.mSize ) + + " number of signal samples leads to a memory requirement that's above the maximum " + "configured of " + + std::to_string( MAX_SAMPLE_MEMORY ) + "Bytes" ); + buffer.mSize = 0; + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::STATE_TEMPLATE_ERROR ); + return false; + } + usedBytes += static_cast( requiredBytes ); + + // reserve the size like new[] + buffer.mBuffer.resize( buffer.mSize ); + } + return true; +} + +void +LastKnownStateInspector::clearUnused( const StateTemplateList &newStateTemplates ) +{ + std::set newSignalIds; + std::set newStateTemplateIds; + for ( auto &stateTemplate : newStateTemplates ) + { + newStateTemplateIds.insert( stateTemplate->id ); + for ( auto &signal : stateTemplate->signals ) + { + newSignalIds.insert( signal.signalID ); + } + } + + // Delete buffers for signals that are not present in the updated state templates + std::vector signalIdsToRemove; + for ( auto &signalBuffer : mSignalBuffers ) + { + auto signalId = signalBuffer.first; + if ( newSignalIds.find( signalId ) == newSignalIds.end() ) + { + signalIdsToRemove.emplace_back( signalId ); + } + } + for ( auto signalId : signalIdsToRemove ) + { + mSignalBuffers.erase( signalId ); + mSignalToBufferTypeMap.erase( signalId ); + } + + std::vector stateTemplatesToRemove; + for ( auto &stateTemplate : mStateTemplates ) + { + auto stateTemplateId = stateTemplate.first; + if ( newStateTemplateIds.find( stateTemplateId ) == newStateTemplateIds.end() ) + { + stateTemplatesToRemove.emplace_back( stateTemplateId ); + } + } + for ( auto stateTemplateId : stateTemplatesToRemove ) + { + mStateTemplates.erase( stateTemplateId ); + } +} + +void +LastKnownStateInspector::deactivateStateTemplate( StateTemplate &stateTemplate ) +{ + FWE_LOG_INFO( "Deactivating state template with ID: " + stateTemplate.info->id ); + stateTemplate.activated = false; + stateTemplate.deactivateAfterMonotonicTimeMs = 0; + removePersistedMetadata( { stateTemplate.info->id } ); +} + +template +void +LastKnownStateInspector::collectLatestSignal( std::vector &collectedSignals, + SignalID signalID, + Timestamp lastTriggerTime ) +{ + SignalHistoryBuffer *signalHistoryBufferPtr = nullptr; + signalHistoryBufferPtr = getSignalHistoryBufferPtr( signalID ); + if ( signalHistoryBufferPtr == nullptr ) + { + // Invalid access to the map Buffer datatype + FWE_LOG_WARN( "Unable to locate the signal history buffer for signal " + std::to_string( signalID ) ); + return; + } + + if ( signalHistoryBufferPtr->mCounter == 0 ) + { + FWE_LOG_WARN( "Can't collect signal as history buffer for signal " + std::to_string( signalID ) + " is empty" ); + return; + } + + collectedSignals.emplace_back( + CollectedSignal( signalID, + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].mTimestamp, + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].mValue, + mSignalToBufferTypeMap[signalID] ) ); + + if ( signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].mTimestamp <= lastTriggerTime ) + { + // Signal value wasn't updated since last trigger + TraceModule::get().incrementVariable( TraceVariable::LAST_KNOWN_STATE_NO_SIGNAL_CHANGE_ON_PERIODIC_UPDATE ); + } +} + +void +LastKnownStateInspector::collectData( std::vector &collectedSignals, + SignalID signalID, + Timestamp lastTriggerTime ) +{ + if ( mSignalToBufferTypeMap.find( signalID ) != mSignalToBufferTypeMap.end() ) + { + auto signalType = mSignalToBufferTypeMap[signalID]; + switch ( signalType ) + { + case SignalType::UINT8: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::INT8: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::UINT16: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::INT16: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::UINT32: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::INT32: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::UINT64: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::INT64: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::FLOAT: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::DOUBLE: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + case SignalType::BOOLEAN: + collectLatestSignal( collectedSignals, signalID, lastTriggerTime ); + break; + default: + FWE_LOG_WARN( "Unknown type :" + std::to_string( static_cast( signalType ) ) ); + break; + } + } +} + +std::shared_ptr +LastKnownStateInspector::collectNextDataToSend( const TimePoint ¤tTime ) +{ + auto collectedData = std::make_shared(); + collectedData->triggerTime = currentTime.systemTimeMs; + + for ( auto &idAndStateTemplate : mStateTemplates ) + { + auto &stateTemplate = idAndStateTemplate.second; + + if ( ( stateTemplate.deactivateAfterMonotonicTimeMs != 0 ) && + ( currentTime.monotonicTimeMs > stateTemplate.deactivateAfterMonotonicTimeMs ) ) + { + deactivateStateTemplate( stateTemplate ); + } + + std::vector signalsToSend; + + if ( stateTemplate.sendSnapshot ) + { + stateTemplate.sendSnapshot = false; + for ( const auto &signalInfo : stateTemplate.info->signals ) + { + FWE_LOG_TRACE( "Collecting signal with ID " + std::to_string( signalInfo.signalID ) + " for snapshot" ); + collectData( signalsToSend, signalInfo.signalID, 0 ); + } + + // After the snapshot, we have to set all the time based conditions as triggered, otherwise + // this could cause the same signals to be sent again the next time we collect data. + stateTemplate.timeBasedCondition.signalIDsToSend.clear(); + stateTemplate.timeBasedCondition.lastTriggerTime = currentTime; + } + else if ( stateTemplate.activated ) + { + signalsToSend = std::move( stateTemplate.changedSignals ); + + // Here we are using monotonic clock to check whether time window has satisfied + if ( currentTime.monotonicTimeMs - stateTemplate.timeBasedCondition.lastTriggerTime.monotonicTimeMs >= + stateTemplate.info->periodMs ) + { + for ( const auto signalID : stateTemplate.timeBasedCondition.signalIDsToSend ) + { + FWE_LOG_TRACE( "Collecting signal with ID " + std::to_string( signalID ) + + " for periodical update" ); + collectData( + signalsToSend, signalID, stateTemplate.timeBasedCondition.lastTriggerTime.systemTimeMs ); + TraceModule::get().incrementVariable( TraceVariable::LAST_KNOWN_STATE_PERIODIC_UPDATES ); + } + // reset signalID set to be ready for next time window + stateTemplate.timeBasedCondition.signalIDsToSend.clear(); + stateTemplate.timeBasedCondition.lastTriggerTime = currentTime; + } + } + + stateTemplate.changedSignals = std::vector(); + + if ( signalsToSend.empty() ) + { + continue; + } + + collectedData->stateTemplateCollectedSignals.emplace_back( + StateTemplateCollectedSignals{ stateTemplate.info->id, std::move( signalsToSend ) } ); + } + + if ( collectedData->stateTemplateCollectedSignals.empty() ) + { + return nullptr; + } + + return collectedData; +} + +void +LastKnownStateInspector::restorePersistedMetadata() +{ + if ( mSchemaPersistency == nullptr ) + { + return; + } + + mPersistedMetadata.clear(); + mPersistedMetadata["stateTemplates"] = Json::Value( Json::objectValue ); + + auto fileSize = mSchemaPersistency->getSize( DataType::STATE_TEMPLATE_LIST_METADATA ); + if ( fileSize <= 0 ) + { + FWE_LOG_INFO( + "No state template metadata found in persistent storage. All state templates will start as deactivated." ); + return; + } + + std::vector fileContent( fileSize ); + if ( mSchemaPersistency->read( fileContent.data(), fileSize, DataType::STATE_TEMPLATE_LIST_METADATA ) != + ErrorCode::SUCCESS ) + { + return; + } + + Json::CharReaderBuilder builder; + std::stringstream contentStream; + contentStream.rdbuf()->pubsetbuf( reinterpret_cast( fileContent.data() ), + static_cast( fileContent.size() ) ); + std::string errors; + if ( !Json::parseFromStream( builder, contentStream, &mPersistedMetadata, &errors ) ) + { + FWE_LOG_ERROR( "Failed to parse persisted state template metadata: " + errors ); + return; + } + + for ( const auto &stateTemplateId : mPersistedMetadata["stateTemplates"].getMemberNames() ) + { + auto &stateTemplate = mPersistedMetadata["stateTemplates"][stateTemplateId]; + FWE_LOG_INFO( "Restored metadata for state template with ID: " + stateTemplateId + " activated: " + + std::to_string( stateTemplate["activated"].asBool() ) + " deactivateAfterSystemTimeMs: " + + std::to_string( stateTemplate["deactivateAfterSystemTimeMs"].asInt64() ) ); + } + + FWE_LOG_INFO( "Successfully restored persisted state template metadata" ); +} + +void +LastKnownStateInspector::updatePersistedMetadata( const PersistedStateTemplateMetadata &metadata ) +{ + if ( mSchemaPersistency == nullptr ) + { + return; + } + + FWE_LOG_TRACE( "Persisting metadata for state template with ID: " + metadata.stateTemplateId ); + auto &stateTemplateMetadataJson = mPersistedMetadata["stateTemplates"][metadata.stateTemplateId]; + stateTemplateMetadataJson["activated"] = metadata.activated; + stateTemplateMetadataJson["deactivateAfterSystemTimeMs"] = metadata.deactivateAfterSystemTimeMs; + + Json::StreamWriterBuilder builder; + std::string output = Json::writeString( builder, mPersistedMetadata ); + mSchemaPersistency->write( + reinterpret_cast( output.data() ), output.size(), DataType::STATE_TEMPLATE_LIST_METADATA ); +} + +void +LastKnownStateInspector::removePersistedMetadata( const std::vector &stateTemplateIds ) +{ + if ( mSchemaPersistency == nullptr ) + { + return; + } + + for ( const auto &stateTemplateId : stateTemplateIds ) + { + if ( mPersistedMetadata["stateTemplates"].isMember( stateTemplateId ) ) + { + FWE_LOG_TRACE( "Removing metadata for state template with ID: " + stateTemplateId ); + mPersistedMetadata["stateTemplates"].removeMember( stateTemplateId ); + } + } + + Json::StreamWriterBuilder builder; + std::string output = Json::writeString( builder, mPersistedMetadata ); + mSchemaPersistency->write( + reinterpret_cast( output.data() ), output.size(), DataType::STATE_TEMPLATE_LIST_METADATA ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateInspector.h b/src/LastKnownStateInspector.h new file mode 100644 index 00000000..147c7ff4 --- /dev/null +++ b/src/LastKnownStateInspector.h @@ -0,0 +1,382 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CacheAndPersist.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "LastKnownStateTypes.h" +#include "LoggingModule.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include "TraceModule.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Main class to implement last known state inspection engine logic + * + * This class is not thread-safe. The caller needs to ensure that the different functions + * are called only from one thread. This class will be instantiated and used from the Last Known + * State Inspector thread + */ +class LastKnownStateInspector +{ + +public: + LastKnownStateInspector( std::shared_ptr commandResponses, + std::shared_ptr schemaPersistency ); + + /** + * @brief This function handles when there's an update on LKS inspection matrix + * + * @param stateTemplates List containing all state templates + * + */ + void onStateTemplatesChanged( std::shared_ptr stateTemplates ); + + /** + * @brief Handle a new LastKnownState command + */ + void onNewCommandReceived( const LastKnownStateCommandRequest &lastKnownStateCommandRequest ); + + /** + * @brief Copy for a triggered condition data out of the signal buffer + * + * It will copy the data the next triggered condition wants to publish out of + * the signal history buffer. + * It will allocate new memory for the data and return a shared ptr to it. + * This data can then be passed on to be serialized and sent to the cloud. + * Should be called after dataReadyToBeSent() true an shortly after adding new signals + * + * @param currentTime current time for send data out + * + * @return the collected data, or nullptr + */ + std::shared_ptr collectNextDataToSend( const TimePoint ¤tTime ); + /** + * @brief Inspect new sample of signal and also cache it + * + * The signals should come in ordered by time (oldest signals first) + * + * @param id signal ID + * @param receiveTime timestamp at which time was the signal seen on the physical bus + * @param value the signal value + */ + template + void inspectNewSignal( SignalID id, const TimePoint &receiveTime, T value ); + +private: + static const uint32_t MAX_SAMPLE_MEMORY = 20 * 1024 * 1024; // 20MB max for all samples + // For Last Known State, we set the signal history buffer sample size as 1. + static const uint32_t MAX_SIGNAL_HISTORY_BUFFER_SIZE = 1; + // coverity[autosar_cpp14_a0_1_3_violation] EVAL_EQUAL_DISTANCE is used in template function below + static inline double + EVAL_EQUAL_DISTANCE() + { + return 0.001; + } // because static const double (non-integral type) not possible + + /** + * @brief stores the history of one signal. + * + * The signal can be used as part of a condition or only to be published in the case + * a condition is true + */ + template + struct SignalHistoryBuffer + { + // no lint for using equals default constructor as there's an open bug on compiler + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88165 + // NOLINTNEXTLINE(modernize-use-equals-default) + SignalHistoryBuffer() + { + } + std::vector> + mBuffer; // ringbuffer, Consider to move to raw pointer allocated with new[] if vector allocates too much + size_t mSize{ 0 }; // minimum size needed by all conditions, buffer must be at least this big + uint32_t mCurrentPosition{ 0 }; /**< position in ringbuffer needs to come after size as it depends on it */ + uint32_t mCounter{ 0 }; /**< over all recorded samples*/ + TimePoint mLastSample{ 0, 0 }; + }; + + /** + * @brief This function allocate signal history buffer for the signal + */ + template + void addSignalBuffer( const LastKnownStateSignalInformation &signalIn ); + + /** + * @brief Allocate memory for signal history buffer + * @param signalID signal ID + * @param usedBytes total allocated bytes for vector mBuffer in signal history buffer. + * @param signalSampleSize number of bytes for the instantiated template + */ + template + bool allocateBuffer( SignalID signalID, size_t &usedBytes, size_t signalSampleSize ); + + /** + * @brief Allocate memory for signal history buffer + */ + bool preAllocateBuffers(); + + /** + * @brief This function signals that are ready for update + * @param collectedSignals where the collected signals should be added to + * @param signalID signal ID + * @param lastTriggerTime last trigger time of timestamp condition (system time) + */ + void collectData( std::vector &collectedSignals, SignalID signalID, Timestamp lastTriggerTime ); + + /** + * @brief This function collect the latest sample of signals + * @param collectedSignals where the collected signals should be added to + * @param signalID signal ID + * @param lastTriggerTime last trigger time of timestamp condition (system time) + */ + template + void collectLatestSignal( std::vector &collectedSignals, + SignalID signalID, + Timestamp lastTriggerTime ); + + /** + * @brief clear the inspection logic as well as buffered data for state templates that were removed + * + * @param newStateTemplates List containing all state templates. Anything not included in this list will be removed + */ + void clearUnused( const StateTemplateList &newStateTemplates ); + + // VSS supported datatypes + using signalHistoryBufferVar = boost::variant, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer, + SignalHistoryBuffer>; + using SignalHistoryBufferCollection = std::unordered_map; + SignalHistoryBufferCollection + mSignalBuffers; /**< signal history buffer. First vector has the signalID as index. In the nested vector + * the different subsampling of this signal are stored. */ + + using SignalToBufferTypeMap = std::unordered_map; + SignalToBufferTypeMap mSignalToBufferTypeMap; + + template + SignalHistoryBuffer * + getSignalHistoryBufferPtr( SignalID signalID ) + { + SignalHistoryBuffer *resVec = nullptr; + if ( mSignalBuffers.find( signalID ) == mSignalBuffers.end() ) + { + // create a new map entry + auto mapEntryVec = SignalHistoryBuffer{}; + try + { + signalHistoryBufferVar mapEntry = mapEntryVec; + mSignalBuffers.insert( { signalID, mapEntry } ); + } + catch ( ... ) + { + FWE_LOG_ERROR( "Cannot insert the signalHistoryBuffer for Signal " + std::to_string( signalID ) ); + return nullptr; + } + } + + try + { + auto signalBufferPtr = mSignalBuffers.find( signalID ); + if ( signalBufferPtr != mSignalBuffers.end() ) + { + resVec = boost::get>( &( signalBufferPtr->second ) ); + } + } + catch ( ... ) + { + FWE_LOG_ERROR( "Cannot retrieve the signalHistoryBuffer for Signal " + std::to_string( signalID ) ); + } + return resVec; + } + + // This struct contains information about one time condition for a specific period + struct TimebasedCondition + { + TimePoint lastTriggerTime{ 0, 0 }; + std::set signalIDsToSend; + }; + + // Queue to send responses to the LastKnownState commands + std::shared_ptr mCommandResponses; + + std::shared_ptr mSchemaPersistency; + + std::shared_ptr mClock = ClockHandler::getClock(); + + struct StateTemplate + { + std::shared_ptr info; + bool activated{ false }; + bool sendSnapshot{ false }; + Timestamp deactivateAfterMonotonicTimeMs{ 0 }; + // Map of signal update period. This only contains signals that have update strategy as period + std::unordered_map signalUpdatePeriodMap; + // Map from period to time based condition + TimebasedCondition timeBasedCondition; + // This is a buffer to hold the signals to be sent out + std::vector changedSignals; + }; + std::map mStateTemplates; + + void deactivateStateTemplate( StateTemplate &stateTemplate ); + + struct PersistedStateTemplateMetadata + { + SyncID stateTemplateId; + bool activated; + // We need to store the system time since the monotonic clock can be reset between system or + // application restarts. + Timestamp deactivateAfterSystemTimeMs; + }; + Json::Value mPersistedMetadata; + + /** + * @brief Reads the persisted metadata (if any) from the persistency storage + * + * The metadata isn't applied to existing state templates. This only parses the content and + * stores the result for later use. + */ + void restorePersistedMetadata(); + + /** + * @brief Update the metadata for a specific state template and immediately store it + * + * @param metadata The modified metadata to update + */ + void updatePersistedMetadata( const PersistedStateTemplateMetadata &metadata ); + + /** + * @brief Remove the persisted metadata for the given state template IDs and store the final result + * + * @param stateTemplateIds The IDs of the state templates to remove + */ + void removePersistedMetadata( const std::vector &stateTemplateIds ); + + /** + * @brief Helper function to get some fields from the json metadata + * + * @param stateTemplateId The ID of the state template + * @param currentTime The current time, used to calculate the monotonic time for auto deactivation + * @param activated Output variable which will contain the updated activation state (if any) + * @param deactivateAfterMonotonicTimeMs Output variable which will contain the updated time to auto deactivate the + * state template + */ + void extractMetadataFields( const SyncID &stateTemplateId, + const TimePoint ¤tTime, + bool *activated, + Timestamp *deactivateAfterMonotonicTimeMs ); +}; + +template +void +// coverity[misra_cpp_2008_rule_14_7_1_violation] function will be instantiated in incoming module +LastKnownStateInspector::inspectNewSignal( SignalID id, const TimePoint &receiveTime, T value ) +{ + if ( mSignalBuffers.find( id ) == mSignalBuffers.end() || mSignalBuffers[id].empty() ) + { + // Signal not collected by any active condition + return; + } + + // Iterate through all sampling intervals of the signal + SignalHistoryBuffer *signalHistoryBufferPtr = nullptr; + signalHistoryBufferPtr = getSignalHistoryBufferPtr( id ); + if ( signalHistoryBufferPtr == nullptr ) + { + // Invalid access to the map Buffer datatype + FWE_LOG_WARN( "Unable to locate the signal history buffer for signal " + std::to_string( id ) ); + return; + } + + for ( auto &stateTemplate : mStateTemplates ) + { + auto updateStrategy = stateTemplate.second.info->updateStrategy; + for ( auto &signal : stateTemplate.second.info->signals ) + { + if ( id != signal.signalID ) + { + continue; + } + + if ( ( updateStrategy == LastKnownStateUpdateStrategy::ON_CHANGE ) && stateTemplate.second.activated ) + { + // Under one of the following conditions, we will send out update for this signal + // 1. signal history buffer is empty previously, or + // 2. signal history buffer is not empty and signal data type is double / float and the + // difference between prev and latest value is greater than EVAL_EQUAL_DISTANCE + // 3. signal history buffer is not empty and signal data type is non-double / non-float and + // the prev value is not equal to latest value. + if ( ( signalHistoryBufferPtr->mCounter == 0 ) || + ( ( ( mSignalToBufferTypeMap[id] == SignalType::DOUBLE ) || + ( mSignalToBufferTypeMap[id] == SignalType::FLOAT ) ) + ? ( std::abs( static_cast( value ) - + static_cast( + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition] + .mValue ) ) > EVAL_EQUAL_DISTANCE() ) + : ( value != + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].mValue ) ) ) + { + // We shall immediately publish this to the buffer + FWE_LOG_TRACE( "Collecting signal with ID " + std::to_string( id ) + " for on change policy" ); + stateTemplate.second.changedSignals.emplace_back( + CollectedSignal( id, receiveTime.systemTimeMs, value, mSignalToBufferTypeMap[id] ) ); + TraceModule::get().incrementVariable( TraceVariable::LAST_KNOWN_STATE_ON_CHANGE_UPDATES ); + } + } + else if ( updateStrategy == LastKnownStateUpdateStrategy::PERIODIC ) + { + stateTemplate.second.timeBasedCondition.signalIDsToSend.emplace( id ); + } + } + } + + signalHistoryBufferPtr->mCurrentPosition++; + if ( signalHistoryBufferPtr->mCurrentPosition >= signalHistoryBufferPtr->mSize ) + { + signalHistoryBufferPtr->mCurrentPosition = 0; + } + // Add this new sample to the signal history buffer + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].mValue = value; + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].mTimestamp = receiveTime.systemTimeMs; + signalHistoryBufferPtr->mBuffer[signalHistoryBufferPtr->mCurrentPosition].setAlreadyConsumed( ALL_CONDITIONS, + false ); + if ( signalHistoryBufferPtr->mCounter < MAX_SIGNAL_HISTORY_BUFFER_SIZE ) + { + signalHistoryBufferPtr->mCounter++; + } + signalHistoryBufferPtr->mLastSample = receiveTime; +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateSchema.cpp b/src/LastKnownStateSchema.cpp new file mode 100644 index 00000000..4c5a0162 --- /dev/null +++ b/src/LastKnownStateSchema.cpp @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateSchema.h" +#include "LoggingModule.h" +#include "TraceModule.h" + +namespace Aws +{ +namespace IoTFleetWise +{ + +LastKnownStateSchema::LastKnownStateSchema( std::shared_ptr receiverLastKnownState ) +{ + receiverLastKnownState->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { + onLastKnownStateReceived( receivedMessage ); + } ); +} + +void +LastKnownStateSchema::onLastKnownStateReceived( const ReceivedConnectivityMessage &receivedMessage ) +{ + auto lastKnownStateIngestion = std::make_shared(); + + if ( !lastKnownStateIngestion->copyData( receivedMessage.buf, receivedMessage.size ) ) + { + FWE_LOG_ERROR( "LastKnownState copyData from IoT core failed" ); + return; + } + + mLastKnownStateListeners.notify( lastKnownStateIngestion ); + FWE_LOG_TRACE( "Received state templates" ); + TraceModule::get().incrementVariable( TraceVariable::STATE_TEMPLATES_RECEIVED ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateSchema.h b/src/LastKnownStateSchema.h new file mode 100644 index 00000000..7bdf2c28 --- /dev/null +++ b/src/LastKnownStateSchema.h @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "IReceiver.h" +#include "LastKnownStateIngestion.h" +#include "Listener.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief This class handles received state templates + */ +class LastKnownStateSchema +{ +public: + /** + * @brief Callback function used to notify when a new state template arrives from the Cloud. + * + * @param stateTemplates + */ + using OnLastKnownStateReceivedCallback = + std::function lastKnownStateIngestion )>; + + /** + * @param receiverLastKnownState Receiver for a state templates.proto message on a LastKnownState + * topic + */ + LastKnownStateSchema( std::shared_ptr receiverLastKnownState ); + + ~LastKnownStateSchema() = default; + + LastKnownStateSchema( const LastKnownStateSchema & ) = delete; + LastKnownStateSchema &operator=( const LastKnownStateSchema & ) = delete; + LastKnownStateSchema( LastKnownStateSchema && ) = delete; + LastKnownStateSchema &operator=( LastKnownStateSchema && ) = delete; + + void + subscribeToLastKnownStateReceived( OnLastKnownStateReceivedCallback callback ) + { + mLastKnownStateListeners.subscribe( callback ); + } + +private: + /** + * @brief Callback that should be called whenever a new message with state templates is received + * from the Cloud. + * @param receivedMessage struct containing message data and metadata + */ + void onLastKnownStateReceived( const ReceivedConnectivityMessage &receivedMessage ); + + ThreadSafeListeners mLastKnownStateListeners; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateTypes.h b/src/LastKnownStateTypes.h new file mode 100644 index 00000000..1de1831d --- /dev/null +++ b/src/LastKnownStateTypes.h @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "DataSenderTypes.h" +#include "ICommandDispatcher.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +constexpr auto REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_ACTIVATED = + "State template activated successfully. The auto-stop value has been updated to reflect the latest settings."; +constexpr auto REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_DEACTIVATED = + "No action taken. The state template you attempted to deactivate was already deactivated."; + +enum class LastKnownStateUpdateStrategy +{ + PERIODIC = 0, + ON_CHANGE = 1, +}; + +struct LastKnownStateSignalInformation +{ + SignalID signalID; + SignalType signalType{ SignalType::DOUBLE }; +}; + +struct StateTemplateInformation +{ + SyncID id; + SyncID decoderManifestID; + std::vector signals; + LastKnownStateUpdateStrategy updateStrategy; + // For periodic update strategy only. Indicates the interval to periodically send the data. + uint64_t periodMs{ 0 }; +}; + +using StateTemplateList = std::vector>; + +struct StateTemplatesDiff +{ + uint64_t version{ 0 }; + StateTemplateList stateTemplatesToAdd; + std::vector stateTemplatesToRemove; +}; + +struct StateTemplateCollectedSignals +{ + SyncID stateTemplateSyncId; + std::vector signals; +}; + +struct LastKnownStateCollectedData : DataToSend +{ + Timestamp triggerTime; + std::vector stateTemplateCollectedSignals; + + ~LastKnownStateCollectedData() override = default; + + SenderDataType + getDataType() const override + { + return SenderDataType::LAST_KNOWN_STATE; + } +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateWorkerThread.cpp b/src/LastKnownStateWorkerThread.cpp new file mode 100644 index 00000000..fe9e58fe --- /dev/null +++ b/src/LastKnownStateWorkerThread.cpp @@ -0,0 +1,293 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateWorkerThread.h" +#include "LastKnownStateTypes.h" +#include "LoggingModule.h" +#include "QueueTypes.h" +#include "SignalTypes.h" +#include "TraceModule.h" +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +LastKnownStateWorkerThread::LastKnownStateWorkerThread( + std::shared_ptr inputSignalBuffer, + std::shared_ptr collectedLastKnownStateData, + std::unique_ptr lastKnownStateInspector, + uint32_t idleTimeMs ) + : mInputSignalBuffer( std::move( inputSignalBuffer ) ) + , mCollectedLastKnownStateData( std::move( collectedLastKnownStateData ) ) + , mLastKnownStateInspector( std::move( lastKnownStateInspector ) ) +{ + if ( idleTimeMs != 0 ) + { + mIdleTimeMs = idleTimeMs; + } +} + +bool +LastKnownStateWorkerThread::start() +{ + if ( mInputSignalBuffer == nullptr ) + { + FWE_LOG_ERROR( "Failed to initialize Last Known State worker thread, no signal buffer provided" ); + return false; + } + + if ( mCollectedLastKnownStateData == nullptr ) + { + FWE_LOG_ERROR( "Failed to initialize Last Known State worker thread, no output queue for upload provided" ); + return false; + } + + // Prevent concurrent stop/init + std::lock_guard lock( mThreadMutex ); + // On multi core systems the shared variable mShouldStop must be updated for + // all cores before starting the thread otherwise thread will directly end + mShouldStop.store( false ); + if ( !mThread.create( doWork, this ) ) + { + FWE_LOG_TRACE( "Last Known State Inspection Thread failed to start" ); + } + else + { + FWE_LOG_TRACE( "Last Known State Inspection Thread started" ); + mThread.setThreadName( "fwDILKSEng" ); + } + + return mThread.isActive() && mThread.isValid(); +} + +void +LastKnownStateWorkerThread::doWork( void *data ) +{ + LastKnownStateWorkerThread *consumer = static_cast( data ); + + while ( true ) + { + { + std::lock_guard lock( consumer->mStateTemplatesMutex ); + if ( consumer->mStateTemplatesAvailable ) + { + consumer->mStateTemplatesAvailable = false; + consumer->mStateTemplates = consumer->mStateTemplatesInput; + consumer->mLastKnownStateInspector->onStateTemplatesChanged( consumer->mStateTemplates ); + } + } + + { + std::lock_guard lock( consumer->mLastKnownStateCommandsMutex ); + for ( auto &command : consumer->mLastKnownStateCommandsInput ) + { + consumer->mLastKnownStateInspector->onNewCommandReceived( command ); + } + consumer->mLastKnownStateCommandsInput.clear(); + } + + // Data should only be processed if state templates are available + if ( ( consumer->mStateTemplates != nullptr ) && ( !consumer->mStateTemplates->empty() ) ) + { + TimePoint currentTime = consumer->mClock->timeSinceEpoch(); + auto consumeSignalGroups = [&]( const CollectedDataFrame &dataFrame ) { + static_cast( dataFrame ); + + for ( auto &signal : dataFrame.mCollectedSignals ) + { + auto signalValue = signal.getValue(); + switch ( signalValue.getType() ) + { + case SignalType::UINT8: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.uint8Val ); + break; + case SignalType::INT8: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.int8Val ); + break; + case SignalType::UINT16: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.uint16Val ); + break; + case SignalType::INT16: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.int16Val ); + break; + case SignalType::UINT32: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.uint32Val ); + break; + case SignalType::INT32: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.int32Val ); + break; + case SignalType::UINT64: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.uint64Val ); + break; + case SignalType::INT64: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.int64Val ); + break; + case SignalType::FLOAT: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.floatVal ); + break; + case SignalType::DOUBLE: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.doubleVal ); + break; + case SignalType::BOOLEAN: + consumer->mLastKnownStateInspector->inspectNewSignal( + signal.signalID, + calculateMonotonicTime( currentTime, signal.receiveTime ), + signalValue.value.boolVal ); + break; + case SignalType::STRING: + FWE_LOG_WARN( "String data is not available for last known state collection" ); + break; + case SignalType::UNKNOWN: + FWE_LOG_WARN( " Unknown signals [signal ID: " + std::to_string( signal.signalID ) + + " ] not supported for last known state collection" ); + break; +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + case SignalType::COMPLEX_SIGNAL: + FWE_LOG_WARN( "Vision system data is not available for last known state collection" ); + break; +#endif + } + } + }; + consumer->mInputSignalBuffer->consumeAll( consumeSignalGroups ); + + // Collect and upload data + auto collectedData = + consumer->mLastKnownStateInspector->collectNextDataToSend( consumer->mClock->timeSinceEpoch() ); + if ( collectedData != nullptr ) + { + TraceModule::get().incrementVariable( TraceVariable::LAST_KNOWN_STATE_COLLECTION_TRIGGERS ); + static_cast( consumer->mCollectedLastKnownStateData->push( collectedData ) ); + } + consumer->mWait.wait( consumer->mIdleTimeMs ); + } + else + { + // Consume all data received so far to prevent the queue from becoming full + consumer->mInputSignalBuffer->consumeAll( [&]( const CollectedDataFrame &dataFrame ) { + static_cast( dataFrame ); + } ); + // Wait for the state templates to arrive + consumer->mWait.wait( Signal::WaitWithPredicate ); + } + + if ( consumer->shouldStop() ) + { + break; + } + } +} + +TimePoint +LastKnownStateWorkerThread::calculateMonotonicTime( const TimePoint &currTime, Timestamp systemTimeMs ) +{ + TimePoint convertedTime = timePointFromSystemTime( currTime, systemTimeMs ); + if ( ( convertedTime.systemTimeMs == 0 ) && ( convertedTime.monotonicTimeMs == 0 ) ) + { + FWE_LOG_ERROR( "The system time " + std::to_string( systemTimeMs ) + + " corresponds to a time in the past before the monotonic" + + " clock started ticking. Current system time: " + std::to_string( currTime.systemTimeMs ) + + ". Current monotonic time: " + std::to_string( currTime.monotonicTimeMs ) ); + return TimePoint{ systemTimeMs, 0 }; + } + return convertedTime; +} + +void +LastKnownStateWorkerThread::onNewDataAvailable() +{ + mWait.notify(); +} + +void +LastKnownStateWorkerThread::onStateTemplatesChanged( std::shared_ptr stateTemplates ) +{ + std::lock_guard lock( mStateTemplatesMutex ); + mStateTemplatesInput = stateTemplates; + mStateTemplatesAvailable = true; + FWE_LOG_TRACE( "State templates were updated" ); + // Wake up the thread. + mWait.notify(); +} + +void +LastKnownStateWorkerThread::onNewCommandReceived( const LastKnownStateCommandRequest &commandRequest ) +{ + std::lock_guard lock( mLastKnownStateCommandsMutex ); + mLastKnownStateCommandsInput.push_back( commandRequest ); + mWait.notify(); +} + +bool +LastKnownStateWorkerThread::stop() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return true; + } + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( true, std::memory_order_relaxed ); + FWE_LOG_TRACE( "Request stop" ); + mWait.notify(); + mThread.release(); + FWE_LOG_TRACE( "Stop finished" ); + mShouldStop.store( false, std::memory_order_relaxed ); + return !mThread.isActive(); +} + +bool +LastKnownStateWorkerThread::shouldStop() const +{ + return mShouldStop.load( std::memory_order_relaxed ); +} + +bool +LastKnownStateWorkerThread::isAlive() +{ + return mThread.isValid() && mThread.isActive(); +} + +LastKnownStateWorkerThread::~LastKnownStateWorkerThread() +{ + if ( isAlive() ) + { + stop(); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/LastKnownStateWorkerThread.h b/src/LastKnownStateWorkerThread.h new file mode 100644 index 00000000..a1a8edf4 --- /dev/null +++ b/src/LastKnownStateWorkerThread.h @@ -0,0 +1,112 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "LastKnownStateInspector.h" +#include "LastKnownStateTypes.h" +#include "Signal.h" +#include "Thread.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief This thread retrieves and inspects data for the Last Known State data collection + * based on the state templates and queues collected data for the upload + */ +class LastKnownStateWorkerThread +{ +public: + LastKnownStateWorkerThread( std::shared_ptr inputSignalBuffer, + std::shared_ptr collectedLastKnownStateData, + std::unique_ptr lastKnownStateInspector, + uint32_t idleTimeMs ); + ~LastKnownStateWorkerThread(); + + LastKnownStateWorkerThread( const LastKnownStateWorkerThread & ) = delete; + LastKnownStateWorkerThread &operator=( const LastKnownStateWorkerThread & ) = delete; + LastKnownStateWorkerThread( LastKnownStateWorkerThread && ) = delete; + LastKnownStateWorkerThread &operator=( LastKnownStateWorkerThread && ) = delete; + + /** + * @brief Callback to notify when there is new data available + */ + void onNewDataAvailable(); + + /** + * @brief Callback to notify that the new state templates are available + */ + void onStateTemplatesChanged( std::shared_ptr stateTemplates ); + + /** + * @brief Handle a new LastKnownState command + */ + void onNewCommandReceived( const LastKnownStateCommandRequest &commandRequest ); + + /** + * @brief stops the internal thread if started and wait until it finishes + * + * @return true if the stop was successful + */ + bool stop(); + + /** + * @brief starts the internal thread + * + * @return true if the start was successful + */ + bool start(); + + /** + * @brief Checks that the worker thread is healthy and consuming data. + */ + bool isAlive(); + +private: + static constexpr uint32_t DEFAULT_THREAD_IDLE_TIME_MS = 1000; + + // Stop the thread + // Intercepts stop signals. + bool shouldStop() const; + + static void doWork( void *data ); + + static TimePoint calculateMonotonicTime( const TimePoint &currTime, Timestamp systemTimeMs ); + + std::shared_ptr mInputSignalBuffer; + Thread mThread; + std::atomic mShouldStop{ false }; + std::mutex mThreadMutex; + Signal mWait; + std::shared_ptr mClock = ClockHandler::getClock(); + + uint32_t mIdleTimeMs{ DEFAULT_THREAD_IDLE_TIME_MS }; + + bool mStateTemplatesAvailable{ false }; + std::shared_ptr mStateTemplatesInput; + std::shared_ptr mStateTemplates; + std::mutex mStateTemplatesMutex; + std::shared_ptr mCollectedLastKnownStateData; + + std::vector mLastKnownStateCommandsInput; + std::mutex mLastKnownStateCommandsMutex; + + std::unique_ptr mLastKnownStateInspector; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/NamedSignalDataSource.cpp b/src/NamedSignalDataSource.cpp new file mode 100644 index 00000000..64f28586 --- /dev/null +++ b/src/NamedSignalDataSource.cpp @@ -0,0 +1,127 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "NamedSignalDataSource.h" +#include "IDecoderManifest.h" +#include "LoggingModule.h" +#include "QueueTypes.h" +#include "TraceModule.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +NamedSignalDataSource::NamedSignalDataSource( InterfaceID interfaceId, + SignalBufferDistributorPtr signalBufferDistributor ) + : mInterfaceId( std::move( interfaceId ) ) + , mSignalBufferDistributor( std::move( signalBufferDistributor ) ) +{ +} + +void +NamedSignalDataSource::ingestSignalValue( Timestamp timestamp, + const std::string &name, + const DecodedSignalValue &value, + FetchRequestID fetchRequestID ) +{ + std::vector> values = { std::make_pair( name, value ) }; + ingestMultipleSignalValues( timestamp, values, fetchRequestID ); +} + +void +NamedSignalDataSource::ingestMultipleSignalValues( + Timestamp timestamp, + const std::vector> &values, + FetchRequestID fetchRequestID ) +{ + if ( timestamp == 0 ) + { + TraceModule::get().incrementVariable( TraceVariable::POLLING_TIMESTAMP_COUNTER ); + timestamp = mClock->systemTimeSinceEpochMs(); + } + if ( timestamp < mLastTimestamp ) + { + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::NOT_TIME_MONOTONIC_FRAMES ); + } + mLastTimestamp = timestamp; + + std::lock_guard lock( mDecoderDictMutex ); + if ( mDecoderDictionary == nullptr ) + { + return; + } + auto decodersForInterface = mDecoderDictionary->customDecoderMethod.find( mInterfaceId ); + if ( decodersForInterface == mDecoderDictionary->customDecoderMethod.end() ) + { + return; + } + + CollectedSignalsGroup collectedSignalsGroup; + for ( const auto &value : values ) + { + auto decoderFormat = decodersForInterface->second.find( value.first ); + if ( decoderFormat == decodersForInterface->second.end() ) + { + continue; + } + + collectedSignalsGroup.push_back( CollectedSignal::fromDecodedSignal( decoderFormat->second.mSignalID, + timestamp, + value.second, + decoderFormat->second.mSignalType, + fetchRequestID ) ); + } + if ( !collectedSignalsGroup.empty() ) + { + mSignalBufferDistributor->push( CollectedDataFrame( collectedSignalsGroup ) ); + } +} + +void +NamedSignalDataSource::onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, + VehicleDataSourceProtocol networkProtocol ) +{ + if ( networkProtocol != VehicleDataSourceProtocol::CUSTOM_DECODING ) + { + return; + } + std::lock_guard lock( mDecoderDictMutex ); + mDecoderDictionary = std::dynamic_pointer_cast( dictionary ); + if ( dictionary == nullptr ) + { + FWE_LOG_TRACE( "Decoder dictionary removed" ); + } + else + { + FWE_LOG_TRACE( "Decoder dictionary updated" ); + } +} + +SignalID +NamedSignalDataSource::getNamedSignalID( const std::string &name ) +{ + std::lock_guard lock( mDecoderDictMutex ); + if ( mDecoderDictionary == nullptr ) + { + return INVALID_SIGNAL_ID; + } + auto decodersForInterface = mDecoderDictionary->customDecoderMethod.find( mInterfaceId ); + if ( decodersForInterface == mDecoderDictionary->customDecoderMethod.end() ) + { + return INVALID_SIGNAL_ID; + } + + auto decoderFormat = decodersForInterface->second.find( name ); + if ( decoderFormat == decodersForInterface->second.end() ) + { + return INVALID_SIGNAL_ID; + } + + return decoderFormat->second.mSignalID; +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/NamedSignalDataSource.h b/src/NamedSignalDataSource.h new file mode 100644 index 00000000..e3581dc3 --- /dev/null +++ b/src/NamedSignalDataSource.h @@ -0,0 +1,81 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "IDecoderDictionary.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include "VehicleDataSourceTypes.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * @brief Named signal data source. This data source uses Custom Signal Decoding, where the decoder is the name of the + * signal, which can for example be the fully-qualified name of the signal from the signal catalog. + */ +class NamedSignalDataSource +{ +public: + /** @brief Construct named signal data source + * @param interfaceId Interface identifier + * @param signalBufferDistributor Signal buffer distributor */ + NamedSignalDataSource( InterfaceID interfaceId, SignalBufferDistributorPtr signalBufferDistributor ); + ~NamedSignalDataSource() = default; + + NamedSignalDataSource( const NamedSignalDataSource & ) = delete; + NamedSignalDataSource &operator=( const NamedSignalDataSource & ) = delete; + NamedSignalDataSource( NamedSignalDataSource && ) = delete; + NamedSignalDataSource &operator=( NamedSignalDataSource && ) = delete; + + /** @brief Ingest signal value by name + * @param timestamp Timestamp of signal value in milliseconds since epoch, or zero if unknown. + * @param name Signal name + * @param value Signal value + * @param fetchRequestID contains fetch request IDs associated with the signal for the given campaign + */ + void ingestSignalValue( Timestamp timestamp, + const std::string &name, + const DecodedSignalValue &value, + FetchRequestID fetchRequestID = DEFAULT_FETCH_REQUEST_ID ); + + /** @brief Ingest multiple signal values by name + * @param timestamp Timestamp of signal values in milliseconds since epoch, or zero if unknown. + * @param values Signal values + * @param fetchRequestID contains fetch request IDs associated with the signal for the given campaign + */ + void ingestMultipleSignalValues( Timestamp timestamp, + const std::vector> &values, + FetchRequestID fetchRequestID = DEFAULT_FETCH_REQUEST_ID ); + + void onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, + VehicleDataSourceProtocol networkProtocol ); + + /** @brief Gets signal id for named signal from the decoder dictionary + * @param name signal name + * @return signal id + */ + SignalID getNamedSignalID( const std::string &name ); + +private: + std::shared_ptr mClock = ClockHandler::getClock(); + InterfaceID mInterfaceId; + SignalBufferDistributorPtr mSignalBufferDistributor; + std::mutex mDecoderDictMutex; + std::shared_ptr mDecoderDictionary; + Timestamp mLastTimestamp{}; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/Persistency.cpp b/src/Persistency.cpp index 08e28738..12db9164 100644 --- a/src/Persistency.cpp +++ b/src/Persistency.cpp @@ -10,6 +10,10 @@ #include #include +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include +#endif + namespace Aws { namespace IoTFleetWise @@ -39,6 +43,12 @@ CollectionSchemeManager::retrieve( DataType retrieveType ) infoStr = "Retrieved a DecoderManifest of size "; errStr = "Failed to retrieve the DecoderManifest from the persistency module due to an error: "; break; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + case DataType::STATE_TEMPLATE_LIST: + infoStr = "Retrieved a StateTemplateList of size "; + errStr = "Failed to retrieve the StateTemplateList from the persistency module due to an error: "; + break; +#endif default: FWE_LOG_ERROR( "Unknown data type: " + std::to_string( toUType( retrieveType ) ) ); return false; @@ -82,6 +92,19 @@ CollectionSchemeManager::retrieve( DataType retrieveType ) mDecoderManifest->copyData( protoOutput.data(), protoSize ); mProcessDecoderManifest = true; } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + // coverity[autosar_cpp14_m0_1_9_violation] - Second if-statement always follows same path as first + // coverity[misra_cpp_2008_rule_0_1_9_violation] - Second if-statement always follows same path as first + else if ( retrieveType == DataType::STATE_TEMPLATE_LIST ) + { + if ( mLastKnownStateIngestion == nullptr ) + { + mLastKnownStateIngestion = std::make_shared(); + } + mLastKnownStateIngestion->copyData( protoOutput.data(), protoSize ); + mProcessStateTemplates = true; + } +#endif return true; } @@ -107,6 +130,7 @@ CollectionSchemeManager::store( DataType storeType ) FWE_LOG_ERROR( "Invalid DecoderManifest" ); return; } + switch ( storeType ) { case DataType::COLLECTION_SCHEME_LIST: @@ -117,6 +141,47 @@ CollectionSchemeManager::store( DataType storeType ) protoInput = mDecoderManifest->getData(); logStr = "The DecoderManifest"; break; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + case DataType::STATE_TEMPLATE_LIST: { + // Different from the other data, we can't just store mLastKnownStateIngestion->getData() + // because it is just a diff of the previous ingested messages. + // So we need to reconstruct a protobuf with all state templates. + Schemas::LastKnownState::StateTemplates protoStateTemplates; + protoStateTemplates.set_version( mLastStateTemplatesDiffVersion ); + for ( auto &stateTemplate : mStateTemplates ) + { + protoStateTemplates.set_decoder_manifest_sync_id( stateTemplate.second->decoderManifestID ); + auto *protoStateTemplate = protoStateTemplates.add_state_templates_to_add(); + protoStateTemplate->set_state_template_sync_id( stateTemplate.first ); + for ( auto &signal : stateTemplate.second->signals ) + { + protoStateTemplate->add_signal_ids( signal.signalID ); + } + switch ( stateTemplate.second->updateStrategy ) + { + case LastKnownStateUpdateStrategy::PERIODIC: { + auto periodicUpdateStrategy = std::make_unique(); + periodicUpdateStrategy->set_period_ms( stateTemplate.second->periodMs ); + protoStateTemplate->set_allocated_periodic_update_strategy( periodicUpdateStrategy.release() ); + break; + } + case LastKnownStateUpdateStrategy::ON_CHANGE: { + auto onChangeUpdateStrategy = std::make_unique(); + protoStateTemplate->set_allocated_on_change_update_strategy( onChangeUpdateStrategy.release() ); + break; + } + } + } + protoInput = std::vector( protoStateTemplates.ByteSizeLong() ); + if ( !protoStateTemplates.SerializeToArray( protoInput.data(), static_cast( protoInput.capacity() ) ) ) + { + FWE_LOG_ERROR( "Failed to serialize StateTemplateList" ); + return; + } + logStr = "The StateTemplateList"; + break; + } +#endif default: FWE_LOG_ERROR( "cannot store unsupported type of " + std::to_string( toUType( storeType ) ) ); return; diff --git a/src/RateLimiter.cpp b/src/RateLimiter.cpp new file mode 100644 index 00000000..20bc0688 --- /dev/null +++ b/src/RateLimiter.cpp @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "RateLimiter.h" +#include "Clock.h" + +namespace Aws +{ +namespace IoTFleetWise +{ + +RateLimiter::RateLimiter() + : mMaxTokens( DEFAULT_MAX_TOKENS ) + , mTokenRefillsPerSecond( DEFAULT_TOKEN_REFILLS_PER_SECOND ) + , mCurrentTokens( DEFAULT_MAX_TOKENS ) + , mLastRefillTime( mClock->timeSinceEpoch().monotonicTimeMs ) +{ +} + +RateLimiter::RateLimiter( uint32_t maxTokens, uint32_t tokenRefillsPerSecond ) + : mMaxTokens( maxTokens ) + , mTokenRefillsPerSecond( tokenRefillsPerSecond ) + , mCurrentTokens( maxTokens ) + , mLastRefillTime( mClock->timeSinceEpoch().monotonicTimeMs ) +{ +} + +bool +RateLimiter::consumeToken() +{ + refillTokens(); + if ( mCurrentTokens > 0 ) + { + --mCurrentTokens; + return true; + } + return false; +} + +void +RateLimiter::refillTokens() +{ + auto currTime = mClock->timeSinceEpoch().monotonicTimeMs; + auto secondsElapsed = ( currTime - mLastRefillTime ) / 1000; + if ( secondsElapsed > 0 ) + { + auto newTokens = secondsElapsed * mTokenRefillsPerSecond; + mCurrentTokens = newTokens >= mMaxTokens ? mMaxTokens : static_cast( newTokens ); + mLastRefillTime = currTime; + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/RateLimiter.h b/src/RateLimiter.h new file mode 100644 index 00000000..3ff1d3ea --- /dev/null +++ b/src/RateLimiter.h @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "TimeTypes.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +static constexpr std::uint32_t DEFAULT_MAX_TOKENS = 100; +static constexpr std::uint32_t DEFAULT_TOKEN_REFILLS_PER_SECOND = DEFAULT_MAX_TOKENS; + +class RateLimiter +{ +public: + RateLimiter(); + RateLimiter( uint32_t maxTokens, uint32_t tokenRefillsPerSecond ); + bool consumeToken(); + +private: + void refillTokens(); + + std::shared_ptr mClock = ClockHandler::getClock(); + + uint32_t mMaxTokens; + uint32_t mTokenRefillsPerSecond; + uint32_t mCurrentTokens; + Timestamp mLastRefillTime; +}; +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/RemoteDiagnosticDataSource.cpp b/src/RemoteDiagnosticDataSource.cpp new file mode 100644 index 00000000..cd8e97a2 --- /dev/null +++ b/src/RemoteDiagnosticDataSource.cpp @@ -0,0 +1,776 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "RemoteDiagnosticDataSource.h" +#include "LoggingModule.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +RemoteDiagnosticDataSource::RemoteDiagnosticDataSource( std::shared_ptr namedSignalDataSource, + std::shared_ptr rawDataBufferManager, + std::shared_ptr diagnosticInterface ) + : mNamedSignalDataSource( std::move( namedSignalDataSource ) ) + , mRawBufferManager( std::move( rawDataBufferManager ) ) + , mDiagnosticInterface( std::move( diagnosticInterface ) ) +{ +} + +void +RemoteDiagnosticDataSource::pushSnapshotJsonToRawDataBufferManager( const std::string &signalName, + FetchRequestID fetchRequestID, + const std::string &jsonString ) +{ + if ( ( mRawBufferManager == nullptr ) || ( mNamedSignalDataSource == nullptr ) ) + { + FWE_LOG_WARN( "Raw message for signal name " + signalName + " can not be handed over to RawBufferManager" ); + return; + } + + auto signalID = mNamedSignalDataSource->getNamedSignalID( signalName ); + if ( signalID == INVALID_SIGNAL_ID ) + { + FWE_LOG_TRACE( "No decoding rules set for signal name " + signalName ); + return; + } + + auto receiveTime = mClock->systemTimeSinceEpochMs(); + std::vector buffer( jsonString.begin(), jsonString.end() ); + auto bufferHandle = mRawBufferManager->push( ( buffer.data() ), buffer.size(), receiveTime, signalID ); + + if ( bufferHandle == RawData::INVALID_BUFFER_HANDLE ) + { + FWE_LOG_WARN( "Raw message id: " + std::to_string( signalID ) + " was rejected by RawBufferManager" ); + return; + } + // immediately set usage hint so buffer handle does not get directly deleted again + mRawBufferManager->increaseHandleUsageHint( + signalID, bufferHandle, RawData::BufferHandleUsageStage::COLLECTED_NOT_IN_HISTORY_BUFFER ); + + mNamedSignalDataSource->ingestSignalValue( + receiveTime, signalName, DecodedSignalValue{ bufferHandle, SignalType::STRING }, fetchRequestID ); +} + +bool +RemoteDiagnosticDataSource::start() +{ + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( false ); + if ( !mThread.create( doWork, this ) ) + { + FWE_LOG_TRACE( "Remote Diagnostics Module Thread failed to start" ); + } + else + { + FWE_LOG_TRACE( "Remote Diagnostics Module Thread started" ); + mThread.setThreadName( "fwDIUDSRD" ); + } + + return mThread.isActive() && mThread.isValid(); +} + +bool +RemoteDiagnosticDataSource::stop() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return true; + } + + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( true, std::memory_order_relaxed ); + mWait.notify(); + + FWE_LOG_TRACE( "Remote Diagnostics Module Thread requested to stop" ); + mThread.release(); + + mShouldStop.store( false, std::memory_order_relaxed ); + FWE_LOG_TRACE( "Thread stopped" ); + return !mThread.isActive(); +} + +bool +RemoteDiagnosticDataSource::shouldStop() const +{ + return mShouldStop.load( std::memory_order_relaxed ); +} + +bool +RemoteDiagnosticDataSource::isAlive() +{ + if ( ( !mThread.isValid() ) || ( !mThread.isActive() ) ) + { + return false; + } + + return true; +} + +void +RemoteDiagnosticDataSource::doWork( void *data ) +{ + RemoteDiagnosticDataSource *remoteDiagnosticDataSource = static_cast( data ); + + while ( !remoteDiagnosticDataSource->shouldStop() ) + { + remoteDiagnosticDataSource->mWait.wait( Signal::WaitWithPredicate ); + + std::lock_guard lock( remoteDiagnosticDataSource->mQueryMapMutex ); + auto it = remoteDiagnosticDataSource->mQueuedDTCQueries.begin(); + while ( it != remoteDiagnosticDataSource->mQueuedDTCQueries.end() ) + { + if ( it->second.pendingQueries == 0 ) + { + // Create Json + if ( !it->second.queryResults.empty() ) + { + Json::Value root = convertDataToJson( it->second.queryResults ); + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + + std::string jsonString = Json::writeString( builder, root ); + + FWE_LOG_TRACE( "Retrieved DTCs and Snapshot: " + jsonString ); + remoteDiagnosticDataSource->pushSnapshotJsonToRawDataBufferManager( + it->second.signalName, it->second.fetchRequestID, jsonString ); + } + auto queryID = it->first; + it = remoteDiagnosticDataSource->mQueuedDTCQueries.erase( it ); + // Erase from all maps + remoteDiagnosticDataSource->mQueuedDTCQueries.erase( queryID ); + } + else + { + ++it; + } + } + } +} + +FetchErrorCode +RemoteDiagnosticDataSource::DTC_QUERY( SignalID receivedSignalID, + FetchRequestID fetchRequestID, + const std::vector ¶ms ) +{ + if ( ( shouldStop() ) || ( !isAlive() ) ) + { + // Don't process response if thread is shutting down + return FetchErrorCode::SIGNAL_NOT_FOUND; + } + + if ( ( mNamedSignalDataSource == nullptr ) || ( mDiagnosticInterface == nullptr ) ) + { + FWE_LOG_ERROR( "DTC Query request cannot be processed" ); + return FetchErrorCode::SIGNAL_NOT_FOUND; + } + + UdsQueryData udsQuery; + UdsQueryRequestParameters udsQueryRequestParameters; + + bool found = false; + for ( const auto &it : mSignalNames ) + { + auto signalID = mNamedSignalDataSource->getNamedSignalID( it ); + if ( ( signalID == INVALID_SIGNAL_ID ) || ( signalID != receivedSignalID ) ) + { + continue; + } + udsQuery.signalName = it; + udsQuery.fetchRequestID = fetchRequestID; + found = true; + break; + } + + if ( !found ) + { + FWE_LOG_TRACE( "No decoding rules set for signal id " + std::to_string( receivedSignalID ) ); + return FetchErrorCode::SIGNAL_NOT_FOUND; + } + + // Ensure the program receives a minimum of three parameters to proceed with request processing; + // otherwise, terminate further execution. + if ( params.size() < 3 ) + { + FWE_LOG_ERROR( "Invalid parameters received." ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + + // Parse incoming request parameters + // Parse target address + if ( !params[0].isBoolOrDouble() ) + { + FWE_LOG_ERROR( "Target address parameter must be of type double" ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + udsQueryRequestParameters.ecuID = static_cast( params[0].asDouble() ); + + // Parse subfunction + if ( !params[1].isBoolOrDouble() ) + { + FWE_LOG_ERROR( "Subfunction parameter must be of type double" ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + int subFnInt = static_cast( params[1].asDouble() ); + if ( !params[2].isBoolOrDouble() ) + { + FWE_LOG_ERROR( "Status mask parameter must be of type double" ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + if ( ( subFnInt < static_cast( UDSSubFunction::NO_DTC_BY_STATUS_MASK ) ) || + ( subFnInt > static_cast( UDSSubFunction::USER_DEF_MR_DTC_EXT_DATA_REC_BY_DTC ) ) ) + { + FWE_LOG_ERROR( "Invalid parameters received. UDS subfunction parameter is out of range." ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + udsQueryRequestParameters.subFn = static_cast( subFnInt ); + + // Parse status mask + int stMaskInt = static_cast( params[2].asDouble() ); + if ( ( ( stMaskInt <= 0 ) && ( stMaskInt != -1 ) ) || ( stMaskInt >= 0xFF ) ) + { + FWE_LOG_ERROR( "Invalid parameters received. UDS status mask parameter is out of range." ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + if ( stMaskInt > -1 ) + { + // coverity[autosar_cpp14_a7_2_1_violation] false-positive, check is done above, values are inside of range + udsQueryRequestParameters.stMask = static_cast( stMaskInt ); + } + + // Parse DTC + if ( params.size() >= 4 ) + { + if ( !params[3].isString() ) + { + FWE_LOG_ERROR( "Invalid DTC code received. Parameter should be of type string." ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + std::string dtcString = *params[3].stringVal; + if ( ( !dtcString.empty() ) && ( dtcString != "-1" ) ) + { + try + { + udsQueryRequestParameters.dtc = std::stoi( dtcString, nullptr, 0 ); + } + catch ( ... ) + { + FWE_LOG_ERROR( "Could not convert received DTC code parameter to int " + dtcString ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + } + } + + // Parse record number + if ( params.size() >= 5 ) + { + if ( !params[4].isBoolOrDouble() ) + { + FWE_LOG_ERROR( "Record number must be of type double" ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + udsQueryRequestParameters.recordNumber = static_cast( params[4].asDouble() ); + } + + std::stringstream ss; + ss << std::uppercase << std::hex << udsQueryRequestParameters.dtc; + FWE_LOG_INFO( "Received Parameters are: Target Address: " + std::to_string( udsQueryRequestParameters.ecuID ) + + " subfn: " + std::to_string( static_cast( udsQueryRequestParameters.subFn ) ) + + ",stMask: " + std::to_string( static_cast( udsQueryRequestParameters.stMask ) ) + " dtc " + + ss.str() + " recordNumber " + std::to_string( udsQueryRequestParameters.recordNumber ) ); + + std::string udsQueryID = generateRandomString( 24 ); + + std::lock_guard lock( mQueryMapMutex ); + mQueuedDTCQueries.emplace( udsQueryID, udsQuery ); + mQueryRequestParameters.emplace( udsQueryID, udsQueryRequestParameters ); + mQueryLookup.emplace( udsQueryID, udsQueryID ); + + if ( udsQueryRequestParameters.subFn == UDSSubFunction::DTC_BY_STATUS_MASK ) + { + return processDtcQueryRequest( udsQueryID, udsQueryRequestParameters ); + } + else if ( ( udsQueryRequestParameters.subFn == UDSSubFunction::DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER ) || + ( udsQueryRequestParameters.subFn == UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ) ) + { + // If DTC code is not given, first query DTC codes + if ( udsQueryRequestParameters.dtc <= 0 ) + { + UdsQueryRequestParameters sequentialRequestParameters = udsQueryRequestParameters; + sequentialRequestParameters.subFn = UDSSubFunction::DTC_BY_STATUS_MASK; + return processDtcQueryRequest( udsQueryID, sequentialRequestParameters ); + } + + // If record number is not given, query for it + if ( udsQueryRequestParameters.recordNumber <= 0 ) + { + UdsQueryRequestParameters sequentialRequestParameters = udsQueryRequestParameters; + sequentialRequestParameters.subFn = UDSSubFunction::DTC_SNAPSHOT_IDENTIFICATION; + return processDtcQueryRequest( udsQueryID, sequentialRequestParameters ); + } + + if ( udsQueryRequestParameters.ecuID == -1 ) + { + FWE_LOG_ERROR( "Unsupported target address format encountered: Unable to process all address(-1) " + "as specified" ); + return FetchErrorCode::UNSUPPORTED_PARAMETERS; + } + + return processDtcSnapshotQueryRequest( udsQueryID, udsQueryRequestParameters ); + } + return FetchErrorCode::NOT_IMPLEMENTED; +} + +FetchErrorCode +RemoteDiagnosticDataSource::processDtcQueryRequest( const std::string &parentQueryID, + const UdsQueryRequestParameters &requestParameters ) +{ + std::string newQueryID = generateRandomString( 24 ); + FWE_LOG_TRACE( "Sending DTC query request " + newQueryID + " for original request " + parentQueryID + + " for target address " + std::to_string( requestParameters.ecuID ) + ", subfunction " + + std::to_string( static_cast( requestParameters.subFn ) ) ) + + ", status mask " + std::to_string( static_cast( requestParameters.stMask ) ); + + mQueuedDTCQueries[parentQueryID].pendingQueries += 1; + mQueryLookup.emplace( newQueryID, parentQueryID ); + mQueryRequestParameters.emplace( newQueryID, requestParameters ); + + mDiagnosticInterface->readDTCInfo( + requestParameters.ecuID, + requestParameters.subFn, + requestParameters.stMask, + std::bind( &RemoteDiagnosticDataSource::processUDSQueryResponse, this, std::placeholders::_1 ), + newQueryID ); + return FetchErrorCode::SUCCESSFUL; +} + +FetchErrorCode +RemoteDiagnosticDataSource::processDtcSnapshotQueryRequest( const std::string &parentQueryID, + const UdsQueryRequestParameters &requestParameters ) +{ + std::string newQueryID = generateRandomString( 24 ); + FWE_LOG_TRACE( "Sending DTC snapshot query request " + newQueryID + " for original request " + parentQueryID + + " for target address " + std::to_string( requestParameters.ecuID ) + ", subfunction " + + std::to_string( static_cast( requestParameters.subFn ) ) + ", dtc code " + + std::to_string( requestParameters.dtc ) + ", recordNumber " + + std::to_string( requestParameters.recordNumber ) ); + + mQueuedDTCQueries[parentQueryID].pendingQueries += 1; + mQueryLookup.emplace( newQueryID, parentQueryID ); + mQueryRequestParameters.emplace( newQueryID, requestParameters ); + + mDiagnosticInterface->readDTCInfoByDTCAndRecordNumber( + requestParameters.ecuID, + requestParameters.subFn, + static_cast( requestParameters.dtc ), + static_cast( requestParameters.recordNumber ), + std::bind( &RemoteDiagnosticDataSource::processUDSQueryResponse, this, std::placeholders::_1 ), + newQueryID.c_str() ); + return FetchErrorCode::SUCCESSFUL; +} + +void +RemoteDiagnosticDataSource::processUDSQueryResponse( const DTCResponse &response ) +{ + if ( ( shouldStop() ) || ( !isAlive() ) ) + { + // Don't process response if thread is shutting down + return; + } + std::string receivedQueryID = std::string( response.token ); + FWE_LOG_TRACE( "Received Response for hash: " + receivedQueryID ); + std::lock_guard lock( mQueryMapMutex ); + auto requestParametersIt = mQueryRequestParameters.find( receivedQueryID ); + auto originalRequestIDIt = mQueryLookup.find( receivedQueryID ); + + if ( requestParametersIt == mQueryRequestParameters.end() || originalRequestIDIt == mQueryLookup.end() ) + { + FWE_LOG_ERROR( "No associated request found for the received hash " + receivedQueryID ); + return; + } + auto originalRequestID = originalRequestIDIt->second; + auto dtcQuery = mQueuedDTCQueries.find( originalRequestID ); + if ( dtcQuery == mQueuedDTCQueries.end() ) + { + FWE_LOG_ERROR( "Original request id " + originalRequestID + "does not exist for the received hash " + + receivedQueryID ); + removeQuery( originalRequestID ); + removeQuery( receivedQueryID ); + return; + } + dtcQuery->second.pendingQueries -= 1; + + auto requestParameters = requestParametersIt->second; + if ( response.result < 0 ) + { + FWE_LOG_ERROR( "Diagnostics interface returned an error with the code " + std::to_string( response.result ) + + " for " + receivedQueryID + " hash" ); + removeQuery( originalRequestID ); + removeQuery( receivedQueryID ); + return; + } + if ( response.dtcInfo.empty() ) + { + FWE_LOG_ERROR( "Received an empty response from diagnostics interface for " + receivedQueryID + " hash" ); + removeQuery( originalRequestID ); + removeQuery( receivedQueryID ); + return; + } + for ( auto &dtcInfoEntry : response.dtcInfo ) + { + if ( ( !dtcInfoEntry.dtcBuffer.empty() ) && + ( ( requestParameters.ecuID == -1 ) || ( dtcInfoEntry.targetAddress == requestParameters.ecuID ) ) ) + { + // Extract raw data from the response + FWE_LOG_TRACE( "Received " + std::to_string( dtcInfoEntry.dtcBuffer.size() ) + + " bytes for target address " + std::to_string( dtcInfoEntry.targetAddress ) + ": " + + getStringFromBytes( dtcInfoEntry.dtcBuffer ) ); + processRawDTCQueryResults( + dtcInfoEntry.targetAddress, dtcInfoEntry.dtcBuffer, requestParameters, dtcQuery->second.queryResults ); + } + } + // Execute sequential requests if required + auto originalRequestParameters = mQueryRequestParameters.find( originalRequestID ); + if ( originalRequestParameters == mQueryRequestParameters.end() ) + { + FWE_LOG_ERROR( "No request parameter found for the original request with hash " + originalRequestID ); + removeQuery( originalRequestID ); + removeQuery( receivedQueryID ); + return; + } + // Delete sequential query from the lists since it's now fully processed + removeQuery( receivedQueryID ); + if ( ( originalRequestParameters->second.subFn == UDSSubFunction::DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER ) || + ( originalRequestParameters->second.subFn == UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ) ) + { + if ( requestParameters.subFn == UDSSubFunction::DTC_BY_STATUS_MASK ) + { + // Request record id if DTCs were queried before + UdsQueryRequestParameters sequentialRequestParameters = requestParameters; + sequentialRequestParameters.subFn = UDSSubFunction::DTC_SNAPSHOT_IDENTIFICATION; + + processDtcQueryRequest( originalRequestID, sequentialRequestParameters ); + return; + } + + if ( requestParameters.subFn == UDSSubFunction::DTC_SNAPSHOT_IDENTIFICATION ) + { + for ( auto &queryResult : dtcQuery->second.queryResults ) + { + for ( auto &capturedDTC : queryResult.capturedDTCData ) + { + if ( capturedDTC.recordID <= 0 ) + { + // Skip request if no record ID exist + continue; + } + // Request snapshot data if record id was queried before + UdsQueryRequestParameters sequentialRequestParameters = requestParameters; + // Query the original request subfunction (snapshot data or extended data) + sequentialRequestParameters.subFn = originalRequestParameters->second.subFn; + sequentialRequestParameters.ecuID = queryResult.ecuID; + sequentialRequestParameters.dtc = static_cast( capturedDTC.dtc ); + sequentialRequestParameters.recordNumber = capturedDTC.recordID; + + processDtcSnapshotQueryRequest( originalRequestID, sequentialRequestParameters ); + } + } + } + } + mWait.notify(); +} + +void +RemoteDiagnosticDataSource::processRawDTCQueryResults( const int ecuID, + const std::vector &rawDTC, + const UdsQueryRequestParameters &queryParameters, + std::vector &queryResults ) +{ + // Define a named lambda here to comply with AUTOSAR A5-1-9 + auto findQuery = [&]( UdsDtcInfo &it ) -> bool { + return it.ecuID == ecuID; + }; + + if ( queryParameters.subFn == UDSSubFunction::DTC_BY_STATUS_MASK ) + { + UdsDtcInfo queryResult; + /* According to ISO 14229-1 Table-272, DTC retrieval involves incrementing by 4. + The response format of DTC_BY_STATUS_MASK is as follows: + +---------------------------------------------+ + | SID | reportType | DTCSAM | DTCASR | ..... | + +---------------------------------------------+ + Since rawDTC contains DTCSAM (DTCStatusAvailabilityMask) followed + by + DTCASR (DTCAndStatusRecord[]), where the structure of DTCAndStatusRecord[] is: + DTCHighByte#N DTCMiddleByte#N DTCLowByte#N StatusOfDTC#N As we are only retrieving DTCs, + we ignore StatusOfDTC#N. Therefore, we start the index at 1 and increment by +4. */ + if ( !extractUint8Value( rawDTC, 0, queryResult.statusAvailabilityMask ) ) + { + return; + } + for ( size_t j = 1; j < rawDTC.size(); j += 4 ) + { + uint32_t dtc = 0; + if ( !extractDtc( rawDTC, j, dtc ) ) + { + break; + } + if ( ( ( queryParameters.dtc <= 0 ) || ( static_cast( queryParameters.dtc ) == dtc ) ) && + ( dtc != 0 ) ) + { + UdsDtcAndSnapshot udsDtcAndSnapshot; + udsDtcAndSnapshot.dtc = dtc; + udsDtcAndSnapshot.recordID = queryParameters.recordNumber; + queryResult.ecuID = ecuID; + queryResult.capturedDTCData.emplace_back( udsDtcAndSnapshot ); + } + } + if ( !queryResult.capturedDTCData.empty() ) + { + queryResults.emplace_back( queryResult ); + } + } + else if ( queryParameters.subFn == UDSSubFunction::DTC_SNAPSHOT_IDENTIFICATION ) + { + /* According to ISO 14229-1 Table-272, DTC retrieval involves incrementing by 4. + The response format of DTC_SNAPSHOT_IDENTIFICATION is as follows: + +------------------------------------------------------+ + | SID | DTCRecord[] | DTCSnapshotRecordNumber | ..... | + +------------------------------------------------------+ + Since rawDTC contains DTCASR_ (DTCRecord) followed by + DTCSSRN (DTCSnapshotRecordNumber), As we are only retrieving DTC RecrodNumber, + we ignore DTCASR_#N. Therefore, we start the index at 1 and increment by +4. + */ + for ( size_t j = 0; j < rawDTC.size(); j += 4 ) + { + uint32_t dtc = 0; + if ( !extractDtc( rawDTC, j, dtc ) ) + { + break; + } + uint8_t record = 0; + if ( !extractUint8Value( rawDTC, j + 3, record ) ) + { + break; + } + + auto queryResult = std::find_if( queryResults.begin(), queryResults.end(), findQuery ); + if ( queryResult == queryResults.end() ) + { + if ( ( static_cast( queryParameters.dtc ) == dtc ) && + ( ( queryParameters.ecuID == ecuID ) || ( queryParameters.ecuID == -1 ) ) ) + { + UdsDtcInfo newQueryResult; + UdsDtcAndSnapshot udsDtcAndSnapshot; + udsDtcAndSnapshot.dtc = dtc; + udsDtcAndSnapshot.recordID = record; + newQueryResult.ecuID = ecuID; + newQueryResult.capturedDTCData.emplace_back( udsDtcAndSnapshot ); + queryResults.emplace_back( newQueryResult ); + } + return; + } + + for ( auto &capturedDTCData : queryResult->capturedDTCData ) + { + if ( capturedDTCData.dtc == dtc ) + { + capturedDTCData.recordID = record; + break; + } + } + } + } + else if ( ( queryParameters.subFn == UDSSubFunction::DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER ) || + ( queryParameters.subFn == UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ) ) + { + uint32_t dtc = 0; + if ( !extractDtc( rawDTC, 0, dtc ) ) + { + return; + } + std::string dtcStr = toHexString( dtc, 6 ); + FWE_LOG_INFO( "Snapshot received for " + dtcStr + " DTC" ); + + auto queryResult = std::find_if( queryResults.begin(), queryResults.end(), findQuery ); + if ( queryResult == queryResults.end() ) + { + if ( ( static_cast( queryParameters.dtc ) == dtc ) && + ( ( queryParameters.ecuID == ecuID ) || ( queryParameters.ecuID == -1 ) ) ) + { + UdsDtcInfo newQueryResult; + UdsDtcAndSnapshot udsDtcAndSnapshot; + udsDtcAndSnapshot.dtc = dtc; + udsDtcAndSnapshot.recordID = queryParameters.recordNumber; + newQueryResult.ecuID = ecuID; + + if ( queryParameters.subFn == UDSSubFunction::DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER ) + { + convertBytesToString( rawDTC, udsDtcAndSnapshot.snapshot ); + } + if ( queryParameters.subFn == UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ) + { + convertBytesToString( rawDTC, udsDtcAndSnapshot.extendedData ); + } + + newQueryResult.capturedDTCData.emplace_back( udsDtcAndSnapshot ); + queryResults.emplace_back( newQueryResult ); + } + return; + } + + for ( auto &dtcAndSnapshot : queryResult->capturedDTCData ) + { + if ( dtcAndSnapshot.dtc == dtc ) + { + if ( queryParameters.subFn == UDSSubFunction::DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER ) + { + convertBytesToString( rawDTC, dtcAndSnapshot.snapshot ); + } + if ( queryParameters.subFn == UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ) + { + convertBytesToString( rawDTC, dtcAndSnapshot.extendedData ); + } + break; + } + } + } +} + +std::string +RemoteDiagnosticDataSource::generateRandomString( int length ) +{ + const std::string ASCII_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + std::random_device randomStr; + std::mt19937 generator( randomStr() ); + std::uniform_int_distribution<> distribution( 0, static_cast( ASCII_CHARACTERS.size() - 1 ) ); + + std::string random_string; + for ( int i = 0; i < length; ++i ) + { + // Use size_t or std::string::size_type for indexing + // coverity[cert_str53_cpp_violation] False-positive, distribution is limited by the string size + random_string += ASCII_CHARACTERS[static_cast( distribution( generator ) )]; + } + return random_string; +} + +bool +RemoteDiagnosticDataSource::extractDtc( const std::vector &buffer, size_t index, uint32_t &result ) +{ + if ( index + 3 > buffer.size() ) + { + FWE_LOG_TRACE( "Received DTC info of invalid size" ); + return false; + } + result = ( static_cast( buffer[index] ) << 16 ) | ( static_cast( buffer[index + 1] ) << 8 ) | + ( static_cast( buffer[index + 2] ) ); + return true; +} + +bool +RemoteDiagnosticDataSource::extractUint8Value( const std::vector &buffer, size_t index, uint8_t &result ) +{ + if ( index >= buffer.size() ) + { + FWE_LOG_TRACE( "Received DTC info of invalid size" ); + return false; + } + result = buffer[index]; + return true; +} + +void +RemoteDiagnosticDataSource::convertBytesToString( const std::vector &bytes, std::string &byteString ) +{ + std::stringstream ss; + ss << std::hex << std::uppercase << std::setfill( '0' ); + for ( size_t i = 0; i < bytes.size(); ++i ) + { + ss << std::setw( 2 ) << static_cast( bytes[i] ); + } + byteString.assign( ss.str() ); +} + +std::string +RemoteDiagnosticDataSource::toHexString( uint32_t value, int width ) +{ + std::stringstream ss; + ss << std::hex << std::uppercase << std::setfill( '0' ) << std::setw( width ) << value; + return ss.str(); +} + +Json::Value +RemoteDiagnosticDataSource::convertDataToJson( const std::vector &queryResults ) +{ + Json::Value root; + Json::Value dataArray( Json::arrayValue ); + for ( auto &queryResult : queryResults ) + { + Json::Value dtcCodes( Json::arrayValue ); + + for ( auto &capturedDTC : queryResult.capturedDTCData ) + { + Json::Value dtc; + dtc["DTC"] = ""; + dtc["DTCSnapshotRecord"] = ""; + dtc["DTCExtendedData"] = ""; + + if ( capturedDTC.dtc != 0U ) + { + dtc["DTC"] = toHexString( capturedDTC.dtc, 6 ); + } + if ( !capturedDTC.snapshot.empty() ) + { + dtc["DTCSnapshotRecord"] = capturedDTC.snapshot; + } + if ( !capturedDTC.extendedData.empty() ) + { + dtc["DTCExtendedData"] = capturedDTC.extendedData; + } + dtcCodes.append( dtc ); + } + + Json::Value dtcSnapshot; + dtcSnapshot["DTCStatusAvailabilityMask"] = toHexString( queryResult.statusAvailabilityMask, 2 ); + dtcSnapshot["dtcCodes"] = dtcCodes; + Json::Value element; + element["ECUID"] = toHexString( static_cast( queryResult.ecuID ), 2 ); + element["DTCAndSnapshot"] = dtcSnapshot; + + dataArray.append( element ); + } + + root["DetectedDTCs"] = dataArray; + + return root; +} + +void +RemoteDiagnosticDataSource::removeQuery( const std::string &queryID ) +{ + FWE_LOG_TRACE( "Removing query with ID " + queryID + " from pending list" ); + mQueuedDTCQueries.erase( queryID ); + mQueryRequestParameters.erase( queryID ); + mQueryLookup.erase( queryID ); +} + +RemoteDiagnosticDataSource::~RemoteDiagnosticDataSource() +{ + // To make sure the thread stops during teardown of tests. + if ( isAlive() ) + { + stop(); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/RemoteDiagnosticDataSource.h b/src/RemoteDiagnosticDataSource.h new file mode 100644 index 00000000..65d379a1 --- /dev/null +++ b/src/RemoteDiagnosticDataSource.h @@ -0,0 +1,285 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "DataFetchManagerAPITypes.h" +#include "IRemoteDiagnostics.h" +#include "NamedSignalDataSource.h" +#include "RawDataManager.h" +#include "Signal.h" +#include "SignalTypes.h" +#include "Thread.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +/* + * Uds query response data format: snapshot and code + */ +struct UdsDtcAndSnapshot +{ + uint32_t dtc{ 0 }; + int32_t recordID{ -1 }; + std::string snapshot; + std::string extendedData; +}; + +/* + * Uds query response data format. Multiple codes can be captured per ECU. + */ +struct UdsDtcInfo +{ + int32_t ecuID{ 0 }; + uint8_t statusAvailabilityMask{ 0 }; + std::vector capturedDTCData; +}; + +/* + * Uds query format submitted via DTC_QUERY_FUNCTION. One query can result in multiples sequential requests. + * When pendingQueries is 0, the query is considered complete. + */ +struct UdsQueryData +{ + FetchRequestID fetchRequestID{ DEFAULT_FETCH_REQUEST_ID }; + std::string signalName; + + std::vector queryResults; + uint32_t pendingQueries{ 0 }; +}; + +/* + * Parameters associated with each UDS query + */ +struct UdsQueryRequestParameters +{ + int32_t ecuID{ 0 }; + UDSSubFunction subFn{ UDSSubFunction::NO_DTC_BY_STATUS_MASK }; + UDSStatusMask stMask{ UDSStatusMask::CONFIRMED_DTC }; + int32_t dtc{ -1 }; + int32_t recordNumber{ -1 }; +}; + +class RemoteDiagnosticDataSource +{ +public: + RemoteDiagnosticDataSource( std::shared_ptr namedSignalDataSource, + std::shared_ptr rawDataBufferManager, + std::shared_ptr diagnosticInterface = nullptr ); + ~RemoteDiagnosticDataSource(); + + RemoteDiagnosticDataSource( const RemoteDiagnosticDataSource & ) = delete; + RemoteDiagnosticDataSource &operator=( const RemoteDiagnosticDataSource & ) = delete; + RemoteDiagnosticDataSource( RemoteDiagnosticDataSource && ) = delete; + RemoteDiagnosticDataSource &operator=( RemoteDiagnosticDataSource && ) = delete; + + /** + * @brief Executes a dtc query based on the provided UDS parameters. + * + * This function retrieves active DTCs, snapshot records, + * and extended data from the ECUs. + * + * @param receivedSignalID The ID of the signal that is being queried. + * @param fetchRequestID The ID associated with the fetch request. + * @param params A vector of inspection values used in the query. + * + * @return FetchErrorCode Indicates the success or failure of the query. + */ + FetchErrorCode DTC_QUERY( SignalID receivedSignalID, + FetchRequestID fetchRequestID, + const std::vector ¶ms ); + + // Start the thread + bool start(); + // Stop the thread + bool stop(); + + /** + * @brief Returns the health state of the cyclic thread + * @return True if successful. False otherwise. + */ + bool isAlive(); + + /** + * @brief Pushes a snapshot JSON to the raw data buffer manager. + * + * Pushes a snapshot JSON string to the raw data buffer manager for storage. + * + * @param signalName Name of the signal associated with the snapshot. + * @param fetchRequestID ID of the fetch request associated with the snapshot. + * @param jsonString Reference to a std::string containing the JSON data. + */ + void pushSnapshotJsonToRawDataBufferManager( const std::string &signalName, + FetchRequestID fetchRequestID, + const std::string &jsonString ); + + /** + * @brief Processes the response to an UDS interface query. + * + * This function handles the response received asynchronously from an UDS interface. + * + * @param response Structure containing the asynchronous response data. + * It includes information such as error codes, diagnostic data, and status. + * This parameter must not be modified by the caller. + * Ensure that the memory associated with `resp` is not freed during the function call. + */ + void processUDSQueryResponse( const DTCResponse &response ); + +private: + std::shared_ptr mClock = ClockHandler::getClock(); + + // Intercepts stop signals. + bool shouldStop() const; + // Main worker function. The following operations are coded by the function + static void doWork( void *data ); + + /** + * @brief Converts data to JSON format. + * + * Converts the provided vector of UdsDtcInfo objects to JSON format using Json::Value. + * + * @param queryResults Vector of UdsDtcInfo objects to convert to JSON. + * @return Json::Value containing the converted JSON data. + */ + static Json::Value convertDataToJson( const std::vector &queryResults ); + + /** + * @brief Converts a number to a hexadecimal string representation. + * + * @param value a number to convert + * @param width width of the hexadecimal string + * @return std::string A string containing the hexadecimal representation of the input number. + * + */ + static std::string toHexString( uint32_t value, int width ); + + /** + * @brief Generates a random string of specified length. + * + * Generates a random string of the specified length using alphanumeric characters. + * + * @param length Length of the random string to generate. + * @return The generated random string. + */ + static std::string generateRandomString( int length ); + /** + * @brief Processes a DTC query request. + * + * Processes a DTC query request with the given parameters and returns an error code. + * + * @param parentQueryID Hash string if the original query + * @param requestParameters Request parameters incl ecuID, subfunction, status mask, dtc and recordID + * @return FetchErrorCode indicating the result of the query. + */ + FetchErrorCode processDtcQueryRequest( const std::string &parentQueryID, + const UdsQueryRequestParameters &requestParameters ); + /** + * @brief Processes a DTC snapshot query request. + * + * Processes a DTC snapshot query request with the given parameters and returns an error code. + * + * @param parentQueryID Hash string of the original query. + * @param requestParameters Request parameters incl ecuID, subfunction, status mask, dtc and recordID + * @return FetchErrorCode indicating the result of the query. + */ + FetchErrorCode processDtcSnapshotQueryRequest( const std::string &parentQueryID, + const UdsQueryRequestParameters &requestParameters ); + + /** + * @brief Converts byte data into a string representation. + * + * Converts a vector of bytes to a string and stores the result in byteString. + * Overloaded to support both array and vector input. + * + * @param bytes Reference to a std::vector containing bytes. + * @param byteString Reference to a std::string where the result is stored. + */ + static void convertBytesToString( const std::vector &bytes, std::string &byteString ); + + /** + * @brief Processes raw DTC query results received from the remote diagnostics interface. + * + * This function takes raw DTC query results and processes them into a structured format. + * It handles the conversion of raw data into UdsDtcInfo objects depending on requested subfunction, which represent + * Diagnostic Trouble Codes (DTCs) and their associated information. + * + * @param ecuID Received ECU ID + * @param rawDTC A vector of bytes containing the raw DTC query results. + * @param queryParameters Request parameters incl ecuID, subfunction, status mask, dtc and recordID + * @param queryResults Reference to a vector of UdsDtcInfo objects where the processed results are stored. + * + */ + static void processRawDTCQueryResults( const int32_t ecuID, + const std::vector &rawDTC, + const UdsQueryRequestParameters &queryParameters, + std::vector &queryResults ); + + /** + * @brief Extracts an 8-bit unsigned integer value from a byte buffer. + * + * @param buffer The source vector of bytes. + * @param index The position in the buffer to extract the value from. + * @param result Reference to store the extracted value. + * @return bool True if extraction was successful, false otherwise. + */ + static bool extractUint8Value( const std::vector &buffer, size_t index, uint8_t &result ); + + /** + * @brief Extracts a Diagnostic Trouble Code (DTC) from a byte buffer. + * + * @param buffer The source vector of bytes containing the DTC. + * @param index The starting position in the buffer to extract the DTC from. + * @param result Reference to store the extracted DTC as a 32-bit unsigned integer. + * @return bool True if extraction was successful, false otherwise. + */ + static bool extractDtc( const std::vector &buffer, size_t index, uint32_t &result ); + + /** + * @brief Removes a query from the system based on its ID. + * + * This function removes a previously queued query identified by its unique queryID. + * It's used to clean up or cancel ongoing queries that are no longer needed. + * + * @param queryID A string representing the unique identifier of the query to be removed. + */ + void removeQuery( const std::string &queryID ); + + Thread mThread; + std::atomic mShouldStop{ false }; + mutable std::mutex mThreadMutex; + + Signal mWait; + + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr mRawBufferManager; + + // mutex for below maps + mutable std::mutex mQueryMapMutex; + // Map containing requests originally received through DTC_QUERY function and related information + std::unordered_map mQueuedDTCQueries; + // Map of all requests sent to remote diagnostics interface + std::unordered_map mQueryRequestParameters; + // Look-up table for sequential requests to find the original query stored in mQueuedDTCQueries + std::unordered_map mQueryLookup; + + std::vector mSignalNames{ "Vehicle.ECU1.DTC_INFO", "Vehicle.ECU2.DTC_INFO", "Vehicle.ECU3.DTC_INFO" }; + + std::shared_ptr mDiagnosticInterface; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/RemoteProfiler.cpp b/src/RemoteProfiler.cpp index b7c48503..0ef860a8 100644 --- a/src/RemoteProfiler.cpp +++ b/src/RemoteProfiler.cpp @@ -4,6 +4,7 @@ #include "RemoteProfiler.h" #include "IConnectionTypes.h" #include "LoggingModule.h" +#include "TopicConfig.h" #include "TraceModule.h" #include #include @@ -15,15 +16,13 @@ namespace Aws namespace IoTFleetWise { -RemoteProfiler::RemoteProfiler( std::shared_ptr metricsSender, - std::shared_ptr logSender, +RemoteProfiler::RemoteProfiler( std::shared_ptr sender, uint32_t initialMetricsUploadInterval, uint32_t initialLogMaxInterval, LogLevel initialLogLevelThresholdToSend, std::string profilerPrefix ) : fShouldStop( false ) - , fMetricsSender( std::move( metricsSender ) ) - , fLogSender( std::move( logSender ) ) + , mMqttSender( std::move( sender ) ) , fCurrentMetricsPending( 0 ) , fInitialUploadInterval( initialMetricsUploadInterval ) , fInitialLogMaxInterval( initialLogMaxInterval ) @@ -54,13 +53,15 @@ RemoteProfiler::sendMetricsOut() Json::StreamWriterBuilder builder; builder["indentation"] = ""; // If you want whitespace-less output const std::string output = Json::writeString( builder, fMetricsRoot ); - fMetricsSender->sendBuffer( - reinterpret_cast( output.c_str() ), output.length(), []( ConnectivityError result ) { - if ( result == ConnectivityError::Success ) - { - FWE_LOG_ERROR( "Send error " + std::to_string( static_cast( result ) ) ); - } - } ); + mMqttSender->sendBuffer( mMqttSender->getTopicConfig().metricsTopic, + reinterpret_cast( output.c_str() ), + output.length(), + []( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_ERROR( "Send error " + std::to_string( static_cast( result ) ) ); + } + } ); fMetricsRoot.clear(); fCurrentMetricsPending = 0; } @@ -80,15 +81,18 @@ RemoteProfiler::sendLogsOut() initLogStructure(); } - if ( ( fLogSender != nullptr ) ) + if ( ( mMqttSender != nullptr ) ) { - fLogSender->sendBuffer( - reinterpret_cast( output.c_str() ), output.length(), []( ConnectivityError result ) { - if ( result == ConnectivityError::Success ) - { - FWE_LOG_ERROR( "Send error " + std::to_string( static_cast( result ) ) ); - } - } ); + mMqttSender->sendBuffer( mMqttSender->getTopicConfig().logsTopic, + reinterpret_cast( output.c_str() ), + output.length(), + []( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_ERROR( "Send error " + + std::to_string( static_cast( result ) ) ); + } + } ); } } } @@ -162,7 +166,7 @@ RemoteProfiler::setMetric( const std::string &name, double value, const std::str bool RemoteProfiler::start() { - if ( fMetricsSender == nullptr ) + if ( mMqttSender == nullptr ) { FWE_LOG_ERROR( "Trying to start without sender" ); return false; diff --git a/src/RemoteProfiler.h b/src/RemoteProfiler.h index a1c4f9f3..6260aee7 100644 --- a/src/RemoteProfiler.h +++ b/src/RemoteProfiler.h @@ -40,8 +40,7 @@ class RemoteProfiler : public IMetricsReceiver, public ILogger /** * @brief Construct the RemoteProfiler which can upload metrics and logs over MQTT * - * @param metricsSender the sender that should be used to upload metrics - * @param logSender the sender that should be used to upload logs + * @param sender the sender that should be used to upload metrics and logs * @param initialMetricsUploadInterval the interval used between two metrics uploads * @param initialLogMaxInterval the max interval that logs can be cached before uploading * @param initialLogLevelThresholdToSend all logs below this threshold will be ignored @@ -49,8 +48,7 @@ class RemoteProfiler : public IMetricsReceiver, public ILogger * specific vehicles * */ - RemoteProfiler( std::shared_ptr metricsSender, - std::shared_ptr logSender, + RemoteProfiler( std::shared_ptr sender, uint32_t initialMetricsUploadInterval, uint32_t initialLogMaxInterval, LogLevel initialLogLevelThresholdToSend, @@ -140,8 +138,7 @@ class RemoteProfiler : public IMetricsReceiver, public ILogger Thread fThread; std::atomic fShouldStop; - std::shared_ptr fMetricsSender; - std::shared_ptr fLogSender; + std::shared_ptr mMqttSender; std::mutex fThreadMutex; std::mutex loggingMutex; Signal fWait; diff --git a/src/Schema.cpp b/src/Schema.cpp index 410db378..21023613 100644 --- a/src/Schema.cpp +++ b/src/Schema.cpp @@ -4,6 +4,7 @@ #include "Schema.h" #include "IConnectionTypes.h" #include "LoggingModule.h" +#include "TopicConfig.h" #include namespace Aws @@ -14,7 +15,7 @@ namespace IoTFleetWise Schema::Schema( std::shared_ptr receiverDecoderManifest, std::shared_ptr receiverCollectionSchemeList, std::shared_ptr sender ) - : mSender( std::move( sender ) ) + : mMqttSender( std::move( sender ) ) { // Register the listeners receiverDecoderManifest->subscribeToDataReceived( [this]( const ReceivedConnectivityMessage &receivedMessage ) { @@ -104,7 +105,7 @@ Schema::sendCheckin( const std::vector &documentARNs, OnCheckinSentCallb void Schema::transmitCheckin( OnCheckinSentCallback callback ) { - if ( mSender == nullptr ) + if ( mMqttSender == nullptr ) { FWE_LOG_ERROR( "Invalid sender instance" ); callback( false ); @@ -126,26 +127,28 @@ Schema::transmitCheckin( OnCheckinSentCallback callback ) } checkinDebugString += "]"; - mSender->sendBuffer( reinterpret_cast( mProtoCheckinMsgOutput.data() ), - mProtoCheckinMsgOutput.size(), - [checkinDebugString, callback]( ConnectivityError result ) { - if ( result == ConnectivityError::Success ) - { - FWE_LOG_TRACE( "Checkin Message sent to the backend" ); - FWE_LOG_TRACE( checkinDebugString ); - callback( true ); - } - else if ( result == ConnectivityError::NoConnection ) - { - FWE_LOG_TRACE( "Couldn't send checkin message because there is no connection" ); - callback( false ); - } - else - { - FWE_LOG_ERROR( "offboardconnectivity error, will retry sending the checkin message" ); - callback( false ); - } - } ); + mMqttSender->sendBuffer( mMqttSender->getTopicConfig().checkinsTopic, + reinterpret_cast( mProtoCheckinMsgOutput.data() ), + mProtoCheckinMsgOutput.size(), + [checkinDebugString, callback]( ConnectivityError result ) { + if ( result == ConnectivityError::Success ) + { + FWE_LOG_TRACE( "Checkin Message sent to the backend" ); + FWE_LOG_TRACE( checkinDebugString ); + callback( true ); + } + else if ( result == ConnectivityError::NoConnection ) + { + FWE_LOG_TRACE( "Couldn't send checkin message because there is no connection" ); + callback( false ); + } + else + { + FWE_LOG_ERROR( + "offboardconnectivity error, will retry sending the checkin message" ); + callback( false ); + } + } ); } } // namespace IoTFleetWise diff --git a/src/Schema.h b/src/Schema.h index d41fae6e..4e65ad39 100644 --- a/src/Schema.h +++ b/src/Schema.h @@ -114,7 +114,7 @@ class Schema : public SchemaListener /** * @brief ISender object used to interface with cloud to send Checkins */ - std::shared_ptr mSender; + std::shared_ptr mMqttSender; /** * @brief CheckinMsg member variable used to hold the checkin data and minimize heap fragmentation diff --git a/src/SignalTypes.h b/src/SignalTypes.h index e054e355..33c70384 100644 --- a/src/SignalTypes.h +++ b/src/SignalTypes.h @@ -48,6 +48,23 @@ using SyncID = std::string; using SignalID = uint32_t; static constexpr SignalID INVALID_SIGNAL_ID = 0; +/** + * @brief Fetch Request ID is generated for each fetch configuration provided in collection scheme. + * It remains the same through the lifecycle of campaign and is used to identify received signal values + * for the specific query. + */ +using FetchRequestID = uint8_t; +static constexpr FetchRequestID DEFAULT_FETCH_REQUEST_ID = 0xFF; + +/** + * @brief Signal buffer condition ID is a virtual id that is generated on top of condition id and fetch + * request id to correctly index signal buffers for the same campaign. If one campaign has multiple + * fetch strategies set for the same signal, data collected with these fetches should be stored in the + * same signal buffer. This behaviour is insured by SignalBufferConditionID. + */ +using SignalBufferConditionID = uint32_t; +static constexpr SignalBufferConditionID DEFAULT_SIGNAL_BUFFER_CONDITION_ID = 0xFFFFFFFF; + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA /** * If this MSB is set the SignalID is an internal ID that is only valid while the process is running. @@ -89,6 +106,7 @@ enum struct SignalType #ifdef FWE_FEATURE_VISION_SYSTEM_DATA COMPLEX_SIGNAL = 12, // internal type RawData::BufferHandle is defined as uint32 #endif + STRING = 13, // internal type RawData::BufferHandle is defined as uint32 }; /** @@ -128,6 +146,8 @@ signalTypeToString( SignalType signalType ) return "BOOLEAN"; case SignalType::UNKNOWN: return "UNKNOWN"; + case SignalType::STRING: + return "STRING"; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA case SignalType::COMPLEX_SIGNAL: return "COMPLEX_SIGNAL"; diff --git a/src/SomeipCommandDispatcher.cpp b/src/SomeipCommandDispatcher.cpp new file mode 100644 index 00000000..c63eaa8c --- /dev/null +++ b/src/SomeipCommandDispatcher.cpp @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "SomeipCommandDispatcher.h" +#include "LoggingModule.h" +#include +#include +#include +#include +#include +#include + +#if !defined( COMMONAPI_INTERNAL_COMPILATION ) +#define COMMONAPI_INTERNAL_COMPILATION +#define HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#endif +#include +#ifdef HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +// coverity[misra_cpp_2008_rule_16_0_3_violation] Required to workaround CommonAPI include mechanism +#undef COMMONAPI_INTERNAL_COMPILATION +// coverity[misra_cpp_2008_rule_16_0_3_violation] Required to workaround CommonAPI include mechanism +#undef HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#endif + +namespace Aws +{ +namespace IoTFleetWise +{ + +SomeipCommandDispatcher::SomeipCommandDispatcher( std::shared_ptr someipInterfaceWrapper ) + : mSomeipInterfaceWrapper( std::move( someipInterfaceWrapper ) ) +{ +} + +std::vector +SomeipCommandDispatcher::getActuatorNames() +{ + std::vector actuatorNames; + for ( const auto &config : mSomeipInterfaceWrapper->getSupportedActuatorInfo() ) + { + actuatorNames.push_back( config.first ); + } + return actuatorNames; +} + +bool +SomeipCommandDispatcher::init() +{ + if ( ( mSomeipInterfaceWrapper == nullptr ) || ( !mSomeipInterfaceWrapper->init() ) ) + { + FWE_LOG_ERROR( "Failed to initiate SOME/IP proxy" ); + return false; + } + + mProxy = mSomeipInterfaceWrapper->getProxy(); + + // Wait for up to 2 seconds for the proxy to become available. + // This allows a command to be received immediately after MQTT connection to be executed, + // which is what happens if the command was started while FWE was not running. + std::promise availabilityPromise; + auto availabilitySubscription = + mProxy->getProxyStatusEvent().subscribe( [&availabilityPromise]( CommonAPI::AvailabilityStatus status ) { + if ( status == CommonAPI::AvailabilityStatus::AVAILABLE ) + { + availabilityPromise.set_value(); + } + } ); + auto res = availabilityPromise.get_future().wait_for( std::chrono::seconds( 2 ) ); + mProxy->getProxyStatusEvent().unsubscribe( availabilitySubscription ); + if ( res == std::future_status::timeout ) + { + FWE_LOG_WARN( "Proxy currently unavailable" ); + // Don't return false, proxy may become available later + } + else + { + FWE_LOG_INFO( "Successfully initiated SOME/IP proxy" ); + } + return true; +} + +void +SomeipCommandDispatcher::setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) +{ + // Sanity check to ensure proxy is valid + if ( mProxy == nullptr ) + { + auto reasonDescription = "Null proxy"; + FWE_LOG_ERROR( reasonDescription ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_INTERNAL_ERROR, reasonDescription ); + return; + } + // First let's check proxy is available + if ( !mProxy->isAvailable() ) + { + std::string reasonDescription = "Proxy unavailable"; + FWE_LOG_ERROR( reasonDescription + " for actuator " + actuatorName + " and command ID " + commandId ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_UNAVAILABLE, reasonDescription ); + return; + } + // Check whether actuator is supported + auto it = mSomeipInterfaceWrapper->getSupportedActuatorInfo().find( actuatorName ); + if ( it == mSomeipInterfaceWrapper->getSupportedActuatorInfo().end() ) + { + FWE_LOG_WARN( "Actuator " + actuatorName + " not supported for command ID " + commandId ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_NOT_SUPPORTED, "" ); + return; + } + // Check whether actuator value type matches with the supported method + if ( signalValue.type != it->second.signalType ) + { + FWE_LOG_ERROR( "Actuator " + actuatorName + + "'s value type mismatches with the supported value type for command ID " + commandId ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_ARGUMENT_TYPE_MISMATCH, "" ); + return; + } + // Invoke the actual method via the method wrapper + it->second.methodWrapper( + signalValue, + commandId, + issuedTimestampMs, + executionTimeoutMs, + [commandId, notifyStatusCallback, actuatorName]( + CommandStatus status, CommandReasonCode reasonCode, const CommandReasonDescription &reasonDescription ) { + std::string msg = "Actuator " + actuatorName + " response with command ID: " + commandId + + ", status: " + commandStatusToString( status ) + + ", reason code: " + std::to_string( reasonCode ) + + ", reason description: " + reasonDescription; + if ( ( status == CommandStatus::IN_PROGRESS ) || ( status == CommandStatus::SUCCEEDED ) ) + { + FWE_LOG_INFO( msg ); + } + else + { + FWE_LOG_ERROR( msg ); + } + notifyStatusCallback( status, reasonCode, reasonDescription ); + } ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/SomeipCommandDispatcher.h b/src/SomeipCommandDispatcher.h new file mode 100644 index 00000000..ef00104d --- /dev/null +++ b/src/SomeipCommandDispatcher.h @@ -0,0 +1,98 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include "ISomeipInterfaceWrapper.h" +#include "TimeTypes.h" +#include +#include +#include +#include + +#if !defined( COMMONAPI_INTERNAL_COMPILATION ) +#define COMMONAPI_INTERNAL_COMPILATION +#define HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#endif +#include +#ifdef HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +// coverity[misra_cpp_2008_rule_16_0_3_violation] Required to workaround CommonAPI include mechanism +#undef COMMONAPI_INTERNAL_COMPILATION +// coverity[misra_cpp_2008_rule_16_0_3_violation] Required to workaround CommonAPI include mechanism +#undef HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#endif + +namespace Aws +{ +namespace IoTFleetWise +{ + +/** + * This class implements interface ICommandDispatcher. It's a generic class for dispatching command + * onto SOME/IP. It accepts a SOME/IP interface specific wrapper to dispatch command to the corresponding + * SOME/IP interface + */ +class SomeipCommandDispatcher : public ICommandDispatcher +{ +public: + SomeipCommandDispatcher( std::shared_ptr someipInterfaceWrapper ); + + ~SomeipCommandDispatcher() override = default; + + SomeipCommandDispatcher( const SomeipCommandDispatcher & ) = delete; + SomeipCommandDispatcher &operator=( const SomeipCommandDispatcher & ) = delete; + SomeipCommandDispatcher( SomeipCommandDispatcher && ) = delete; + SomeipCommandDispatcher &operator=( SomeipCommandDispatcher && ) = delete; + + /** + * @brief Initializer command dispatcher with its associated underlying vehicle network / service + * @return True if successful. False otherwise. + */ + bool init() override; + + /** + * @brief set actuator value + * @param actuatorName Actuator name + * @param signalValue Signal value + * @param commandId Command ID + * @param issuedTimestampMs Timestamp of when the command was issued in the cloud in ms since + * epoch. + * @param executionTimeoutMs Relative execution timeout in ms since `issuedTimestampMs`. A value + * of zero means no timeout. + * @param notifyStatusCallback Callback to notify command status + */ + void setActuatorValue( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) override; + + /** + * @brief Gets the actuator names supported by the command dispatcher + * @todo The decoder manifest doesn't yet have an indication of whether a signal is + * READ/WRITE/READ_WRITE. Until it does this interface is needed to get the names of the + * actuators supported by the command dispatcher, so that for string signals, buffers can be + * pre-allocated in the RawDataManager by the CollectionSchemeManager when a new decoder + * manifest arrives. When the READ/WRITE/READ_WRITE usage of a signal is available this + * interface can be removed. + * @return List of actuator names + */ + std::vector getActuatorNames() override; + +private: + /** + * @brief The shared pointer to the base class commonAPI proxy + */ + std::shared_ptr mProxy; + + /** + * @brief The shared pointer to the interface specific wrapper + */ + std::shared_ptr mSomeipInterfaceWrapper; +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/SomeipDataSource.cpp b/src/SomeipDataSource.cpp new file mode 100644 index 00000000..3fddf65e --- /dev/null +++ b/src/SomeipDataSource.cpp @@ -0,0 +1,167 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "SomeipDataSource.h" +#include "LoggingModule.h" +#include "SignalTypes.h" +#include "Thread.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +SomeipDataSource::SomeipDataSource( std::shared_ptr exampleSomeipInterfaceWrapper, + std::shared_ptr namedSignalDataSource, + std::shared_ptr rawDataBufferManager, + uint32_t cyclicUpdatePeriodMs ) + : mExampleSomeipInterfaceWrapper( std::move( exampleSomeipInterfaceWrapper ) ) + , mNamedSignalDataSource( std::move( namedSignalDataSource ) ) + , mRawBufferManager( std::move( rawDataBufferManager ) ) + , mCyclicUpdatePeriodMs( cyclicUpdatePeriodMs ) +{ +} + +SomeipDataSource::~SomeipDataSource() +{ + if ( mThread.joinable() ) + { + mShouldStop = true; + mThread.join(); + } + if ( mProxy ) + { + if ( mXSubscription != 0 ) + { + mProxy->getXAttribute().getChangedEvent().unsubscribe( mXSubscription ); + } + if ( mA1Subscription != 0 ) + { + mProxy->getA1Attribute().getChangedEvent().unsubscribe( mA1Subscription ); + } + } + // coverity[cert_err50_cpp_violation] false positive - join is called to exit the previous thread + // coverity[autosar_cpp14_a15_5_2_violation] false positive - join is called to exit the previous thread +} + +void +SomeipDataSource::pushXValue( const int32_t &val ) +{ + mNamedSignalDataSource->ingestSignalValue( + 0, "Vehicle.ExampleSomeipInterface.X", DecodedSignalValue{ val, SignalType::UINT32 } ); +} + +void +SomeipDataSource::pushA1Value( const v1::commonapi::CommonTypes::a1Struct &val ) +{ + std::vector> values; + values.emplace_back( std::make_pair( "Vehicle.ExampleSomeipInterface.A1.A2.A", + DecodedSignalValue{ val.getA2().getA(), SignalType::INT32 } ) ); + values.emplace_back( std::make_pair( "Vehicle.ExampleSomeipInterface.A1.A2.B", + DecodedSignalValue{ val.getA2().getB(), SignalType::BOOLEAN } ) ); + values.emplace_back( std::make_pair( "Vehicle.ExampleSomeipInterface.A1.A2.D", + DecodedSignalValue{ val.getA2().getD(), SignalType::DOUBLE } ) ); + pushStringSignalToNamedDataSource( "Vehicle.ExampleSomeipInterface.A1.S", val.getS() ); + mNamedSignalDataSource->ingestMultipleSignalValues( 0, values ); +} + +void +SomeipDataSource::pushStringSignalToNamedDataSource( const std::string &signalName, const std::string &stringValue ) +{ + SignalID signalID = mNamedSignalDataSource->getNamedSignalID( signalName ); + if ( signalID == INVALID_SIGNAL_ID ) + { + FWE_LOG_TRACE( "No decoding rules set for signal name " + signalName ); + return; + } + + if ( mRawBufferManager == nullptr ) + { + FWE_LOG_WARN( "Raw message id: " + std::to_string( signalID ) + " can not be handed over to RawBufferManager" ); + return; + } + auto receiveTime = mClock->systemTimeSinceEpochMs(); + std::vector buffer( stringValue.begin(), stringValue.end() ); + auto bufferHandle = mRawBufferManager->push( ( buffer.data() ), buffer.size(), receiveTime, signalID ); + + if ( bufferHandle == RawData::INVALID_BUFFER_HANDLE ) + { + FWE_LOG_WARN( "Raw message id: " + std::to_string( signalID ) + " was rejected by RawBufferManager" ); + return; + } + // immediately set usage hint so buffer handle does not get directly deleted again + mRawBufferManager->increaseHandleUsageHint( + signalID, bufferHandle, RawData::BufferHandleUsageStage::COLLECTED_NOT_IN_HISTORY_BUFFER ); + + mNamedSignalDataSource->ingestSignalValue( + receiveTime, signalName, DecodedSignalValue{ bufferHandle, SignalType::STRING } ); +} + +bool +SomeipDataSource::init() +{ + if ( !mExampleSomeipInterfaceWrapper->init() ) + { + return false; + } + + mProxy = std::dynamic_pointer_cast>( + mExampleSomeipInterfaceWrapper->getProxy() ); + + mXSubscription = mProxy->getXAttribute().getChangedEvent().subscribe( [this]( const int32_t &val ) { + std::lock_guard lock( mLastValMutex ); + mLastXVal = val; + mLastXValAvailable = true; + pushXValue( val ); + } ); + + mA1Subscription = mProxy->getA1Attribute().getChangedEvent().subscribe( + [this]( const v1::commonapi::CommonTypes::a1Struct &val ) { + std::lock_guard lock( mLastValMutex ); + mLastA1Val = val; + mLastA1ValAvailable = true; + pushA1Value( val ); + } ); + + if ( mCyclicUpdatePeriodMs > 0 ) + { + mThread = std::thread( [this]() { + Thread::setCurrentThreadName( "SomeipDataSource" ); + while ( !mShouldStop ) + { + // If the proxy is available, push the last vals periodically: + { + std::lock_guard lock( mLastValMutex ); + if ( !mProxy->isAvailable() ) + { + mLastXValAvailable = false; + mLastA1ValAvailable = false; + } + else + { + if ( mLastXValAvailable ) + { + pushXValue( mLastXVal ); + } + if ( mLastA1ValAvailable ) + { + pushA1Value( mLastA1Val ); + } + } + } + std::this_thread::sleep_for( std::chrono::milliseconds( mCyclicUpdatePeriodMs ) ); + } + } ); + } + + FWE_LOG_INFO( "Successfully initialized" ); + return true; +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/SomeipDataSource.h b/src/SomeipDataSource.h new file mode 100644 index 00000000..376734bf --- /dev/null +++ b/src/SomeipDataSource.h @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "ExampleSomeipInterfaceWrapper.h" +#include "IDecoderDictionary.h" +#include "NamedSignalDataSource.h" +#include "RawDataManager.h" +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class SomeipDataSource +{ +public: + SomeipDataSource( std::shared_ptr exampleSomeipInterfaceWrapper, + std::shared_ptr namedSignalDataSource, + std::shared_ptr rawDataBufferManager, + uint32_t cyclicUpdatePeriodMs ); + ~SomeipDataSource(); + + bool init(); + + SomeipDataSource( const SomeipDataSource & ) = delete; + SomeipDataSource &operator=( const SomeipDataSource & ) = delete; + SomeipDataSource( SomeipDataSource && ) = delete; + SomeipDataSource &operator=( SomeipDataSource && ) = delete; + +private: + std::shared_ptr mExampleSomeipInterfaceWrapper; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr> mProxy; + std::shared_ptr mClock = ClockHandler::getClock(); + std::shared_ptr mDecoderDictionary; + std::shared_ptr mRawBufferManager; + + uint32_t mCyclicUpdatePeriodMs{}; + std::thread mThread; + std::atomic mShouldStop{}; + std::mutex mLastValMutex; + + uint32_t mXSubscription{}; + bool mLastXValAvailable{}; + int32_t mLastXVal{}; + void pushXValue( const int32_t &val ); + + uint32_t mA1Subscription{}; + bool mLastA1ValAvailable{}; + v1::commonapi::CommonTypes::a1Struct mLastA1Val; + void pushA1Value( const v1::commonapi::CommonTypes::a1Struct &val ); + + void pushStringSignalToNamedDataSource( const std::string &signalName, const std::string &stringValue ); +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/SomeipToCanBridge.cpp b/src/SomeipToCanBridge.cpp new file mode 100644 index 00000000..4919126f --- /dev/null +++ b/src/SomeipToCanBridge.cpp @@ -0,0 +1,154 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "SomeipToCanBridge.h" +#include "EnumUtility.h" +#include "LoggingModule.h" +#include "Thread.h" +#include "TraceModule.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +SomeipToCanBridge::SomeipToCanBridge( + uint16_t someipServiceId, + uint16_t someipInstanceId, + uint16_t someipEventId, + uint16_t someipEventGroupId, + std::string someipApplicationName, + CANChannelNumericID canChannelId, + CANDataConsumer &canDataConsumer, + std::function( std::string )> createSomeipApplication, + std::function removeSomeipApplication ) + : mSomeipServiceId( someipServiceId ) + , mSomeipInstanceId( someipInstanceId ) + , mSomeipEventId( someipEventId ) + , mSomeipEventGroupId( someipEventGroupId ) + , mSomeipApplicationName( std::move( someipApplicationName ) ) + , mCanChannelId( canChannelId ) + , mCanDataConsumer( canDataConsumer ) + , mCreateSomeipApplication( std::move( createSomeipApplication ) ) + , mRemoveSomeipApplication( std::move( removeSomeipApplication ) ) +{ +} + +bool +SomeipToCanBridge::init() +{ + mSomeipApplication = mCreateSomeipApplication( mSomeipApplicationName ); + if ( !mSomeipApplication->init() ) + { + FWE_LOG_ERROR( "Couldn't initialize the someip application" ); + mRemoveSomeipApplication( mSomeipApplicationName ); + return false; + } + mSomeipApplication->register_sd_acceptance_handler( []( const vsomeip::remote_info_t &remoteInfo ) -> bool { + FWE_LOG_INFO( + "Accepting service discovery requests: first: " + std::to_string( remoteInfo.first_ ) + " last: " + + std::to_string( remoteInfo.last_ ) + " ip: " + std::to_string( remoteInfo.ip_.address_.v4_[0] ) + "." + + std::to_string( remoteInfo.ip_.address_.v4_[1] ) + "." + std::to_string( remoteInfo.ip_.address_.v4_[2] ) + + "." + std::to_string( remoteInfo.ip_.address_.v4_[3] ) + " is_range: " + + std::to_string( remoteInfo.is_range_ ) + " is_reliable: " + std::to_string( remoteInfo.is_reliable_ ) ); + return true; + } ); + mSomeipApplication->register_availability_handler( + mSomeipServiceId, + mSomeipInstanceId, + []( vsomeip::service_t service, vsomeip::instance_t instance, bool isAvailable ) { + std::stringstream stream; + stream << "Service [" << std::setw( 4 ) << std::setfill( '0' ) << std::hex << service << "." << instance + << "] is " << ( isAvailable ? "available" : "NOT available" ); + FWE_LOG_INFO( stream.str() ); + } ); + mSomeipApplication->request_service( mSomeipServiceId, mSomeipInstanceId ); + mSomeipApplication->register_message_handler( + mSomeipServiceId, + mSomeipInstanceId, + mSomeipEventId, + [this]( const std::shared_ptr &response ) { + auto payload = response->get_payload(); + if ( payload->get_length() < HEADER_SIZE ) + { + FWE_LOG_ERROR( "Someip event message is too short" ); + return; + } + uint32_t canMessageId = be32toh( reinterpret_cast( payload->get_data() )[0] ); + Timestamp timestamp = be64toh( reinterpret_cast( payload->get_data() + 4 )[0] ); + if ( timestamp == 0 ) + { + TraceModule::get().incrementVariable( TraceVariable::POLLING_TIMESTAMP_COUNTER ); + timestamp = mClock->systemTimeSinceEpochMs(); + } + else + { + timestamp /= 1000; + } + if ( timestamp < mLastFrameTime ) + { + TraceModule::get().incrementAtomicVariable( TraceAtomicVariable::NOT_TIME_MONOTONIC_FRAMES ); + } + mLastFrameTime = timestamp; + unsigned traceFrames = mCanChannelId + toUType( TraceVariable::READ_SOCKET_FRAMES_0 ); + TraceModule::get().incrementVariable( + ( traceFrames < static_cast( toUType( TraceVariable::READ_SOCKET_FRAMES_19 ) ) ) + ? static_cast( traceFrames ) + : TraceVariable::READ_SOCKET_FRAMES_19 ); + std::lock_guard lock( mDecoderDictMutex ); + mCanDataConsumer.processMessage( mCanChannelId, + mDecoderDictionary, + canMessageId, + payload->get_data() + HEADER_SIZE, + payload->get_length() - HEADER_SIZE, + timestamp ); + } ); + std::set eventGroupSet; + eventGroupSet.insert( mSomeipEventGroupId ); + mSomeipApplication->request_event( mSomeipServiceId, mSomeipInstanceId, mSomeipEventId, eventGroupSet ); + mSomeipApplication->subscribe( mSomeipServiceId, mSomeipInstanceId, mSomeipEventGroupId ); + mSomeipThread = std::thread( [this]() { + Thread::setCurrentThreadName( "SomeipBridge" + std::to_string( mCanChannelId ) ); + mSomeipApplication->start(); + } ); + return true; +} + +void +SomeipToCanBridge::disconnect() +{ + if ( mSomeipThread.joinable() ) + { + mSomeipApplication->stop(); + mSomeipThread.join(); + mRemoveSomeipApplication( mSomeipApplicationName ); + } +} + +void +SomeipToCanBridge::onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, + VehicleDataSourceProtocol networkProtocol ) +{ + if ( networkProtocol != VehicleDataSourceProtocol::RAW_SOCKET ) + { + return; + } + std::lock_guard lock( mDecoderDictMutex ); + mDecoderDictionary = std::dynamic_pointer_cast( dictionary ); + if ( dictionary == nullptr ) + { + FWE_LOG_TRACE( "Decoder dictionary removed" ); + } + else + { + FWE_LOG_TRACE( "Decoder dictionary updated" ); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/SomeipToCanBridge.h b/src/SomeipToCanBridge.h new file mode 100644 index 00000000..a3148626 --- /dev/null +++ b/src/SomeipToCanBridge.h @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "CANDataConsumer.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "IDecoderDictionary.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include "VehicleDataSourceTypes.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class SomeipToCanBridge +{ +public: + SomeipToCanBridge( uint16_t someipServiceId, + uint16_t someipInstanceId, + uint16_t someipEventId, + uint16_t someipEventGroupId, + std::string someipApplicationName, + CANChannelNumericID canChannelId, + CANDataConsumer &canDataConsumer, + std::function( std::string )> createSomeipApplication, + std::function removeSomeipApplication ); + ~SomeipToCanBridge() = default; + + SomeipToCanBridge( const SomeipToCanBridge & ) = delete; + SomeipToCanBridge &operator=( const SomeipToCanBridge & ) = delete; + SomeipToCanBridge( SomeipToCanBridge && ) = delete; + SomeipToCanBridge &operator=( SomeipToCanBridge && ) = delete; + + bool init(); + void disconnect(); + + void onChangeOfActiveDictionary( ConstDecoderDictionaryConstPtr &dictionary, + VehicleDataSourceProtocol networkProtocol ); + +private: + static constexpr uint8_t HEADER_SIZE = 12; + uint16_t mSomeipServiceId; + uint16_t mSomeipInstanceId; + uint16_t mSomeipEventId; + uint16_t mSomeipEventGroupId; + std::string mSomeipApplicationName; + CANChannelNumericID mCanChannelId; + CANDataConsumer &mCanDataConsumer; + std::function( std::string )> mCreateSomeipApplication; + std::function mRemoveSomeipApplication; + std::shared_ptr mSomeipApplication; + std::thread mSomeipThread; + std::mutex mDecoderDictMutex; + std::shared_ptr mDecoderDictionary; + Timestamp mLastFrameTime{}; + std::shared_ptr mClock = ClockHandler::getClock(); +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StoreFileSystem.cpp b/src/StoreFileSystem.cpp new file mode 100644 index 00000000..fe739f51 --- /dev/null +++ b/src/StoreFileSystem.cpp @@ -0,0 +1,253 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StoreFileSystem.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ +static void +sync( int fileno ) +{ + // Only sync data if available on this OS. Otherwise, just fsync. +#if _POSIX_SYNCHRONIZED_IO > 0 + std::ignore = fdatasync( fileno ); +#else + std::ignore = fsync( fileno ); +#endif +} + +aws::store::filesystem::FileError +errnoToFileError( const int err, const std::string &str ) +{ + switch ( err ) + { + case EACCES: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::AccessDenied, + str + " Access denied" }; + case EDQUOT: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::DiskFull, + str + " User inode/disk block quota exhausted" }; + case EINVAL: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::InvalidArguments, + str + " Unknown invalid arguments" }; + case EISDIR: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::InvalidArguments, + str + + " Path cannot be opened for writing because it is a directory" }; + case ELOOP: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::InvalidArguments, + str + " Too many symbolic links" }; + case EMFILE: // fallthrough + case ENFILE: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::TooManyOpenFiles, + str + " Too many open files. Consider raising limits." }; + case ENOENT: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::FileDoesNotExist, + str + " Path does not exist" }; + case EFBIG: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::InvalidArguments, + str + " File is too large" }; + case EIO: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::IOError, + str + " Unknown IO error" }; + case ENOSPC: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::DiskFull, str + " Disk full" }; + default: + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::Unknown, + str + " Unknown error code: " + std::to_string( err ) }; + } +} + +PosixFileLike::PosixFileLike( boost::filesystem::path &&path ) + : _path( std::move( path ) ) +{ +} + +PosixFileLike::~PosixFileLike() +{ + if ( _f != nullptr ) + { + std::ignore = std::fclose( _f ); + } +} + +aws::store::filesystem::FileError +PosixFileLike::open() noexcept +{ + _f = std::fopen( _path.c_str(), "ab+" ); + if ( _f == nullptr ) + { + // coverity[autosar_cpp14_m19_3_1_violation] fopen gives us errors via errno + // coverity[misra_cpp_2008_rule_19_3_1_violation] fopen gives us errors via errno + return errnoToFileError( errno ); + } + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::NoError, {} }; +} + +aws::store::common::Expected +PosixFileLike::read( uint32_t begin, uint32_t end ) +{ + if ( end < begin ) + { + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::InvalidArguments, + "End must be after the beginning" }; + } + if ( end == begin ) + { + return aws::store::common::OwnedSlice{ 0U }; + } + + std::lock_guard lock{ _read_lock }; + clearerr( _f ); + auto d = aws::store::common::OwnedSlice{ ( end - begin ) }; + if ( std::fseek( _f, static_cast( begin ), SEEK_SET ) != 0 ) + { + // coverity[autosar_cpp14_m19_3_1_violation] fseek gives us errors via errno + // coverity[misra_cpp_2008_rule_19_3_1_violation] fseek gives us errors via errno + return errnoToFileError( errno ); + } + if ( std::fread( d.data(), d.size(), 1U, _f ) != 1U ) + { + if ( feof( _f ) != 0 ) + { + return { aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::EndOfFile, {} } }; + } + // coverity[autosar_cpp14_m19_3_1_violation] fread gives us errors via errno + // coverity[misra_cpp_2008_rule_19_3_1_violation] fread gives us errors via errno + return errnoToFileError( errno ); + } + return d; +} + +aws::store::filesystem::FileError +PosixFileLike::append( aws::store::common::BorrowedSlice data ) +{ + clearerr( _f ); + if ( fwrite( data.data(), data.size(), 1U, _f ) != 1U ) + { + // coverity[autosar_cpp14_m19_3_1_violation] fwrite gives us errors via errno + // coverity[misra_cpp_2008_rule_19_3_1_violation] fwrite gives us errors via errno + return errnoToFileError( errno ); + } + return { aws::store::filesystem::FileErrorCode::NoError, {} }; +} + +aws::store::filesystem::FileError +PosixFileLike::flush() +{ + if ( fflush( _f ) == 0 ) + { + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::NoError, {} }; + } + // coverity[autosar_cpp14_m19_3_1_violation] fflush gives us errors via errno + // coverity[misra_cpp_2008_rule_19_3_1_violation] fflush gives us errors via errno + return errnoToFileError( errno ); +} + +void +PosixFileLike::sync() +{ + Aws::IoTFleetWise::Store::sync( fileno( _f ) ); +} + +aws::store::filesystem::FileError +PosixFileLike::truncate( uint32_t max ) +{ + // Flush buffers before truncating since truncation is operating on the FD directly rather than the file + // stream + std::ignore = flush(); + if ( ftruncate( fileno( _f ), static_cast( max ) ) != 0 ) + { + // coverity[autosar_cpp14_m19_3_1_violation] ftruncate gives us errors via errno + // coverity[misra_cpp_2008_rule_19_3_1_violation] ftruncate gives us errors via errno + return errnoToFileError( errno ); + } + return flush(); +} + +PosixFileSystem::PosixFileSystem( boost::filesystem::path base_path ) + : _base_path( std::move( base_path ) ) +{ +} + +aws::store::common::Expected, aws::store::filesystem::FileError> +PosixFileSystem::open( const std::string &identifier ) +{ + if ( !_initialized ) + { + boost::system::error_code ec; + boost::filesystem::create_directories( _base_path, ec ); + if ( ec.failed() ) + { + return aws::store::filesystem::FileError{ aws::store::filesystem::FileErrorCode::Unknown, ec.what() }; + } + _initialized = true; + } + + auto f = std::make_unique( _base_path / boost::filesystem::path{ identifier } ); + auto res = f->open(); + if ( res.ok() ) + { + return { std::move( f ) }; + } + return res; +} + +bool +PosixFileSystem::exists( const std::string &identifier ) +{ + return boost::filesystem::exists( _base_path / boost::filesystem::path{ identifier } ); +} + +aws::store::filesystem::FileError +PosixFileSystem::rename( const std::string &old_id, const std::string &new_id ) +{ + boost::system::error_code ec; + boost::filesystem::rename( + _base_path / boost::filesystem::path{ old_id }, _base_path / boost::filesystem::path{ new_id }, ec ); + if ( !ec ) + { + return { aws::store::filesystem::FileErrorCode::NoError, {} }; + } + return errnoToFileError( ec.value(), ec.message() ); +} + +aws::store::filesystem::FileError +PosixFileSystem::remove( const std::string &id ) +{ + boost::system::error_code ec; + std::ignore = boost::filesystem::remove( _base_path / boost::filesystem::path{ id }, ec ); + if ( !ec ) + { + return { aws::store::filesystem::FileErrorCode::NoError, {} }; + } + return errnoToFileError( ec.value(), ec.message() ); +} + +aws::store::common::Expected, aws::store::filesystem::FileError> +PosixFileSystem::list() +{ + std::vector output; + for ( const auto &entry : boost::filesystem::directory_iterator( _base_path ) ) + { + output.emplace_back( entry.path().filename().string() ); + } + return output; +} +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StoreFileSystem.h b/src/StoreFileSystem.h new file mode 100644 index 00000000..28d92743 --- /dev/null +++ b/src/StoreFileSystem.h @@ -0,0 +1,77 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ +class PosixFileLike : public aws::store::filesystem::FileLike +{ + std::mutex _read_lock{}; + boost::filesystem::path _path; + FILE *_f = nullptr; + +public: + explicit PosixFileLike( boost::filesystem::path &&path ); + PosixFileLike( PosixFileLike && ) = delete; + PosixFileLike( PosixFileLike & ) = delete; + PosixFileLike &operator=( PosixFileLike & ) = delete; + PosixFileLike &operator=( PosixFileLike && ) = delete; + + ~PosixFileLike() override; + + aws::store::filesystem::FileError open() noexcept; + + aws::store::common::Expected read( + uint32_t begin, uint32_t end ) override; + + aws::store::filesystem::FileError append( aws::store::common::BorrowedSlice data ) override; + + aws::store::filesystem::FileError flush() override; + + void sync() override; + + aws::store::filesystem::FileError truncate( uint32_t max ) override; +}; + +class PosixFileSystem : public aws::store::filesystem::FileSystemInterface +{ +protected: + bool _initialized{ false }; + boost::filesystem::path _base_path; + +public: + explicit PosixFileSystem( boost::filesystem::path base_path ); + + aws::store::common::Expected, aws::store::filesystem::FileError> + open( const std::string &identifier ) override; + + bool exists( const std::string &identifier ) override; + + aws::store::filesystem::FileError rename( const std::string &old_id, const std::string &new_id ) override; + + aws::store::filesystem::FileError remove( const std::string &id ) override; + + aws::store::common::Expected, aws::store::filesystem::FileError> list() override; +}; + +aws::store::filesystem::FileError errnoToFileError( const int err, const std::string &str = {} ); + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StoreLogger.cpp b/src/StoreLogger.cpp new file mode 100644 index 00000000..2ea13f10 --- /dev/null +++ b/src/StoreLogger.cpp @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StoreLogger.h" +#include "LogLevel.h" +#include "LoggingModule.h" +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +Logger::Logger() +{ + switch ( gSystemWideLogLevel ) + { + case LogLevel::Trace: + level = aws::store::logging::LogLevel::Trace; + break; + case LogLevel::Info: + level = aws::store::logging::LogLevel::Info; + break; + case LogLevel::Warning: + level = aws::store::logging::LogLevel::Warning; + break; + case LogLevel::Error: + level = aws::store::logging::LogLevel::Error; + break; + case LogLevel::Off: + level = aws::store::logging::LogLevel::Disabled; + break; + } +} + +void +Logger::log( aws::store::logging::LogLevel l, const std::string &message ) const +{ + switch ( l ) + { + case aws::store::logging::LogLevel::Disabled: + break; + case aws::store::logging::LogLevel::Trace: + LoggingModule::log( LogLevel::Trace, {}, 0, {}, message ); + break; + case aws::store::logging::LogLevel::Debug: + LoggingModule::log( LogLevel::Trace, {}, 0, {}, message ); + break; + case aws::store::logging::LogLevel::Info: + LoggingModule::log( LogLevel::Info, {}, 0, {}, message ); + break; + case aws::store::logging::LogLevel::Warning: + LoggingModule::log( LogLevel::Warning, {}, 0, {}, message ); + break; + case aws::store::logging::LogLevel::Error: + LoggingModule::log( LogLevel::Error, {}, 0, {}, message ); + break; + } +} +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StoreLogger.h b/src/StoreLogger.h new file mode 100644 index 00000000..442cad95 --- /dev/null +++ b/src/StoreLogger.h @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ +class Logger : public aws::store::logging::Logger +{ +public: + Logger(); + + void log( aws::store::logging::LogLevel l, const std::string &message ) const override; +}; +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StreamForwarder.cpp b/src/StreamForwarder.cpp new file mode 100644 index 00000000..50c717f5 --- /dev/null +++ b/src/StreamForwarder.cpp @@ -0,0 +1,379 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StreamForwarder.h" +#include "Clock.h" +#include "DataSenderTypes.h" +#include "LoggingModule.h" +#include "RateLimiter.h" +#include "Signal.h" +#include "StreamManager.h" +#include "TelemetryDataSender.h" +#include "Thread.h" +#include "TraceModule.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +StreamForwarder::StreamForwarder( std::shared_ptr streamManager, + std::shared_ptr dataSender, + std::shared_ptr rateLimiter, + uint32_t idleTimeMs ) + : mStreamManager( std::move( streamManager ) ) + , mDataSender( std::move( dataSender ) ) + , mIdleTimeMs( idleTimeMs ) + , mRateLimiter( std::move( rateLimiter ) ) +{ +} + +StreamForwarder::~StreamForwarder() +{ + if ( isAlive() ) + { + stop(); + } +} + +bool +StreamForwarder::start() +{ + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( false ); + if ( !mThread.create( doWork, this ) ) + { + FWE_LOG_ERROR( "Thread failed to start" ); + } + else + { + FWE_LOG_INFO( "Thread started" ); + mThread.setThreadName( "fwDMStrUpldr" ); + } + return mThread.isActive() && mThread.isValid(); +} + +bool +StreamForwarder::stop() +{ + FWE_LOG_INFO( "Stream Forwarder Thread stopping" ); + std::lock_guard lock( mThreadMutex ); + mShouldStop.store( true, std::memory_order_relaxed ); + mWait.notify(); + mSenderFinished.notify(); + mThread.release(); + mShouldStop.store( false, std::memory_order_relaxed ); + FWE_LOG_INFO( "Stream Forwarder Thread stopped" ); + return !mThread.isActive(); +} + +bool +StreamForwarder::isAlive() +{ + return mThread.isValid() && mThread.isActive(); +} + +bool +StreamForwarder::shouldStop() const +{ + return mShouldStop.load( std::memory_order_relaxed ); +} + +void +StreamForwarder::doWork( void *data ) +{ + StreamForwarder *forwarder = static_cast( data ); + Timestamp lastTraceOutput = 0; + size_t numberOfSignalsProcessed = 0; + uint32_t activations = 0; + + while ( !forwarder->shouldStop() ) + { + activations++; + + // Don't print every iteration to avoid spamming the log + if ( forwarder->mClock->monotonicTimeSinceEpochMs() > + ( lastTraceOutput + LoggingModule::LOG_AGGREGATION_TIME_MS ) ) + { + FWE_LOG_TRACE( "Activations: " + std::to_string( activations ) + " Since last idling processed " + + std::to_string( numberOfSignalsProcessed ) + " signals" ); + activations = 0; + numberOfSignalsProcessed = 0; + lastTraceOutput = forwarder->mClock->monotonicTimeSinceEpochMs(); + } + + forwarder->updatePartitionsWaitingForData(); + + std::vector partitionsToSkip; + std::map partitionsToRead; + { + std::lock_guard lock( forwarder->mPartitionMutex ); + for ( auto const &partitions : forwarder->mPartitionsToUpload ) + { + auto campaignPartition = partitions.first; + if ( forwarder->partitionWaitingForData( campaignPartition ) ) + { + partitionsToSkip.emplace_back( campaignPartition ); + } + else if ( forwarder->partitionIsEnabled( ( campaignPartition ) ) ) + { + uint64_t endTime = 0; + auto campaignId = campaignPartition.first; + if ( forwarder->mJobCampaignToEndTime.find( campaignId ) != forwarder->mJobCampaignToEndTime.end() ) + { + endTime = forwarder->mJobCampaignToEndTime[campaignId]; + } + partitionsToRead[campaignPartition] = endTime; + } + } + } + + if ( partitionsToRead.empty() ) + { + if ( partitionsToSkip.empty() ) + { + FWE_LOG_TRACE( "Waiting indefinitely until campaign forwarding is requested" ); + forwarder->mWait.wait( Signal::WaitWithPredicate ); + continue; + } + else + { + // all partitions skipped, wait for data to arrive in stream + forwarder->mWait.wait( forwarder->mIdleTimeMs ); + continue; + } + } + + // remove partitions that don't exist anymore + // (happens when campaigns are removed) + std::vector partitionsToRemove; + + // read from partitions and forward the data to the shared data queue, + // which in turn will be sent to cloud via a DataSender. + for ( const auto &partition : partitionsToRead ) + { + auto campaignPartition = partition.first; + auto endTime = partition.second; + + if ( forwarder->shouldStop() ) + { + return; + } + + std::string schemeData; + StreamManager::RecordMetadata metadata; + std::function checkpoint; + + StreamManager::ReturnCode result = forwarder->mStreamManager->readFromStream( + campaignPartition.first, campaignPartition.second, schemeData, metadata, checkpoint ); + + if ( ( endTime != 0 ) && ( metadata.triggerTime >= static_cast( endTime ) ) ) + { + forwarder->checkIfJobCompleted( campaignPartition ); + } + else if ( result == StreamManager::ReturnCode::SUCCESS ) + { + if ( !forwarder->mRateLimiter->consumeToken() ) + { + forwarder->mWait.wait( forwarder->mIdleTimeMs ); + continue; + } + + numberOfSignalsProcessed += metadata.numSignals; + + FWE_LOG_INFO( "Processing campaign " + campaignPartition.first + " partition " + + std::to_string( campaignPartition.second ) ); + forwarder->mDataSender->processSerializedData( + schemeData, + [checkpoint, forwarder, metadata]( bool success, std::shared_ptr unused ) { + static_cast( unused ); + if ( success ) + { + checkpoint(); + TraceModule::get().addToVariable( TraceVariable::DATA_FORWARD_SIGNAL_COUNT, + metadata.numSignals ); + } + else + { + TraceModule::get().incrementVariable( TraceVariable::DATA_FORWARD_ERROR ); + } + forwarder->mSenderFinished.notify(); + } ); + forwarder->mSenderFinished.wait( Signal::WaitWithPredicate ); + } + else if ( result == StreamManager::ReturnCode::END_OF_STREAM ) + { + forwarder->checkIfJobCompleted( campaignPartition ); + forwarder->mPartitionsWaitingForData[campaignPartition] = + forwarder->mClock->monotonicTimeSinceEpochMs() + 1000; + } + else if ( result == StreamManager::ReturnCode::STREAM_NOT_FOUND ) + { + partitionsToRemove.emplace_back( campaignPartition ); + } + else if ( result == StreamManager::ReturnCode::ERROR ) + { + FWE_LOG_ERROR( "Unable to read from stream. campaignID: " + campaignPartition.first + + ", partitionID: " + std::to_string( campaignPartition.second ) ); + } + } + + for ( const auto &campaignPartition : partitionsToRemove ) + { + FWE_LOG_INFO( "Removing partition. campaignID: " + campaignPartition.first + + ", partitionID: " + std::to_string( campaignPartition.second ) ); + std::lock_guard lock( forwarder->mPartitionMutex ); + if ( forwarder->mPartitionsToUpload.erase( campaignPartition ) > 0 ) + { + FWE_LOG_TRACE( "Stream for partition not found, removing. campaignID: " + campaignPartition.first + + ", partitionID: " + std::to_string( campaignPartition.second ) ) + } + } + + forwarder->mWait.wait( forwarder->mIdleTimeMs ); + } +} + +bool +StreamForwarder::partitionIsEnabled( const CampaignPartition &campaignPartition ) +{ + auto partition = mPartitionsToUpload.find( campaignPartition ); + return ( partition != mPartitionsToUpload.end() ) && ( !partition->second.enabled.empty() ); +} + +void +StreamForwarder::checkIfJobCompleted( const CampaignPartition &campaignPartition ) +{ + std::unique_lock lock( mPartitionMutex ); + + if ( mJobCampaignToPartitions.find( campaignPartition.first ) != mJobCampaignToPartitions.end() ) + { + FWE_LOG_TRACE( "Cancel IoT Job forward for campaign: " + campaignPartition.first + + ", partitionID: " + std::to_string( campaignPartition.second ) ); + // This means that the campaign and subsequent partition that we are looking at is for an IoT + // Job Since we have reached END_OF_STREAM or hit endTime, remove this partition + mJobCampaignToPartitions[campaignPartition.first].erase( campaignPartition.second ); + + // This is equivalent to calling cancelForward + mPartitionsToUpload[campaignPartition].enabled.erase( Source::IOT_JOB ); + + if ( mJobCampaignToPartitions[campaignPartition.first].empty() ) + { + // all the campaign partitions have reached end of data stream so complete the Iot Job + mJobCampaignToPartitions.erase( campaignPartition.first ); + mJobCampaignToEndTime.erase( campaignPartition.first ); + + // don't hold the mPartitionMutex in the job completion callback + lock.unlock(); + + if ( mJobCompletionCallback ) + { + // Triggers IotJobDataRequestHandler to update job status + FWE_LOG_TRACE( "Notifying IoTJobDataRequestHandler that a job has completed uploading data" ); + mJobCompletionCallback( campaignPartition.first ); + } + } + } +} + +void +StreamForwarder::beginJobForward( const CampaignID &cID, uint64_t endTime ) +{ + auto partitions = mStreamManager->getPartitionIdsFromCampaign( cID ); + for ( auto &pID : partitions ) + { + FWE_LOG_TRACE( "Forward requested. campaignID: " + cID + ", partitionID: " + std::to_string( pID ) + + " endTime: " + std::to_string( endTime ) + " source: IOT_JOB" ); + std::lock_guard lock( mPartitionMutex ); + if ( mJobCampaignToPartitions.find( cID ) != mJobCampaignToPartitions.end() && + ( mJobCampaignToPartitions[cID].find( pID ) != mJobCampaignToPartitions[cID].end() ) && + ( mJobCampaignToEndTime.find( cID ) != mJobCampaignToEndTime.end() ) ) + { + // Note: we are checking partition to avoid calculating the maxEndtime for only one Job beginJobForward call + // A job has already targeted this campaign cID. Take the max endTime unless one of the endTimes is 0 + uint64_t maxEndTime = 0; + uint64_t currentEndTime = mJobCampaignToEndTime[cID]; + if ( ( endTime != 0 ) && ( currentEndTime != 0 ) ) + { + maxEndTime = currentEndTime < endTime ? endTime : currentEndTime; + } + mJobCampaignToEndTime[cID] = maxEndTime; + } + else + { + mJobCampaignToEndTime[cID] = endTime; + } + mJobCampaignToPartitions[cID].insert( pID ); + mPartitionsToUpload[{ cID, pID }].enabled.insert( Source::IOT_JOB ); + } + mWait.notify(); +} + +void +StreamForwarder::beginForward( const CampaignID &cID, PartitionID pID, Source source ) +{ + std::lock_guard lock( mPartitionMutex ); + if ( mPartitionsToUpload[{ cID, pID }].enabled.insert( source ).second ) + { + FWE_LOG_TRACE( "Forward requested. campaignID: " + cID + ", partitionID: " + std::to_string( pID ) ); + mWait.notify(); + } +} + +void +StreamForwarder::cancelForward( const CampaignID &cID, PartitionID pID, Source source ) +{ + std::lock_guard lock( mPartitionMutex ); + if ( mPartitionsToUpload[{ cID, pID }].enabled.find( source ) != mPartitionsToUpload[{ cID, pID }].enabled.end() ) + { + if ( mPartitionsToUpload[{ cID, pID }].enabled.erase( source ) > 0 ) + { + FWE_LOG_TRACE( "Forward cancellation requested. campaignID: " + cID + + ", partitionID: " + std::to_string( pID ) ); + } + if ( mPartitionsToUpload[{ cID, pID }].enabled.empty() ) + { + mPartitionsToUpload.erase( { cID, pID } ); + } + } +} + +void +StreamForwarder::registerJobCompletionCallback( JobCompletionCallback callback ) +{ + mJobCompletionCallback = std::move( callback ); +} + +void +StreamForwarder::updatePartitionsWaitingForData() +{ + std::vector expired; + for ( auto &partition : mPartitionsWaitingForData ) + { + if ( partition.second > mClock->timeSinceEpoch().monotonicTimeMs ) + { + expired.emplace_back( partition.first ); + } + } + for ( auto &partition : expired ) + { + mPartitionsWaitingForData.erase( partition ); + } +} + +bool +StreamForwarder::partitionWaitingForData( const CampaignPartition &campaignPartition ) +{ + return mPartitionsWaitingForData.find( campaignPartition ) != mPartitionsWaitingForData.end(); +} + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StreamForwarder.h b/src/StreamForwarder.h new file mode 100644 index 00000000..1d9a6056 --- /dev/null +++ b/src/StreamForwarder.h @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "RateLimiter.h" +#include "Signal.h" +#include "StreamManager.h" +#include "TelemetryDataSender.h" +#include "Thread.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +class StreamForwarder +{ +public: + StreamForwarder( std::shared_ptr streamManager, + std::shared_ptr dataSender, + std::shared_ptr rateLimiter, + uint32_t idleTimeMs = 50 ); + virtual ~StreamForwarder(); + + StreamForwarder( const StreamForwarder & ) = delete; + StreamForwarder &operator=( const StreamForwarder & ) = delete; + StreamForwarder( StreamForwarder && ) = delete; + StreamForwarder &operator=( StreamForwarder && ) = delete; + + enum struct Source + { + IOT_JOB, + CONDITION + }; + + void beginJobForward( const CampaignID &cID, uint64_t endTime ); + void beginForward( const CampaignID &cID, PartitionID pID, Source source ); + void cancelForward( const CampaignID &cID, PartitionID pID, Source source ); + + using JobCompletionCallback = std::function; + virtual void registerJobCompletionCallback( JobCompletionCallback callback ); + + bool start(); + bool stop(); + bool isAlive(); + bool shouldStop() const; + + static void doWork( void *data ); + +private: + using IoTJobsEndTime = uint64_t; + using CampaignPartition = std::pair; + + bool partitionIsEnabled( const CampaignPartition &campaignPartition ); + void checkIfJobCompleted( const CampaignPartition &campaignPartition ); + void updatePartitionsWaitingForData(); + bool partitionWaitingForData( const CampaignPartition &campaignPartition ); + + struct PartitionToUpload + { + std::set enabled; + }; + + std::shared_ptr mStreamManager; + std::shared_ptr mDataSender; + + std::map mPartitionsToUpload; + std::map mPartitionsWaitingForData; + std::map> mJobCampaignToPartitions; + std::map mJobCampaignToEndTime; + std::mutex mPartitionMutex; + + JobCompletionCallback mJobCompletionCallback; + + Thread mThread; + std::atomic mShouldStop{ false }; + mutable std::mutex mThreadMutex; + Signal mWait; + uint32_t mIdleTimeMs; + std::shared_ptr mRateLimiter; + std::shared_ptr mClock = ClockHandler::getClock(); + + Signal mSenderFinished; +}; + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StreamManager.cpp b/src/StreamManager.cpp new file mode 100644 index 00000000..840f2b1d --- /dev/null +++ b/src/StreamManager.cpp @@ -0,0 +1,552 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StreamManager.h" +#include "Assert.h" +#include "Clock.h" +#include "CollectionInspectionAPITypes.h" +#include "DataSenderProtoWriter.h" +#include "ICollectionScheme.h" +#include "ICollectionSchemeList.h" +#include "LoggingModule.h" +#include "OBDDataTypes.h" +#include "SignalTypes.h" +#include "StoreFileSystem.h" +#include "StoreLogger.h" + +#include "TimeTypes.h" +#include "TraceModule.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +const PartitionID StreamManager::DEFAULT_PARTITION = 0; +const std::string StreamManager::STREAM_ITER_IDENTIFIER = "i"; +const std::string StreamManager::KV_STORE_IDENTIFIER = "s"; +const int32_t StreamManager::KV_COMPACT_AFTER = 1024; +const uint32_t StreamManager::STREAM_DEFAULT_MIN_SEGMENT_SIZE = 16U * 1024U; + +StreamManager::StreamManager( std::string persistenceRootDir, + std::shared_ptr protoWriter, + uint32_t transmitThreshold ) + : mProtoWriter( std::move( protoWriter ) ) + , mTransmitThreshold{ ( transmitThreshold > 0U ) ? transmitThreshold : UINT_MAX } + , mPersistenceRootDir( std::move( persistenceRootDir ) ) + , mLogger{ std::make_shared() } +{ +} + +void +StreamManager::onChangeCollectionSchemeList( + const std::shared_ptr &activeCollectionSchemes ) +{ + // mark removed campaigns for deletion + std::vector> campaignsToErase; + for ( const auto &campaign : mCampaigns ) + { + // delete campaign if it's not present in new config + if ( activeCollectionSchemes == nullptr ) + { + campaignsToErase.emplace_back( campaign ); + continue; + } + std::shared_ptr newCampaignConfig; + for ( const auto &conf : activeCollectionSchemes->activeCollectionSchemes ) + { + if ( conf == nullptr ) + { + continue; + } + if ( ( getName( conf->getCampaignArn() ) == campaign.first ) && ( campaign.second.config == conf ) ) + { + newCampaignConfig = conf; + break; + } + } + if ( newCampaignConfig == nullptr ) + { + campaignsToErase.emplace_back( campaign ); + } + } + + // delete streams for removed campaigns + for ( const auto &campaign : campaignsToErase ) + { + { + std::lock_guard lock( mCampaignsMutex ); + + // clear internal state + mCampaigns.erase( campaign.first ); + } + // remove all partitions for campaign from disk + boost::filesystem::path campaignPath = + boost::filesystem::path{ mPersistenceRootDir } / boost::filesystem::path{ campaign.first }; + boost::system::error_code ec; + boost::filesystem::remove_all( campaignPath, ec ); + FWE_GRACEFUL_FATAL_ASSERT( + !ec.failed(), "Unable to delete campaign data at " + campaignPath.string() + " err: " + ec.to_string(), ); + FWE_LOG_INFO( "Deleted streams for campaign " + campaign.first ); + } + + // mark new campaigns for creation + std::unordered_set> campaignsToCreate; + if ( activeCollectionSchemes != nullptr ) + { + for ( const auto &newCampaignConfig : activeCollectionSchemes->activeCollectionSchemes ) + { + if ( newCampaignConfig == nullptr ) + { + continue; + } + // skip if stream already exists + if ( mCampaigns.find( getName( newCampaignConfig->getCampaignArn() ) ) != mCampaigns.end() ) + { + continue; + } + if ( newCampaignConfig->getStoreAndForwardConfiguration().empty() ) + { + FWE_LOG_TRACE( "Campaign " + getName( newCampaignConfig->getCampaignArn() ) + + " is not configured for store-and-forward" ); + continue; + } + + campaignsToCreate.emplace( newCampaignConfig ); + } + } + + // create streams for new campaigns + for ( const auto &campaignConfig : campaignsToCreate ) + { + + // Identify an invalid storage locations first + bool hasInvalidStorageLocations = false; + for ( PartitionID pID = 0; pID < campaignConfig->getStoreAndForwardConfiguration().size(); ++pID ) + { + const auto &storagePartition = campaignConfig->getStoreAndForwardConfiguration()[pID]; + const auto location = boost::filesystem::path{ storagePartition.storageOptions.storageLocation }; + if ( location.filename().empty() || location.filename_is_dot() || location.filename_is_dot_dot() ) + { + FWE_LOG_ERROR( "Campaign " + getName( campaignConfig->getCampaignArn() ) + + " has an invalid storage location in partition " + std::to_string( pID ) ); + hasInvalidStorageLocations = true; + } + } + if ( hasInvalidStorageLocations ) + { + continue; + // Do not continue and actually create anything on disk or in memory for this invalid campaign + } + + for ( PartitionID pID = 0; pID < campaignConfig->getStoreAndForwardConfiguration().size(); ++pID ) + { + // create stream partition on disk + const auto &storagePartition = campaignConfig->getStoreAndForwardConfiguration()[pID]; + + const auto location = boost::filesystem::path{ storagePartition.storageOptions.storageLocation }; + const boost::filesystem::path absoluteStorageLocation( + boost::filesystem::path{ mPersistenceRootDir } / + boost::filesystem::path{ getName( campaignConfig->getCampaignArn() ) } / location.filename() ); + const std::shared_ptr fs = + std::make_shared( absoluteStorageLocation ); + + auto storageMaxSizeBytes = static_cast( storagePartition.storageOptions.maximumSizeInBytes ); + auto storageMinSegmentSize = std::min( STREAM_DEFAULT_MIN_SEGMENT_SIZE, storageMaxSizeBytes ); + + auto stream = aws::store::stream::FileStream::openOrCreate( aws::store::stream::StreamOptions{ + storageMinSegmentSize, + storageMaxSizeBytes, + true, + fs, + mLogger, + aws::store::kv::KVOptions{ + true, + fs, + mLogger, + KV_STORE_IDENTIFIER, + KV_COMPACT_AFTER, + }, + } ); + FWE_GRACEFUL_FATAL_ASSERT( stream.ok(), "Failed to create stream: " + stream.err().msg, ); + FWE_LOG_INFO( "Opened stream for campaign " + getName( campaignConfig->getCampaignArn() ) + " partition " + + std::to_string( pID ) ); + + { + // update internal state + std::lock_guard lock( mCampaignsMutex ); + + std::unordered_set signalIDs; + for ( auto signal : campaignConfig->getCollectSignals() ) + { + if ( signal.dataPartitionId == pID ) + { + signalIDs.emplace( signal.signalID ); + } + } + mCampaigns[getName( campaignConfig->getCampaignArn() )].config = campaignConfig; + mCampaigns[getName( campaignConfig->getCampaignArn() )].partitions.emplace_back( + Partition{ pID, stream.val(), signalIDs } ); + } + } + } + + // cleanup any stray campaigns. + // this could happen, for example, when a campaign is removed while FWE is not running. + // + // out of abundance of caution, only cleanup files that appear to be stream manager files: + // ///s + boost::system::error_code ec; + // look for unknown campaigns at root level + for ( auto &rootEntry : boost::make_iterator_range( + boost::filesystem::directory_iterator( boost::filesystem::path{ mPersistenceRootDir }, ec ) ) ) + { + auto potentialCampaignDir = rootEntry.path(); + if ( boost::filesystem::is_directory( potentialCampaignDir, ec ) && + ( potentialCampaignDir.filename() != boost::filesystem::path{ "FWE_Persistency" } ) && + ( mCampaigns.find( potentialCampaignDir.filename().string() ) == mCampaigns.end() ) ) + { + // look for partitions at depth 1 + for ( auto &depth1Entry : boost::make_iterator_range( + boost::filesystem::directory_iterator( boost::filesystem::path{ potentialCampaignDir }, ec ) ) ) + { + auto potentialPartitionDir = depth1Entry.path(); + if ( boost::filesystem::is_directory( potentialPartitionDir, ec ) ) + { + // look for stream manager files at depth 2 + for ( auto &depth2Entry : boost::make_iterator_range( boost::filesystem::directory_iterator( + boost::filesystem::path{ potentialPartitionDir }, ec ) ) ) + { + auto potentialStreamManagerFile = depth2Entry.path(); + if ( boost::filesystem::is_regular_file( potentialStreamManagerFile, ec ) && + ( ( potentialStreamManagerFile.extension().string() == ".log" ) || + potentialStreamManagerFile.filename().string() == KV_STORE_IDENTIFIER ) ) + { + if ( boost::filesystem::remove( potentialStreamManagerFile, ec ) ) + { + FWE_LOG_TRACE( "Removed stray file from campaign: " + + potentialCampaignDir.filename().string() ); + } + } + } + if ( boost::filesystem::is_empty( potentialPartitionDir, ec ) ) + { + boost::filesystem::remove( potentialPartitionDir, ec ); + } + } + } + } + if ( boost::filesystem::is_empty( potentialCampaignDir, ec ) ) + { + boost::filesystem::remove( potentialCampaignDir, ec ); + } + } + + removeOlderRecords(); +} + +void +StreamManager::removeOlderRecords() +{ + for ( auto campaign : mCampaigns ) + { + for ( PartitionID pID = 0; pID < campaign.second.partitions.size(); ++pID ) + { + auto partition = campaign.second.partitions[pID]; + auto minimumTTl = campaign.second.config->getStoreAndForwardConfiguration()[pID] + .storageOptions.minimumTimeToLiveInSeconds; + if ( minimumTTl > 0 ) + { + auto ttlMs = minimumTTl * 1000; + Timestamp removeTime = mClock->systemTimeSinceEpochMs(); + if ( removeTime >= ttlMs ) + { + removeTime -= ttlMs; + } + FWE_LOG_INFO( "Cleaning up records older than " + std::to_string( removeTime ) + " for campaign " + + campaign.first + " partition " + std::to_string( pID ) ); + auto totalSizeBytes = partition.stream->removeOlderRecords( static_cast( removeTime ) ); + TraceModule::get().addToVariable( TraceVariable::DATA_EXPIRED_BYTES, totalSizeBytes ); + } + } + } +} + +StreamManager::ReturnCode +StreamManager::appendToStreams( const TriggeredCollectionSchemeData &data ) +{ + // Holding lock for the entire method because we do not want to append to a stream which is being deleted + // by reconfiguring the campaigns. The assumption is that this is low-contention because campaigns change + // infrequently. + std::lock_guard lock( mCampaignsMutex ); + + CampaignName campaign = getName( data.metadata.campaignArn ); + if ( mCampaigns.find( campaign ) == mCampaigns.end() ) + { + return ReturnCode::STREAM_NOT_FOUND; + } + if ( data.signals.empty() && data.canFrames.empty() ) + { + return ReturnCode::EMPTY_DATA; + } + + TriggeredCollectionSchemeData emptyChunk; + emptyChunk.metadata = data.metadata; + emptyChunk.triggerTime = data.triggerTime; + emptyChunk.eventID = data.eventID; + + for ( const auto &partition : mCampaigns[campaign].partitions ) + { + // each partition requires its own chunk + TriggeredCollectionSchemeData currChunk = emptyChunk; + uint32_t currChunkSize = 0; + for ( auto collectedSignal : data.signals ) + { + if ( partition.signalIDs.find( collectedSignal.signalID ) != partition.signalIDs.end() ) + { + currChunk.signals.emplace_back( collectedSignal ); + currChunkSize++; + if ( currChunkSize >= mTransmitThreshold ) + { + auto res = store( currChunk, partition ); + if ( res != ReturnCode::SUCCESS ) + { + return res; + } + currChunk = emptyChunk; + currChunkSize = 0; + } + } + } + + // currently, only signals are partitions. + // CAN frames will go in partition 0 (default). + // DTC info is not currenlty supported. + if ( partition.id == DEFAULT_PARTITION ) + { + for ( auto frame : data.canFrames ) + { + currChunk.canFrames.emplace_back( frame ); + currChunkSize++; + if ( currChunkSize >= mTransmitThreshold ) + { + auto res = store( currChunk, partition ); + if ( res != ReturnCode::SUCCESS ) + { + return res; + } + currChunk = emptyChunk; + currChunkSize = 0; + } + } + } + + // send out the chunk if we haven't already + if ( currChunkSize > 0 ) + { + auto res = store( currChunk, partition ); + if ( res != ReturnCode::SUCCESS ) + { + return res; + } + } + } + return ReturnCode::SUCCESS; +} + +StreamManager::ReturnCode +StreamManager::store( const TriggeredCollectionSchemeData &data, const Partition &partition ) +{ + std::string dataToStore; + if ( !serialize( data, dataToStore ) ) + { + FWE_LOG_WARN( "Failed to serialize data. cID: " + data.metadata.collectionSchemeID + + " partition: " + std::to_string( partition.id ) ) + return ReturnCode::ERROR; + } + + if ( mCampaigns[getName( data.metadata.campaignArn )].config->isCompressionNeeded() ) + { + std::string out; + if ( snappy::Compress( dataToStore.data(), dataToStore.size(), &out ) == 0U ) + { + FWE_LOG_TRACE( "Error in compressing the payload. cID: " + data.metadata.collectionSchemeID + + " partition: " + std::to_string( partition.id ) ); + return ReturnCode::ERROR; + } + dataToStore = out; + } + + auto dataBufSize = sizeof( RecordMetadata ) + dataToStore.size(); + std::vector dataBuf( dataBufSize ); + + auto metadata = RecordMetadata{ data.signals.size(), data.triggerTime }; + std::memcpy( dataBuf.data(), &metadata, sizeof( RecordMetadata ) ); + + std::memcpy( dataBuf.data() + sizeof( RecordMetadata ), dataToStore.data(), dataToStore.size() ); + + auto append_or = partition.stream->append( aws::store::common::BorrowedSlice{ dataBuf.data(), dataBuf.size() }, + aws::store::stream::AppendOptions{ true, true } ); + if ( !append_or.ok() ) + { + FWE_LOG_WARN( "Failed to append data to stream. cID: " + data.metadata.collectionSchemeID + + " partition: " + std::to_string( partition.id ) + + " errCode: " + std::to_string( static_cast( append_or.err().code ) ) + + " errMsg: " + append_or.err().msg ) + TraceModule::get().incrementVariable( TraceVariable::DATA_STORE_ERROR ); + return ReturnCode::ERROR; + } + + TraceModule::get().addToVariable( TraceVariable::DATA_STORE_BYTES, dataToStore.size() ); + TraceModule::get().addToVariable( TraceVariable::DATA_STORE_SIGNAL_COUNT, data.signals.size() ); + return ReturnCode::SUCCESS; +} + +bool +StreamManager::serialize( const TriggeredCollectionSchemeData &data, std::string &out ) +{ + auto dataPtr = std::make_shared( data ); + mProtoWriter->setupVehicleData( dataPtr, data.eventID ); + + // Add signals to the protobuf + for ( const auto &signal : dataPtr->signals ) + { + { + mProtoWriter->append( signal ); + } + } + + // Add raw CAN frames to the protobuf + for ( const auto &canFrame : dataPtr->canFrames ) + { + mProtoWriter->append( canFrame ); + } + + // Add DTC info to the payload + if ( dataPtr->mDTCInfo.hasItems() ) + { + mProtoWriter->setupDTCInfo( dataPtr->mDTCInfo ); + const auto &dtcCodes = dataPtr->mDTCInfo.mDTCCodes; + for ( const auto &dtc : dtcCodes ) + { + mProtoWriter->append( dtc ); + } + } + + return mProtoWriter->serializeVehicleData( &out ); +} + +StreamManager::ReturnCode +StreamManager::readFromStream( const CampaignID &cID, + PartitionID pID, + std::string &serializedData, + RecordMetadata &metadata, + std::function &checkpoint ) +{ + std::shared_ptr stream; + { + auto campaign = getName( cID ); + std::lock_guard lock( mCampaignsMutex ); + if ( mCampaigns.find( campaign ) == mCampaigns.end() ) + { + return StreamManager::ReturnCode::STREAM_NOT_FOUND; + } + if ( pID >= mCampaigns[campaign].partitions.size() ) + { + return StreamManager::ReturnCode::STREAM_NOT_FOUND; + } + stream = mCampaigns[campaign].partitions[pID].stream; + } + + auto iter = stream->openOrCreateIterator( STREAM_ITER_IDENTIFIER, aws::store::stream::IteratorOptions{} ); + + auto streamRecord = *iter; + if ( !streamRecord.ok() ) + { + if ( streamRecord.err().code == aws::store::stream::StreamErrorCode::RecordNotFound ) + { + return StreamManager::ReturnCode::END_OF_STREAM; + } + FWE_LOG_WARN( "Unable to read stream record. cID: " + cID + " partition: " + std::to_string( pID ) + + " errCode: " + std::to_string( static_cast( streamRecord.err().code ) ) + + " errMsg: " + streamRecord.err().msg ) + return StreamManager::ReturnCode::ERROR; + } + + checkpoint = [stream, sequenceNumber = iter.sequence_number, cID, pID]() { + auto err = stream->setCheckpoint( STREAM_ITER_IDENTIFIER, sequenceNumber ); + if ( !err.ok() ) + { + // TODO is there anything else we can do besides log? + FWE_LOG_ERROR( "Unable to checkpoint stream. cID: " + cID + " partition: " + std::to_string( pID ) + + " sequenceNumber: " + std::to_string( sequenceNumber ) ); + } + }; + + // metadata + if ( streamRecord.val().data.size() >= sizeof( RecordMetadata ) ) + { + std::copy( reinterpret_cast( streamRecord.val().data.data() ), + reinterpret_cast( streamRecord.val().data.data() ) + sizeof( RecordMetadata ), + reinterpret_cast( &metadata ) ); + } + + // data + if ( streamRecord.val().data.size() > sizeof( RecordMetadata ) ) + { + auto data = streamRecord.val().data.char_data() + sizeof( RecordMetadata ); + serializedData = std::string{ data, streamRecord.val().data.size() - sizeof( RecordMetadata ) }; + } + + return StreamManager::ReturnCode::SUCCESS; +} + +bool +StreamManager::hasCampaign( const CampaignID &campaignID ) +{ + auto campaign = getName( campaignID ); + std::lock_guard lock( mCampaignsMutex ); + return mCampaigns.find( campaign ) != mCampaigns.end(); +} + +std::set +StreamManager::getPartitionIdsFromCampaign( const CampaignID &campaignID ) +{ + auto campaign = getName( campaignID ); + + std::lock_guard lock( mCampaignsMutex ); + auto partitions = mCampaigns[campaign].partitions; + + std::set pIDs; + + for ( auto &partition : partitions ) + { + pIDs.insert( partition.id ); + } + return pIDs; +} + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/StreamManager.h b/src/StreamManager.h new file mode 100644 index 00000000..83bf04a8 --- /dev/null +++ b/src/StreamManager.h @@ -0,0 +1,143 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "DataSenderProtoWriter.h" +#include "ICollectionScheme.h" +#include "ICollectionSchemeList.h" +#include "SignalTypes.h" +#include "StoreLogger.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +using CampaignID = std::string; // full campaign arn +using CampaignName = std::string; // last part of a campaign id +using PartitionID = uint32_t; + +class StreamManager +{ + +public: + static const PartitionID DEFAULT_PARTITION; + static const std::string STREAM_ITER_IDENTIFIER; + static const std::string KV_STORE_IDENTIFIER; + static const int32_t KV_COMPACT_AFTER; + static const uint32_t STREAM_DEFAULT_MIN_SEGMENT_SIZE; + + enum class ReturnCode + { + // The operation completed successfully. + SUCCESS = 0, + // The stream associated with the operation is not part of + // any campaign known to Stream Manager. + STREAM_NOT_FOUND, + // The stream iterator has reached the current end of the stream. + // Please note that records can be added to the stream at any time, + // so operations that return this code will likely eventually succeed. + END_OF_STREAM, + // If data is empty (no signal information), it will not be stored. + EMPTY_DATA, + // A catch-all status indicating the operation failed. + ERROR + }; + + struct RecordMetadata + { + size_t numSignals; + Timestamp triggerTime; + }; + + StreamManager( std::string persistenceRootDir, + std::shared_ptr protoWriter, + uint32_t transmitThreshold ); + virtual ~StreamManager() = default; + void onChangeCollectionSchemeList( const std::shared_ptr &activeCollectionSchemes ); + + virtual ReturnCode appendToStreams( const TriggeredCollectionSchemeData &data ); + + /** + * Read a record directly from the stream for a specific campaign and partition. + * + * @param cID campaign id + * @param pID partition id + * @param serializedData record data + * @param metadata record metadata + * @param checkpoint function that checkpoints the stream and advances the iterator to the next record + * @return code describing the result + */ + virtual ReturnCode readFromStream( const CampaignID &cID, + PartitionID pID, + std::string &serializedData, + RecordMetadata &metadata, + std::function &checkpoint ); + + virtual bool hasCampaign( const CampaignID &campaignID ); + + virtual std::set getPartitionIdsFromCampaign( const CampaignID &campaignID ); + + static CampaignName + getName( const CampaignID &campaignID ) + { + auto lastArnSeperator = campaignID.find_last_of( '/' ); + if ( ( lastArnSeperator == CampaignID::npos ) || ( lastArnSeperator + 1 == campaignID.size() ) ) + { + return campaignID; + } + return campaignID.substr( lastArnSeperator + 1 ); + } + +private: + struct Partition + { + PartitionID id; + std::shared_ptr stream; + std::unordered_set signalIDs; + }; + + struct Campaign + { + std::vector partitions; + std::shared_ptr config; + }; + + std::shared_ptr mProtoWriter; + uint32_t mTransmitThreshold; + + std::string mPersistenceRootDir; + std::shared_ptr mLogger; + + std::map mCampaigns; + std::mutex mCampaignsMutex; + + std::shared_ptr mClock = ClockHandler::getClock(); + + bool serialize( const TriggeredCollectionSchemeData &data, std::string &out ); + void removeOlderRecords(); + + ReturnCode store( const TriggeredCollectionSchemeData &data, const Partition &partition ); +}; + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/TelemetryDataSender.cpp b/src/TelemetryDataSender.cpp index a58bbcdc..d0966dd8 100644 --- a/src/TelemetryDataSender.cpp +++ b/src/TelemetryDataSender.cpp @@ -6,6 +6,7 @@ #include "LoggingModule.h" #include "OBDDataTypes.h" #include "SignalTypes.h" +#include "TopicConfig.h" #include "TraceModule.h" #include #include @@ -150,6 +151,9 @@ TelemetryDataSender::processData( std::shared_ptr data, OnData case SignalType::BOOLEAN: firstSignalValues += std::to_string( static_cast( signalValue.value.boolVal ) ) + ","; break; + case SignalType::STRING: + firstSignalValues += std::to_string( signalValue.value.uint32Val ) + ","; + break; case SignalType::UNKNOWN: // Signal of type UNKNOWN cannot be processed break; @@ -286,6 +290,27 @@ TelemetryDataSender::compress( std::string &input, std::string &output ) const return true; } +#ifdef FWE_FEATURE_STORE_AND_FORWARD +void +TelemetryDataSender::processSerializedData( std::string &data, OnDataProcessedCallback callback ) +{ + mMQTTSender->sendBuffer( mMQTTSender->getTopicConfig().telemetryDataTopic, + reinterpret_cast( data.data() ), + data.size(), + [callback, data]( ConnectivityError result ) { + auto success = result == ConnectivityError::Success; + if ( success ) + { + FWE_LOG_INFO( "A Payload of size: " + std::to_string( data.size() ) + + " bytes has been uploaded" ); + TraceModule::get().addToVariable( TraceVariable::DATA_FORWARD_BYTES, data.size() ); + TraceModule::get().incrementVariable( TraceVariable::VEHICLE_DATA_PUBLISH_COUNT ); + } + callback( success, nullptr ); + } ); +} +#endif + size_t TelemetryDataSender::uploadProto( OnDataProcessedCallback callback, unsigned recursionLevel ) { @@ -352,6 +377,7 @@ TelemetryDataSender::uploadProto( OnDataProcessedCallback callback, unsigned rec mPartNumber++; mMQTTSender->sendBuffer( + mMQTTSender->getTopicConfig().telemetryDataTopic, reinterpret_cast( protoOutput->data() ), protoOutput->size(), [callback, @@ -417,16 +443,17 @@ TelemetryDataSender::processPersistedData( std::istream &data, auto buf = reinterpret_cast( dataAsArray.data() ); auto bufSize = static_cast( size ); - mMQTTSender->sendBuffer( buf, bufSize, [callback, size]( ConnectivityError result ) { - if ( result != ConnectivityError::Success ) - { - callback( false ); - return; - } + mMQTTSender->sendBuffer( + mMQTTSender->getTopicConfig().telemetryDataTopic, buf, bufSize, [callback, size]( ConnectivityError result ) { + if ( result != ConnectivityError::Success ) + { + callback( false ); + return; + } - FWE_LOG_INFO( "A Payload of size: " + std::to_string( size ) + " bytes has been uploaded" ); - callback( true ); - } ); + FWE_LOG_INFO( "A Payload of size: " + std::to_string( size ) + " bytes has been uploaded" ); + callback( true ); + } ); } } // namespace IoTFleetWise diff --git a/src/TelemetryDataSender.h b/src/TelemetryDataSender.h index f89c4e29..84bbfcf7 100644 --- a/src/TelemetryDataSender.h +++ b/src/TelemetryDataSender.h @@ -45,6 +45,10 @@ class TelemetryDataSender : public DataSender void processData( std::shared_ptr data, OnDataProcessedCallback callback ) override; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + void processSerializedData( std::string &data, OnDataProcessedCallback callback ); +#endif + void processPersistedData( std::istream &data, const Json::Value &metadata, OnPersistedDataProcessedCallback callback ) override; diff --git a/src/TopicConfig.h b/src/TopicConfig.h new file mode 100644 index 00000000..fd2d1318 --- /dev/null +++ b/src/TopicConfig.h @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "IConnectionTypes.h" +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +struct TopicConfigArgs +{ + boost::optional iotFleetWisePrefix; + boost::optional deviceShadowPrefix; + boost::optional commandsPrefix; + boost::optional jobsPrefix; + std::string metricsTopic; + std::string logsTopic; +}; + +struct TopicConfig +{ + const std::string iotFleetWisePrefix; + const std::string deviceShadowPrefix; + const std::string namedDeviceShadowPrefix; + const std::string commandsPrefix; + const std::string jobsPrefix; + + const std::string telemetryDataTopic; + const std::string checkinsTopic; + const std::string collectionSchemesTopic; + const std::string decoderManifestTopic; + const std::string metricsTopic; + const std::string logsTopic; + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + const std::string lastKnownStateDataTopic; + const std::string lastKnownStateConfigTopic; +#endif + +#ifdef FWE_FEATURE_REMOTE_COMMANDS + const std::string commandRequestTopic; + const std::string commandResponseAcceptedTopic; + const std::string commandResponseRejectedTopic; +#endif + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + const std::string getPendingJobExecutionsTopic; + const std::string getPendingJobExecutionsAcceptedTopic; + const std::string getPendingJobExecutionsRejectedTopic; + const std::string getJobExecutionAcceptedTopic; + const std::string getJobExecutionRejectedTopic; + const std::string updateJobExecutionAcceptedTopic; + const std::string updateJobExecutionRejectedTopic; + const std::string jobNotificationTopic; + const std::string jobCancellationInProgressTopic; +#endif + + TopicConfig( const std::string &thingName, const TopicConfigArgs &topicConfigArgs ) + : iotFleetWisePrefix( topicConfigArgs.iotFleetWisePrefix.value_or( "$aws/iotfleetwise/" ) + "vehicles/" + + thingName + "/" ) + , deviceShadowPrefix( topicConfigArgs.deviceShadowPrefix.value_or( "$aws/things/" ) + thingName + "/shadow/" ) + , namedDeviceShadowPrefix( deviceShadowPrefix + "name/" ) + , commandsPrefix( topicConfigArgs.commandsPrefix.value_or( "$aws/commands/" ) + "things/" + thingName + "/" ) + , jobsPrefix( topicConfigArgs.jobsPrefix.value_or( "$aws/things/" ) + thingName + "/jobs/" ) + , telemetryDataTopic( iotFleetWisePrefix + "signals" ) + , checkinsTopic( iotFleetWisePrefix + "checkins" ) + , collectionSchemesTopic( iotFleetWisePrefix + "collection_schemes" ) + , decoderManifestTopic( iotFleetWisePrefix + "decoder_manifests" ) + , metricsTopic( topicConfigArgs.metricsTopic ) + , logsTopic( topicConfigArgs.logsTopic ) +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + , lastKnownStateDataTopic( iotFleetWisePrefix + "last_known_states/data" ) + , lastKnownStateConfigTopic( iotFleetWisePrefix + "last_known_states/config" ) +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , commandRequestTopic( commandsPrefix + "executions/+/request/protobuf" ) + , commandResponseAcceptedTopic( commandsPrefix + "executions/+/response/accepted/protobuf" ) + , commandResponseRejectedTopic( commandsPrefix + "executions/+/response/rejected/protobuf" ) +#endif +#ifdef FWE_FEATURE_STORE_AND_FORWARD + , getPendingJobExecutionsTopic( jobsPrefix + "get" ) + , getPendingJobExecutionsAcceptedTopic( jobsPrefix + "get/accepted" ) + , getPendingJobExecutionsRejectedTopic( jobsPrefix + "get/rejected" ) + , getJobExecutionAcceptedTopic( jobsPrefix + "+/get/accepted" ) + , getJobExecutionRejectedTopic( jobsPrefix + "+/get/rejected" ) + , updateJobExecutionAcceptedTopic( jobsPrefix + "+/update/accepted" ) + , updateJobExecutionRejectedTopic( jobsPrefix + "+/update/rejected" ) + , jobNotificationTopic( jobsPrefix + "notify" ) + , jobCancellationInProgressTopic( "$aws/events/job/+/cancellation_in_progress" ) +#endif + { + } + + ~TopicConfig() = default; + +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::string + commandResponseTopic( const std::string &commandId ) const + { + return commandsPrefix + "executions/" + commandId + "/response/protobuf"; + } +#endif + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + std::string + getJobExecutionTopic( const std::string &jobId ) const + { + return jobsPrefix + jobId + "/get"; + } + + std::string + updateJobExecutionTopic( const std::string &jobId ) const + { + return jobsPrefix + jobId + "/update"; + } +#endif + +#ifdef FWE_FEATURE_SOMEIP + std::string + getDeviceShadowTopic( const std::string &shadowName ) const + { + return ( shadowName.empty() ? deviceShadowPrefix : namedDeviceShadowPrefix + shadowName + "/" ) + "get"; + } + + std::string + updateDeviceShadowTopic( const std::string &shadowName ) const + { + return ( shadowName.empty() ? deviceShadowPrefix : namedDeviceShadowPrefix + shadowName + "/" ) + "update"; + } + + std::string + deleteDeviceShadowTopic( const std::string &shadowName ) const + { + return ( shadowName.empty() ? deviceShadowPrefix : namedDeviceShadowPrefix + shadowName + "/" ) + "delete"; + } +#endif +}; + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/src/TraceModule.cpp b/src/TraceModule.cpp index 3a6b5f9e..eca19c7a 100644 --- a/src/TraceModule.cpp +++ b/src/TraceModule.cpp @@ -145,6 +145,8 @@ TraceModule::getVariableName( TraceVariable variable ) return "MqttHeapSize"; case TraceVariable::SIGNAL_BUFFER_SIZE: return "SigBufSize"; + case TraceVariable::LAST_KNOWN_STATE_SIGNAL_HISTORY_BUFFER_SIZE: + return "LastKnownStateSigHistoryBufSize"; case TraceVariable::RAW_DATA_OVERWRITTEN_DATA_WITH_USED_HANDLE: return "RawOverw_id62"; case TraceVariable::RAW_DATA_BUFFER_ELEMENTS_PER_TYPE: @@ -157,8 +159,48 @@ TraceModule::getVariableName( TraceVariable variable ) return "CEProcessedDataFrames"; case TraceVariable::CE_PROCESSED_DTCS: return "CEProcessedDTCs"; + case TraceVariable::MQTT_COMMAND_RESPONSE_MESSAGE_SENT_OUT: + return "MqttCommandResponseMessage"; + case TraceVariable::COMMAND_REQUESTS_RECEIVED: + return "CommandRequestReceived"; + case TraceVariable::COMMAND_EXECUTION_FAILURE: + return "CommandExecutionFailure"; + case TraceVariable::COMMAND_PRECONDITION_CHECK_FAILURE: + return "CommandPreconditionFailure"; + case TraceVariable::COMMAND_EXECUTION_TIMEOUT: + return "CommandExecutionTimeout"; + case TraceVariable::COMMAND_DECODER_MANIFEST_FAILURE: + return "CommandDecoderFailure"; + case TraceVariable::STATE_TEMPLATES_RECEIVED: + return "LastKnownStateReceived"; + case TraceVariable::LAST_KNOWN_STATE_COLLECTION_TRIGGERS: + return "LastKnownStateCollectionTriggered"; + case TraceVariable::MQTT_LAST_KNOWN_STATE_MESSAGE_SENT_OUT: + return "MqttLastKnownStateResponseMessage"; + case TraceVariable::MQTT_LAST_KNOWN_STATE_MESSAGE_FAILED_TO_BE_SENT: + return "MqttLastKnownStateResponseMessageFailed"; + case TraceVariable::LAST_KNOWN_STATE_PERIODIC_UPDATES: + return "LastKnownStatePeriodicUpdates"; + case TraceVariable::LAST_KNOWN_STATE_ON_CHANGE_UPDATES: + return "LastKnownStateOnChangeUpdates"; + case TraceVariable::LAST_KNOWN_STATE_NO_SIGNAL_CHANGE_ON_PERIODIC_UPDATE: + return "LastKnownStateNoSignalChangeOnPeriodicUpdate"; + case TraceVariable::DATA_STORE_BYTES: + return "DataStoreBytes"; + case TraceVariable::DATA_STORE_SIGNAL_COUNT: + return "DataStoreSignalSampleCount"; case TraceVariable::DATA_FORWARD_BYTES: return "DataForwardBytes"; + case TraceVariable::DATA_FORWARD_SIGNAL_COUNT: + return "DataForwardSignalSampleCount"; + case TraceVariable::DATA_EXPIRED_BYTES: + return "DataExpiredBytes"; + case TraceVariable::DATA_DROPPED_BYTES: + return "DataDroppedBytes"; + case TraceVariable::DATA_STORE_ERROR: + return "DataStoreError"; + case TraceVariable::DATA_FORWARD_ERROR: + return "DataForwardError"; case TraceVariable::VEHICLE_DATA_PUBLISH_COUNT: return "VehicleDataPublishCount"; // Intentionally omit default so that we can use compiler warnings to remind us about missing values @@ -183,6 +225,14 @@ TraceModule::getAtomicVariableName( TraceAtomicVariable variable ) return "QueueConsumerToInspectionDataFrames"; case TraceAtomicVariable::QUEUE_CONSUMER_TO_INSPECTION_DTCS: return "QueueConsumerToInspectionDtcs"; + case TraceAtomicVariable::QUEUE_PENDING_COMMAND_REQUESTS: + return "QueuePendingCommandRequests"; + case TraceAtomicVariable::QUEUE_PENDING_COMMAND_RESPONSES: + return "QueuePendingCommandResponses"; + case TraceAtomicVariable::QUEUE_CONSUMER_TO_LAST_KNOWN_STATE_INSPECTION: + return "QueueConsumerToLastKnownStateInspection"; + case TraceAtomicVariable::QUEUE_LAST_KNOWN_STATE_INSPECTION_TO_SENDER: + return "QueueLastKnownStateInspectionToSender"; case TraceAtomicVariable::QUEUE_INSPECTION_TO_SENDER: return "QStS_id40"; case TraceAtomicVariable::NOT_TIME_MONOTONIC_FRAMES: @@ -201,6 +251,8 @@ TraceModule::getAtomicVariableName( TraceAtomicVariable variable ) return "ConRes_id8"; case TraceAtomicVariable::COLLECTION_SCHEME_ERROR: return "CampaignFailures"; + case TraceAtomicVariable::STATE_TEMPLATE_ERROR: + return "StateTemplateFailures"; // Intentionally omit default so that we can use compiler warnings to remind us about missing values } return nullptr; @@ -225,6 +277,8 @@ TraceModule::getSectionName( TraceSection section ) return "DEC_BUILD_id3"; case TraceSection::MANAGER_COLLECTION_BUILD: return "COL_BUILD_id4"; + case TraceSection::MANAGER_LAST_KNOWN_STATE_BUILD: + return "StateTemplatesBuild"; case TraceSection::MANAGER_EXTRACTION: return "EXTRACT_id5"; case TraceSection::CAN_DECODER_CYCLE_0: diff --git a/src/TraceModule.h b/src/TraceModule.h index 44ecd828..5d38cc33 100644 --- a/src/TraceModule.h +++ b/src/TraceModule.h @@ -68,11 +68,32 @@ enum class TraceVariable MQTT_SIGNAL_MESSAGES_SENT_OUT, // Can be multiple messages per event id MQTT_HEAP_USAGE, SIGNAL_BUFFER_SIZE, + LAST_KNOWN_STATE_SIGNAL_HISTORY_BUFFER_SIZE, RAW_DATA_OVERWRITTEN_DATA_WITH_USED_HANDLE, RAW_DATA_BUFFER_ELEMENTS_PER_TYPE, RAW_DATA_BUFFER_MANAGER_BYTES, + MQTT_COMMAND_RESPONSE_MESSAGE_SENT_OUT, + COMMAND_REQUESTS_RECEIVED, + COMMAND_EXECUTION_FAILURE, + COMMAND_PRECONDITION_CHECK_FAILURE, + COMMAND_EXECUTION_TIMEOUT, + COMMAND_DECODER_MANIFEST_FAILURE, + STATE_TEMPLATES_RECEIVED, + LAST_KNOWN_STATE_COLLECTION_TRIGGERS, + MQTT_LAST_KNOWN_STATE_MESSAGE_SENT_OUT, + MQTT_LAST_KNOWN_STATE_MESSAGE_FAILED_TO_BE_SENT, + LAST_KNOWN_STATE_PERIODIC_UPDATES, + LAST_KNOWN_STATE_ON_CHANGE_UPDATES, + LAST_KNOWN_STATE_NO_SIGNAL_CHANGE_ON_PERIODIC_UPDATE, + DATA_STORE_BYTES, + DATA_STORE_SIGNAL_COUNT, QUEUED_S3_OBJECTS, DATA_FORWARD_BYTES, + DATA_FORWARD_SIGNAL_COUNT, + DATA_EXPIRED_BYTES, + DATA_DROPPED_BYTES, + DATA_STORE_ERROR, + DATA_FORWARD_ERROR, VEHICLE_DATA_PUBLISH_COUNT, // If you add more, remember to add the name to TraceModule::getVariableName TRACE_VARIABLE_SIZE @@ -88,6 +109,10 @@ enum class TraceAtomicVariable QUEUE_CONSUMER_TO_INSPECTION_DATA_FRAMES, QUEUE_CONSUMER_TO_INSPECTION_DTCS, QUEUE_INSPECTION_TO_SENDER, + QUEUE_PENDING_COMMAND_REQUESTS, + QUEUE_PENDING_COMMAND_RESPONSES, + QUEUE_CONSUMER_TO_LAST_KNOWN_STATE_INSPECTION, + QUEUE_LAST_KNOWN_STATE_INSPECTION_TO_SENDER, NOT_TIME_MONOTONIC_FRAMES, SUBSCRIBE_ERROR, SUBSCRIBE_REJECT, @@ -96,6 +121,7 @@ enum class TraceAtomicVariable CONNECTION_INTERRUPTED, CONNECTION_RESUMED, COLLECTION_SCHEME_ERROR, + STATE_TEMPLATE_ERROR, // If you add more, remember to add the name to TraceModule::getAtomicVariableName TRACE_ATOMIC_VARIABLE_SIZE }; @@ -111,6 +137,7 @@ enum class TraceSection FWE_SHUTDOWN, MANAGER_DECODER_BUILD, MANAGER_COLLECTION_BUILD, + MANAGER_LAST_KNOWN_STATE_BUILD, MANAGER_EXTRACTION, CAN_DECODER_CYCLE_0, CAN_DECODER_CYCLE_1, diff --git a/src/VehicleDataSourceTypes.h b/src/VehicleDataSourceTypes.h index 7e1ef079..e21de558 100644 --- a/src/VehicleDataSourceTypes.h +++ b/src/VehicleDataSourceTypes.h @@ -16,15 +16,17 @@ enum class VehicleDataSourceProtocol INVALID_PROTOCOL, OBD, RAW_SOCKET, + CUSTOM_DECODING, #ifdef FWE_FEATURE_VISION_SYSTEM_DATA COMPLEX_DATA #endif // Add any new protocols to the list of supported protocols below }; -constexpr std::array SUPPORTED_NETWORK_PROTOCOL = { +constexpr std::array SUPPORTED_NETWORK_PROTOCOL = { { VehicleDataSourceProtocol::RAW_SOCKET, VehicleDataSourceProtocol::OBD, + VehicleDataSourceProtocol::CUSTOM_DECODING, #ifdef FWE_FEATURE_VISION_SYSTEM_DATA VehicleDataSourceProtocol::COMPLEX_DATA #endif diff --git a/src/android_shared_library.cpp b/src/android_shared_library.cpp index 824c65da..03b3f6d1 100644 --- a/src/android_shared_library.cpp +++ b/src/android_shared_library.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #define LOG_TAG "FWE" #define LOGE( x ) __android_log_print( ANDROID_LOG_ERROR, LOG_TAG, "%s", ( x ).c_str() ) @@ -126,11 +128,7 @@ Java_com_aws_iotfleetwise_Fwe_run( JNIEnv *env, mqttConnection["certificate"] = certificate; mqttConnection["rootCA"] = rootCA; mqttConnection["clientId"] = vehicleName; - mqttTopicPrefix += "vehicles/" + vehicleName; - mqttConnection["collectionSchemeListTopic"] = mqttTopicPrefix + "/collection_schemes"; - mqttConnection["decoderManifestTopic"] = mqttTopicPrefix + "/decoder_manifests"; - mqttConnection["canDataTopic"] = mqttTopicPrefix + "/signals"; - mqttConnection["checkinTopic"] = mqttTopicPrefix + "/checkins"; + mqttConnection["iotFleetWiseTopicPrefix"] = mqttTopicPrefix; // Set system wide log level configureLogging( config ); @@ -321,6 +319,115 @@ Java_com_aws_iotfleetwise_Fwe_ingestCanMessage( interfaceId, static_cast( timestamp ), static_cast( messageId ), data ); } +extern "C" JNIEXPORT void JNICALL +Java_com_aws_iotfleetwise_Fwe_ingestSignalValueByName( + JNIEnv *env, jobject me, jlong timestamp, jstring nameJString, jobject value ) +{ + static_cast( me ); + if ( mEngine == nullptr ) + { + return; + } + + auto nameCString = env->GetStringUTFChars( nameJString, 0 ); + std::string name( nameCString ); + env->ReleaseStringUTFChars( nameJString, nameCString ); + jclass doubleJClass = env->FindClass( "java/lang/Double" ); + jclass longJClass = env->FindClass( "java/lang/Long" ); + + if ( env->IsInstanceOf( value, doubleJClass ) ) + { + jmethodID doubleJMethodIdDoubleValue = env->GetMethodID( doubleJClass, "doubleValue", "()D" ); + jdouble doubleValue = env->CallDoubleMethod( value, doubleJMethodIdDoubleValue ); + mEngine->ingestSignalValueByName( + static_cast( timestamp ), + name, + Aws::IoTFleetWise::DecodedSignalValue( doubleValue, Aws::IoTFleetWise::SignalType::DOUBLE ) ); + } + else if ( env->IsInstanceOf( value, longJClass ) ) + { + jmethodID longJMethodIdLongValue = env->GetMethodID( longJClass, "longValue", "()J" ); + jlong longValue = env->CallLongMethod( value, longJMethodIdLongValue ); + mEngine->ingestSignalValueByName( + static_cast( timestamp ), + name, + Aws::IoTFleetWise::DecodedSignalValue( longValue, Aws::IoTFleetWise::SignalType::INT64 ) ); + } + else + { + LOGE( std::string( "Unsupported value type" ) ); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_aws_iotfleetwise_Fwe_ingestMultipleSignalValuesByName( JNIEnv *env, + jobject me, + jlong timestamp, + jobject valuesJObject ) +{ + static_cast( me ); + if ( mEngine == nullptr ) + { + return; + } + + jclass valuesJClass = env->GetObjectClass( valuesJObject ); + jmethodID valuesJMethodIdEntrySet = env->GetMethodID( valuesJClass, "entrySet", "()Ljava/util/Set;" ); + jclass setJClass = env->FindClass( "java/util/Set" ); + jmethodID setJMethodIdIterator = env->GetMethodID( setJClass, "iterator", "()Ljava/util/Iterator;" ); + jclass iteratorJClass = env->FindClass( "java/util/Iterator" ); + jmethodID iteratorJMethodIdHasNext = env->GetMethodID( iteratorJClass, "hasNext", "()Z" ); + jmethodID iteratorJMethodIdNext = env->GetMethodID( iteratorJClass, "next", "()Ljava/lang/Object;" ); + jclass mapEntryJClass = env->FindClass( "java/util/Map$Entry" ); + jmethodID mapEntryJMethodIdGetKey = env->GetMethodID( mapEntryJClass, "getKey", "()Ljava/lang/Object;" ); + jmethodID mapEntryJMethodIdGetValue = env->GetMethodID( mapEntryJClass, "getValue", "()Ljava/lang/Object;" ); + jclass stringJClass = env->FindClass( "java/lang/String" ); + jmethodID stringJMethodIdToString = env->GetMethodID( stringJClass, "toString", "()Ljava/lang/String;" ); + jclass doubleJClass = env->FindClass( "java/lang/Double" ); + jmethodID doubleJMethodIdDoubleValue = env->GetMethodID( doubleJClass, "doubleValue", "()D" ); + jclass longJClass = env->FindClass( "java/lang/Long" ); + jmethodID longJMethodIdLongValue = env->GetMethodID( longJClass, "longValue", "()J" ); + + jobject valuesEntrySetJObject = env->CallObjectMethod( valuesJObject, valuesJMethodIdEntrySet ); + jobject valuesEntrySetIteratorJObject = env->CallObjectMethod( valuesEntrySetJObject, setJMethodIdIterator ); + + std::vector> values; + while ( env->CallBooleanMethod( valuesEntrySetIteratorJObject, iteratorJMethodIdHasNext ) ) + { + jobject entryJObject = env->CallObjectMethod( valuesEntrySetIteratorJObject, iteratorJMethodIdNext ); + jobject nameJObject = env->CallObjectMethod( entryJObject, mapEntryJMethodIdGetKey ); + jobject valueJObject = env->CallObjectMethod( entryJObject, mapEntryJMethodIdGetValue ); + jstring nameJString = (jstring)env->CallObjectMethod( nameJObject, stringJMethodIdToString ); + auto nameCString = env->GetStringUTFChars( nameJString, 0 ); + std::string name( nameCString ); + if ( env->IsInstanceOf( valueJObject, doubleJClass ) ) + { + jdouble value = env->CallDoubleMethod( valueJObject, doubleJMethodIdDoubleValue ); + values.emplace_back( + name, Aws::IoTFleetWise::DecodedSignalValue( value, Aws::IoTFleetWise::SignalType::DOUBLE ) ); + } + else if ( env->IsInstanceOf( valueJObject, longJClass ) ) + { + jlong value = env->CallLongMethod( valueJObject, longJMethodIdLongValue ); + values.emplace_back( name, + Aws::IoTFleetWise::DecodedSignalValue( value, Aws::IoTFleetWise::SignalType::INT64 ) ); + } + else + { + LOGE( std::string( "Unsupported value type" ) ); + } + env->ReleaseStringUTFChars( nameJString, nameCString ); + env->DeleteLocalRef( nameJString ); + env->DeleteLocalRef( valueJObject ); + env->DeleteLocalRef( nameJObject ); + env->DeleteLocalRef( entryJObject ); + } + env->DeleteLocalRef( valuesEntrySetIteratorJObject ); + env->DeleteLocalRef( valuesEntrySetJObject ); + + mEngine->ingestMultipleSignalValuesByName( static_cast( timestamp ), values ); +} + extern "C" JNIEXPORT jstring JNICALL Java_com_aws_iotfleetwise_Fwe_getStatusSummary( JNIEnv *env, jobject me ) { diff --git a/test/unit/AaosVhalSourceTest.cpp b/test/unit/AaosVhalSourceTest.cpp index 827a8e12..88ccf722 100644 --- a/test/unit/AaosVhalSourceTest.cpp +++ b/test/unit/AaosVhalSourceTest.cpp @@ -4,7 +4,7 @@ #include "AaosVhalSource.h" #include "CollectionInspectionAPITypes.h" #include "IDecoderDictionary.h" -#include "MessageTypes.h" +#include "IDecoderManifest.h" #include "QueueTypes.h" #include "SignalTypes.h" #include "VehicleDataSourceTypes.h" @@ -27,31 +27,15 @@ class AaosVhalSourceTest : public ::testing::Test void SetUp() override { - std::unordered_map frameMap; - CANMessageDecoderMethod decoderMethod; - decoderMethod.collectType = CANMessageCollectType::DECODE; - decoderMethod.format.mMessageID = 12345; - CANSignalFormat sig1; - CANSignalFormat sig2; - CANSignalFormat sig3; - sig1.mOffset = 0x0207; - sig1.mFirstBitPosition = 0xAA; - sig1.mSizeInBits = 0x55; - sig1.mSignalID = 0x1234; - sig2.mOffset = 0x0209; - sig2.mFirstBitPosition = 0x55; - sig2.mSizeInBits = 0xAA; - sig2.mSignalID = 0x5678; - sig3.mOffset = 0x020A; - sig3.mFirstBitPosition = 0x77; - sig3.mSizeInBits = 0xBB; - sig3.mSignalID = 0x8888; - decoderMethod.format.mSignals.push_back( sig1 ); - decoderMethod.format.mSignals.push_back( sig2 ); - decoderMethod.format.mSignals.push_back( sig3 ); - frameMap[1] = decoderMethod; - mDictionary = std::make_shared(); - mDictionary->canMessageDecoderMethod[1] = frameMap; + std::unordered_map customDecoders; + customDecoders["0x0207,0xAA,0x55"] = + CustomSignalDecoderFormat{ "5", "0x0207,0xAA,0x55", 0x1234, SignalType::DOUBLE }; + customDecoders["0x0209,0x55,0xAA"] = + CustomSignalDecoderFormat{ "5", "0x0209,0x55,0xAA", 0x5678, SignalType::INT8 }; + customDecoders["0x020A,0x77,0xBB"] = + CustomSignalDecoderFormat{ "5", "0x020A,0x77,0xBB", 0x8888, SignalType::INT64 }; + mDictionary = std::make_shared(); + mDictionary->customDecoderMethod["AAOS-VHAL"] = customDecoders; } void @@ -59,7 +43,7 @@ class AaosVhalSourceTest : public ::testing::Test { } - std::shared_ptr mDictionary; + std::shared_ptr mDictionary; }; TEST_F( AaosVhalSourceTest, testDecoding ) // NOLINT @@ -67,14 +51,11 @@ TEST_F( AaosVhalSourceTest, testDecoding ) // NOLINT auto signalBuffer = std::make_shared( 100, "Signal Buffer" ); auto signalBufferDistributor = std::make_shared(); signalBufferDistributor->registerQueue( signalBuffer ); - AaosVhalSource vhalSource( signalBufferDistributor ); - ASSERT_FALSE( vhalSource.init( INVALID_CAN_SOURCE_NUMERIC_ID, 1 ) ); - ASSERT_TRUE( vhalSource.init( 1, 1 ) ); - vhalSource.start(); + AaosVhalSource vhalSource( "AAOS-VHAL", signalBufferDistributor ); CollectedDataFrame collectedDataFrame; DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); - vhalSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + vhalSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); std::unordered_map> propInfoBySignalId; @@ -105,8 +86,8 @@ TEST_F( AaosVhalSourceTest, testDecoding ) // NOLINT ASSERT_EQ( secondSignal.signalID, 0x5678 ); ASSERT_EQ( thirdSignal.signalID, 0x8888 ); ASSERT_NEAR( firstSignal.value.value.doubleVal, 52.5761, 0.0001 ); - ASSERT_EQ( secondSignal.value.value.doubleVal, 12 ); - ASSERT_EQ( thirdSignal.value.value.doubleVal, 123456 ); + ASSERT_EQ( secondSignal.value.value.int8Val, 12 ); + ASSERT_EQ( thirdSignal.value.value.int64Val, 123456 ); } } // namespace IoTFleetWise diff --git a/test/unit/ActuatorCommandManagerTest.cpp b/test/unit/ActuatorCommandManagerTest.cpp new file mode 100644 index 00000000..270484ba --- /dev/null +++ b/test/unit/ActuatorCommandManagerTest.cpp @@ -0,0 +1,528 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "ActuatorCommandManager.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommandDispatcherMock.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "ICommandDispatcher.h" +#include "IDecoderManifest.h" +#include "QueueTypes.h" +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include "WaitUntil.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::InvokeArgument; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::Sequence; +using ::testing::StrictMock; + +constexpr uint32_t maxConcurrentCommandRequests = 3; + +class ActuatorCommandManagerTest : public ::testing::Test +{ +public: + ActuatorCommandManagerTest() + : mRawBufferManagerSpy( std::make_shared>( + RawData::BufferManagerConfig::create().get() ) ) + { + } + void + SetUp() override + { + mCommandResponses = std::make_shared( 100, "Command Responses" ); + mCommandDispatcher = std::make_shared>(); + + mActuatorCommandManager = std::make_unique( + mCommandResponses, maxConcurrentCommandRequests, mRawBufferManagerSpy ); + + mCommandResponses->subscribeToNewDataAvailable( [&]() { + mReadyToPublishCallbackCount++; + } ); + + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomSignalDecoderFormatMap = { + { 1, CustomSignalDecoderFormat{ "30", "custom-decoder-0" } }, + { 2, CustomSignalDecoderFormat{ "30", "custom-decoder-1" } }, + { 10, CustomSignalDecoderFormat{ "31", "custom-decoder-10" } }, + }; + ASSERT_TRUE( mActuatorCommandManager->registerDispatcher( "30", mCommandDispatcher ) ); + + mSignalIDToCustomSignalDecoderFormatMap = + std::make_shared( signalIDToCustomSignalDecoderFormatMap ); + + mDecoderManifestID = "dm1"; + + EXPECT_CALL( *mCommandDispatcher, init() ).WillRepeatedly( Return( true ) ); + } + + void + TearDown() override + { + mActuatorCommandManager->stop(); + } + + bool + popCommandResponse( std::shared_ptr &commandResponse ) + { + std::shared_ptr senderData; + auto succeeded = mCommandResponses->pop( senderData ); + commandResponse = std::dynamic_pointer_cast( senderData ); + return succeeded; + } + +protected: + std::shared_ptr> mRawBufferManagerSpy; + std::unique_ptr mActuatorCommandManager; + std::shared_ptr mCommandResponses; + std::shared_ptr> mCommandDispatcher; + std::atomic mReadyToPublishCallbackCount{ 0 }; + std::shared_ptr mSignalIDToCustomSignalDecoderFormatMap; + SyncID mDecoderManifestID; +}; + +TEST_F( ActuatorCommandManagerTest, DuplicateInterfaceId ) +{ + ASSERT_FALSE( mActuatorCommandManager->registerDispatcher( "30", mCommandDispatcher ) ); +} + +TEST_F( ActuatorCommandManagerTest, getActuatorNames ) +{ + EXPECT_CALL( *mCommandDispatcher, getActuatorNames() ).WillOnce( Return( std::vector{ "abc" } ) ); + auto names = mActuatorCommandManager->getActuatorNames(); + ASSERT_EQ( names.size(), 1 ); + ASSERT_EQ( names["30"].size(), 1 ); + ASSERT_EQ( names["30"][0], "abc" ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandSuccess ) +{ + EXPECT_CALL( *mCommandDispatcher, setActuatorValue( _, _, _, _, _, _ ) ) + .WillOnce( Invoke( []( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) { + static_cast( actuatorName ); + static_cast( signalValue ); + static_cast( commandId ); + static_cast( issuedTimestampMs ); + static_cast( executionTimeoutMs ); + notifyStatusCallback( CommandStatus::SUCCEEDED, 0x1234, "" ); + } ) ); + + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "successful-command"; + commandRequest.decoderID = mDecoderManifestID; + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest.executionTimeoutMs = 10000; + commandRequest.signalID = 1; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + // Validate only one command was successfully processed + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "successful-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, 0x1234 ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandNoDecoderManifest ) +{ + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "no-decoder-manifest-command"; + commandRequest.decoderID = mDecoderManifestID; + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest.executionTimeoutMs = 10000; + commandRequest.signalID = 1; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + // Validate only one command was successfully processed + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "no-decoder-manifest-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandDecoderManifestMismatch ) +{ + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "decoder-manifest-out-of-sync-command"; + commandRequest.decoderID = "wrong-dm-id"; + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest.executionTimeoutMs = 10000; + commandRequest.signalID = 1; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + // Validate only one command was successfully processed + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "decoder-manifest-out-of-sync-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandTimeout ) +{ + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "timed-out-command"; + commandRequest.decoderID = mDecoderManifestID; + // Set the issued time to be in the past, so that the command will have already expired + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs() - 1000; + commandRequest.executionTimeoutMs = 500; + commandRequest.signalID = 2; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + // Validate only one command was processed with the status timeout + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "timed-out-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_TIMEOUT ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_TIMED_OUT_BEFORE_DISPATCH ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandNoCustomDecoders ) +{ + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, nullptr ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "no-custom-decoder-command"; + commandRequest.decoderID = mDecoderManifestID; + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest.executionTimeoutMs = 10000; + commandRequest.signalID = 3; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "no-custom-decoder-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandNoCustomDecoder ) +{ + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "no-custom-decoder-command"; + commandRequest.decoderID = mDecoderManifestID; + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest.executionTimeoutMs = 10000; + commandRequest.signalID = 3; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + // Validate only one command was processed with the status timeout + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "no-custom-decoder-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_NO_DECODING_RULES_FOUND ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessCommandNoCommandDispatcherForInterface ) +{ + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest; + commandRequest.commandID = "no-dispatcher-command"; + commandRequest.decoderID = mDecoderManifestID; + commandRequest.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest.executionTimeoutMs = 10000; + commandRequest.signalID = 10; + commandRequest.signalValueWrapper.value.doubleVal = 10.5; + + ASSERT_EQ( mReadyToPublishCallbackCount, 0 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest ); + + // Validate only one command was processed with the status timeout + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "no-dispatcher-command" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_NO_COMMAND_DISPATCHER_FOUND ); + ASSERT_EQ( mReadyToPublishCallbackCount, 1 ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, ProcessMultipleCommands ) +{ + EXPECT_CALL( *mCommandDispatcher, setActuatorValue( _, _, _, _, _, _ ) ) + .WillOnce( Invoke( []( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) { + static_cast( actuatorName ); + static_cast( signalValue ); + static_cast( commandId ); + static_cast( issuedTimestampMs ); + static_cast( executionTimeoutMs ); + notifyStatusCallback( CommandStatus::SUCCEEDED, 0x1234, "" ); + } ) ) + .WillOnce( Invoke( []( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) { + static_cast( actuatorName ); + static_cast( signalValue ); + static_cast( commandId ); + static_cast( issuedTimestampMs ); + static_cast( executionTimeoutMs ); + notifyStatusCallback( CommandStatus::EXECUTION_FAILED, REASON_CODE_PRECONDITION_FAILED, "" ); + } ) ); + + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest1; + commandRequest1.commandID = "command1"; + commandRequest1.decoderID = mDecoderManifestID; + commandRequest1.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest1.executionTimeoutMs = 10000; + commandRequest1.signalID = 1; + commandRequest1.signalValueWrapper.value.doubleVal = 10.5; + + ActuatorCommandRequest commandRequest2; + commandRequest2.commandID = "command2"; + commandRequest2.decoderID = mDecoderManifestID; + commandRequest2.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest2.executionTimeoutMs = 10000; + commandRequest2.signalID = 2; + commandRequest2.signalValueWrapper.value.doubleVal = 20.5; + + ActuatorCommandRequest commandRequest3; + commandRequest3.commandID = "command3"; + commandRequest3.decoderID = mDecoderManifestID; + commandRequest3.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest3.executionTimeoutMs = 10000; + commandRequest3.signalID = 3; + commandRequest3.signalValueWrapper.value.doubleVal = 30.5; + + ActuatorCommandRequest commandRequest4; + commandRequest4.commandID = "command4"; + commandRequest4.decoderID = mDecoderManifestID; + commandRequest4.issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + commandRequest4.executionTimeoutMs = 10000; + commandRequest4.signalID = 4; + commandRequest4.signalValueWrapper.value.doubleVal = 40.5; + + mActuatorCommandManager->onReceivingCommandRequest( commandRequest1 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest2 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest3 ); + mActuatorCommandManager->onReceivingCommandRequest( commandRequest4 ); + + // Validate only one command was successfully processed + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, 0x1234 ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command2" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_PRECONDITION_FAILED ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command3" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_NO_DECODING_RULES_FOUND ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); +} + +TEST_F( ActuatorCommandManagerTest, NoCommandResponsesQueue ) +{ + std::unique_ptr commandManager = + std::make_unique( nullptr, maxConcurrentCommandRequests, mRawBufferManagerSpy ); + ASSERT_FALSE( commandManager->start() ); +} + +TEST_F( ActuatorCommandManagerTest, StringValue ) +{ + auto currentTime = ClockHandler::getClock()->systemTimeSinceEpochMs(); + mRawBufferManagerSpy->updateConfig( { { 1, { 1, "", "" } } } ); + std::string stringVal = "hello"; + auto handle = + mRawBufferManagerSpy->push( reinterpret_cast( stringVal.data() ), stringVal.size(), 1234, 1 ); + mRawBufferManagerSpy->increaseHandleUsageHint( 1, handle, RawData::BufferHandleUsageStage::UPLOADING ); + + EXPECT_CALL( *mCommandDispatcher, setActuatorValue( _, _, _, _, _, _ ) ) + .WillOnce( Invoke( [this, currentTime, handle, stringVal]( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) { + EXPECT_EQ( actuatorName, "custom-decoder-0" ); + EXPECT_EQ( signalValue.type, SignalType::STRING ); + EXPECT_EQ( commandId, "command1" ); + EXPECT_EQ( issuedTimestampMs, currentTime ); + EXPECT_EQ( executionTimeoutMs, 10000 ); + auto loanedFrame = mRawBufferManagerSpy->borrowFrame( signalValue.value.rawDataVal.signalId, + signalValue.value.rawDataVal.handle ); + EXPECT_FALSE( loanedFrame.isNull() ); + if ( !loanedFrame.isNull() ) + { + std::string receivedStringVal; + receivedStringVal.assign( reinterpret_cast( loanedFrame.getData() ), + loanedFrame.getSize() ); + EXPECT_EQ( receivedStringVal, stringVal ); + } + notifyStatusCallback( CommandStatus::SUCCEEDED, 0x1234, "xyz" ); + } ) ); + + mActuatorCommandManager->start(); + WAIT_ASSERT_TRUE( mActuatorCommandManager->isAlive() ); + + mActuatorCommandManager->onChangeOfCustomSignalDecoderFormatMap( mDecoderManifestID, + mSignalIDToCustomSignalDecoderFormatMap ); + + ASSERT_TRUE( mCommandResponses->isEmpty() ); + + ActuatorCommandRequest commandRequest1; + commandRequest1.commandID = "command1"; + commandRequest1.decoderID = mDecoderManifestID; + commandRequest1.issuedTimestampMs = currentTime; + commandRequest1.executionTimeoutMs = 10000; + commandRequest1.signalID = 1; + commandRequest1.signalValueWrapper.type = SignalType::STRING; + commandRequest1.signalValueWrapper.value.rawDataVal.signalId = 1; + commandRequest1.signalValueWrapper.value.rawDataVal.handle = handle; + + mActuatorCommandManager->onReceivingCommandRequest( commandRequest1 ); + + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, 0x1234 ); + ASSERT_EQ( commandResponse->reasonDescription, "xyz" ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/AwsIotConnectivityModuleTest.cpp b/test/unit/AwsIotConnectivityModuleTest.cpp index 5ea54381..35bebaf9 100644 --- a/test/unit/AwsIotConnectivityModuleTest.cpp +++ b/test/unit/AwsIotConnectivityModuleTest.cpp @@ -12,6 +12,7 @@ #include "IReceiver.h" #include "MqttClientWrapper.h" #include "MqttClientWrapperMock.h" +#include "TopicConfig.h" #include "WaitUntil.h" #include #include @@ -136,8 +137,11 @@ class AwsIotConnectivityModuleTest : public ::testing::Test ON_CALL( *mMqttClientBuilderWrapperMock, WithCertificateAuthority( _ ) ) .WillByDefault( ReturnRef( *mMqttClientBuilderWrapperMock ) ); - mConnectivityModule = - std::make_shared( "", "clientIdTest", mMqttClientBuilderWrapperMock ); + TopicConfigArgs topicConfigArgs; + mTopicConfig = std::make_unique( "thing-name", topicConfigArgs ); + + mConnectivityModule = std::make_shared( + "", "clientIdTest", mMqttClientBuilderWrapperMock, *mTopicConfig ); } void @@ -155,7 +159,7 @@ class AwsIotConnectivityModuleTest : public ::testing::Test auto publishPacket = std::make_shared( topic.c_str(), Aws::Crt::ByteCursorFromCString( data.c_str() ), - Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_LEAST_ONCE ); eventData.publishPacket = publishPacket; mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); } @@ -164,6 +168,7 @@ class AwsIotConnectivityModuleTest : public ::testing::Test std::shared_ptr> mMqttClientWrapperMock; std::shared_ptr mMqttClientWrapper; std::shared_ptr> mMqttClientBuilderWrapperMock; + std::unique_ptr mTopicConfig; std::shared_ptr mConnectivityModule; std::shared_ptr mConnectPacket; aws_mqtt5_negotiated_settings mNegotiatedSettings; @@ -212,14 +217,16 @@ class AwsIotConnectivityModuleTestAfterSuccessfulConnection : public AwsIotConne TEST_F( AwsIotConnectivityModuleTest, failToConnectWhenBuilderIsInvalid ) { - mConnectivityModule = std::make_shared( "fakeRootCA", "clientIdTest", nullptr ); + mConnectivityModule = + std::make_shared( "fakeRootCA", "clientIdTest", nullptr, *mTopicConfig ); ASSERT_FALSE( mConnectivityModule->connect() ); } /** @brief Test attempting to disconnect when connection has already failed */ TEST_F( AwsIotConnectivityModuleTest, disconnectAfterFailedConnect ) { - mConnectivityModule = std::make_shared( "", "", mMqttClientBuilderWrapperMock ); + mConnectivityModule = + std::make_shared( "", "", mMqttClientBuilderWrapperMock, *mTopicConfig ); ASSERT_FALSE( mConnectivityModule->connect() ); // disconnect must only disconnect when connection is available so this should not seg fault mConnectivityModule->disconnect(); @@ -249,8 +256,8 @@ TEST_F( AwsIotConnectivityModuleTest, connectSuccessfully ) /** @brief Test successful connection with root CA */ TEST_F( AwsIotConnectivityModuleTest, connectSuccessfullyWithRootCA ) { - mConnectivityModule = - std::make_shared( "fakeRootCA", "clientIdTest", mMqttClientBuilderWrapperMock ); + mConnectivityModule = std::make_shared( + "fakeRootCA", "clientIdTest", mMqttClientBuilderWrapperMock, *mTopicConfig ); EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithClientExtendedValidationAndFlowControl( _ ) ).Times( 1 ); EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithConnectOptions( _ ) ).Times( 1 ); @@ -274,7 +281,7 @@ TEST_F( AwsIotConnectivityModuleTest, connectSuccessfullyWithOverridenConnection mqttConnectionConfig.pingTimeoutMs = 17; mConnectivityModule = std::make_shared( - "", "clientIdTest", mMqttClientBuilderWrapperMock, mqttConnectionConfig ); + "", "clientIdTest", mMqttClientBuilderWrapperMock, *mTopicConfig, mqttConnectionConfig ); EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithClientExtendedValidationAndFlowControl( _ ) ).Times( 1 ); EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithConnectOptions( _ ) ).Times( 1 ); @@ -298,7 +305,7 @@ TEST_F( AwsIotConnectivityModuleTest, connectSuccessfullyWithPersistentSession ) mqttConnectionConfig.sessionExpiryIntervalSeconds = 7890; mConnectivityModule = std::make_shared( - "", "clientIdTest", mMqttClientBuilderWrapperMock, mqttConnectionConfig ); + "", "clientIdTest", mMqttClientBuilderWrapperMock, *mTopicConfig, mqttConnectionConfig ); EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithClientExtendedValidationAndFlowControl( _ ) ).Times( 1 ); EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithConnectOptions( _ ) ).Times( 1 ); @@ -733,7 +740,7 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, receiveMessageFro std::string data1 = "data1"; Aws::Crt::Mqtt5::PublishReceivedEventData eventData; auto publishPacket = std::make_shared( - "topic1", Aws::Crt::ByteCursorFromCString( data1.c_str() ), Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + "topic1", Aws::Crt::ByteCursorFromCString( data1.c_str() ), Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_LEAST_ONCE ); eventData.publishPacket = publishPacket; mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); // This situation shouldn't normally happen as we won't receive messages from a topic that we @@ -787,51 +794,47 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, subscribeSuccessf /** @brief Test without a configured topic, expect an error */ TEST_F( AwsIotConnectivityModuleTest, sendWithoutTopic ) { - AwsIotSender sender( - mConnectivityModule.get(), mMqttClientWrapper, "", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + AwsIotSender sender( mConnectivityModule.get(), mMqttClientWrapper, *mTopicConfig ); std::uint8_t input[] = { 0xca, 0xfe }; MockFunction resultCallback; EXPECT_CALL( resultCallback, Call( ConnectivityError::NotConfigured ) ).Times( 1 ); - sender.sendBuffer( input, sizeof( input ), resultCallback.AsStdFunction() ); + sender.sendBuffer( "", input, sizeof( input ), resultCallback.AsStdFunction() ); sender.invalidateConnection(); } /** @brief Test sending without a connection, expect an error */ TEST_F( AwsIotConnectivityModuleTest, sendWithoutConnection ) { - AwsIotSender sender( - mConnectivityModule.get(), mMqttClientWrapper, "topic", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + AwsIotSender sender( mConnectivityModule.get(), mMqttClientWrapper, *mTopicConfig ); MockFunction resultCallback; EXPECT_CALL( resultCallback, Call( ConnectivityError::NoConnection ) ).Times( 1 ); std::uint8_t input[] = { 0xca, 0xfe }; - sender.sendBuffer( input, sizeof( input ), resultCallback.AsStdFunction() ); + sender.sendBuffer( "topic", input, sizeof( input ), resultCallback.AsStdFunction() ); sender.invalidateConnection(); } /** @brief Test passing a null pointer, expect an error */ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sendWrongInput ) { - AwsIotSender sender( - mConnectivityModule.get(), mMqttClientWrapper, "topic", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + AwsIotSender sender( mConnectivityModule.get(), mMqttClientWrapper, *mTopicConfig ); MockFunction resultCallback; EXPECT_CALL( resultCallback, Call( ConnectivityError::WrongInputData ) ).Times( 1 ); - sender.sendBuffer( nullptr, 10, resultCallback.AsStdFunction() ); + sender.sendBuffer( "topic", nullptr, 10, resultCallback.AsStdFunction() ); sender.invalidateConnection(); } /** @brief Test sending a message larger then the maximum send size, expect an error */ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sendTooBig ) { - AwsIotSender sender( - mConnectivityModule.get(), mMqttClientWrapper, "topic", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + AwsIotSender sender( mConnectivityModule.get(), mMqttClientWrapper, *mTopicConfig ); MockFunction resultCallback; EXPECT_CALL( resultCallback, Call( ConnectivityError::WrongInputData ) ).Times( 1 ); std::vector a; a.resize( sender.getMaxSendSize() + 1U ); - sender.sendBuffer( a.data(), a.size(), resultCallback.AsStdFunction() ); + sender.sendBuffer( "topic", a.data(), a.size(), resultCallback.AsStdFunction() ); sender.invalidateConnection(); } @@ -840,8 +843,7 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sendTooBig ) * messages as failed to send to check that path. */ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sendMultiple ) { - AwsIotSender sender( - mConnectivityModule.get(), mMqttClientWrapper, "topic", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + AwsIotSender sender( mConnectivityModule.get(), mMqttClientWrapper, *mTopicConfig ); std::uint8_t input[] = { 0xca, 0xfe }; std::list completeHandlers; @@ -857,9 +859,9 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sendMultiple ) // Queue 2 packets MockFunction resultCallback1; - sender.sendBuffer( input, sizeof( input ), resultCallback1.AsStdFunction() ); + sender.sendBuffer( "topic", input, sizeof( input ), resultCallback1.AsStdFunction() ); MockFunction resultCallback2; - sender.sendBuffer( input, sizeof( input ), resultCallback2.AsStdFunction() ); + sender.sendBuffer( "topic", input, sizeof( input ), resultCallback2.AsStdFunction() ); // Confirm 1st EXPECT_CALL( resultCallback1, Call( ConnectivityError::Success ) ).Times( 1 ); @@ -868,7 +870,7 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sendMultiple ) // Queue another: MockFunction resultCallback3; - sender.sendBuffer( input, sizeof( input ), resultCallback3.AsStdFunction() ); + sender.sendBuffer( "topic", input, sizeof( input ), resultCallback3.AsStdFunction() ); // Confirm 2nd EXPECT_CALL( resultCallback2, Call( ConnectivityError::Success ) ).Times( 1 ); @@ -889,8 +891,7 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sdkRAMExceeded ) { auto &memMgr = AwsSDKMemoryManager::getInstance(); - AwsIotSender sender( - mConnectivityModule.get(), mMqttClientWrapper, "topic", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + AwsIotSender sender( mConnectivityModule.get(), mMqttClientWrapper, *mTopicConfig ); std::array input = { 0xCA, 0xFE }; const auto required = input.size() * sizeof( std::uint8_t ); @@ -900,7 +901,8 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sdkRAMExceeded ) MockFunction resultCallback1; EXPECT_CALL( resultCallback1, Call( ConnectivityError::QuotaReached ) ).Times( 1 ); - sender.sendBuffer( input.data(), input.size() * sizeof( std::uint8_t ), resultCallback1.AsStdFunction() ); + sender.sendBuffer( + "topic", input.data(), input.size() * sizeof( std::uint8_t ), resultCallback1.AsStdFunction() ); ASSERT_EQ( memMgr.releaseReservedMemory( reservedMemory ), 0 ); } { @@ -921,7 +923,7 @@ TEST_F( AwsIotConnectivityModuleTestAfterSuccessfulConnection, sdkRAMExceeded ) } ) ); MockFunction resultCallback3; - sender.sendBuffer( input.data(), sizeof( input ), resultCallback3.AsStdFunction() ); + sender.sendBuffer( "topic", input.data(), sizeof( input ), resultCallback3.AsStdFunction() ); // // Confirm 1st EXPECT_CALL( resultCallback3, Call( ConnectivityError::Success ) ).Times( 1 ); diff --git a/test/unit/CacheAndPersistTest.cpp b/test/unit/CacheAndPersistTest.cpp index a638d140..d44dc9d5 100644 --- a/test/unit/CacheAndPersistTest.cpp +++ b/test/unit/CacheAndPersistTest.cpp @@ -63,6 +63,29 @@ TEST( CacheAndPersistTest, testDecoderManifestPersistency ) ASSERT_EQ( storage->getSize( DataType::DECODER_MANIFEST ), 0 ); } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +TEST( CacheAndPersistTest, testStateTemplatesPersistency ) +{ + auto storage = createCacheAndPersist(); + // create a test obj + std::string testString = "Test StateTemplate"; + size_t size = testString.size(); + + ASSERT_EQ( + storage->write( reinterpret_cast( testString.c_str() ), size, DataType::STATE_TEMPLATE_LIST ), + ErrorCode::SUCCESS ); + ASSERT_EQ( storage->getSize( DataType::STATE_TEMPLATE_LIST ), size ); + + std::unique_ptr readBufPtr( new uint8_t[size]() ); + ASSERT_EQ( storage->read( readBufPtr.get(), size, DataType::STATE_TEMPLATE_LIST ), ErrorCode::SUCCESS ); + std::string out( reinterpret_cast( readBufPtr.get() ), size ); + std::cout << "File contents: " << out << std::endl; + ASSERT_STREQ( out.c_str(), testString.c_str() ); + ASSERT_EQ( storage->erase( DataType::STATE_TEMPLATE_LIST ), ErrorCode::SUCCESS ); + ASSERT_EQ( storage->getSize( DataType::STATE_TEMPLATE_LIST ), 0 ); +} +#endif + TEST( CacheAndPersistTest, testWriteEmptyBuffer ) { auto storage = createCacheAndPersist(); diff --git a/test/unit/CanCommandDispatcherTest.cpp b/test/unit/CanCommandDispatcherTest.cpp new file mode 100644 index 00000000..027f8e55 --- /dev/null +++ b/test/unit/CanCommandDispatcherTest.cpp @@ -0,0 +1,691 @@ + +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CanCommandDispatcher.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "ICommandDispatcher.h" +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::Invoke; +using ::testing::MockFunction; +using ::testing::NiceMock; + +class CanCommandDispatcherTest : public ::testing::Test +{ +protected: + const Timestamp TIMEOUT_MS = 100; + const std::string CAN_INTERFACE_NAME = "vcan0"; + std::unordered_map mConfig = { + { "Vehicle.actuator1", { 0x100, 0x101, SignalType::UINT8 } }, + { "Vehicle.actuator2", { 0x200, 0x201, SignalType::INT8 } }, + { "Vehicle.actuator3", { 0x300, 0x301, SignalType::UINT16 } }, + { "Vehicle.actuator4", { 0x400, 0x401, SignalType::INT16 } }, + { "Vehicle.actuator5", { 0x500, 0x501, SignalType::UINT32 } }, + { "Vehicle.actuator6", { 0x600, 0x601, SignalType::INT32 } }, + { "Vehicle.actuator7", { 0x700, 0x701, SignalType::UINT64 } }, + { "Vehicle.actuator8", { 0x800, 0x801, SignalType::INT64 } }, + { "Vehicle.actuator9", { 0x900, 0x901, SignalType::FLOAT } }, + { "Vehicle.actuator10", { 0xA00, 0xA01, SignalType::DOUBLE } }, + { "Vehicle.actuator11", { 0xB00, 0xB01, SignalType::BOOLEAN } }, + { "Vehicle.actuator12", { 0xC00, 0xC01, SignalType::STRING } }, + { "Vehicle.actuator13", { 0xC00, 0xC01, static_cast( -1 ) } }, + }; + using MockNotifyCommandStatusCallback = MockFunction; + + std::shared_ptr> mRawBufferManagerSpy; + + CanCommandDispatcherTest() + : mRawBufferManagerSpy( std::make_shared>( + RawData::BufferManagerConfig::create().get() ) ) + { + } + + void + SetUp() override + { + if ( !setup() ) + { + GTEST_FAIL() << "Test failed due to unavailability of socket"; + } + } + + void + TearDown() override + { + cleanUp(); + } + + int mCanSocket; + + void + cleanUp() + { + close( mCanSocket ); + } + + bool + setup() + { + mCanSocket = socket( PF_CAN, SOCK_RAW, CAN_RAW ); + if ( mCanSocket < 0 ) + { + return false; + } + int canFdOn = 1; + if ( setsockopt( mCanSocket, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &canFdOn, sizeof( canFdOn ) ) != 0 ) + { + cleanUp(); + return false; + } + auto interfaceIndex = if_nametoindex( CAN_INTERFACE_NAME.c_str() ); + if ( interfaceIndex == 0 ) + { + cleanUp(); + return false; + } + struct sockaddr_can interfaceAddress = {}; + interfaceAddress.can_family = AF_CAN; + interfaceAddress.can_ifindex = static_cast( interfaceIndex ); + if ( bind( mCanSocket, (struct sockaddr *)&interfaceAddress, sizeof( interfaceAddress ) ) < 0 ) + { + cleanUp(); + return false; + } + return true; + } + + void + sendMessage( uint32_t id, const std::vector &data ) + { + struct canfd_frame frame = {}; + frame.can_id = id; + if ( data.size() > CANFD_MAX_DLEN ) + { + throw std::runtime_error( "data size too big" ); + } + frame.len = static_cast( data.size() ); + memcpy( frame.data, data.data(), data.size() ); + auto bytesWritten = write( mCanSocket, &frame, sizeof( struct canfd_frame ) ); + if ( bytesWritten != sizeof( struct canfd_frame ) ) + { + throw std::runtime_error( "error writing CAN frame" ); + } + } + + void + receiveMessage( uint32_t &id, std::vector &data ) + { + struct pollfd pfd = { mCanSocket, POLLIN, 0 }; + int res = poll( &pfd, 1U, static_cast( TIMEOUT_MS ) ); + if ( res < 0 ) + { + throw std::runtime_error( "Error reading from CAN" ); + } + if ( res == 0 ) + { + throw std::runtime_error( "Timeout waiting for response" ); + } + struct canfd_frame frame = {}; + auto bytesRead = read( mCanSocket, &frame, sizeof( struct canfd_frame ) ); + if ( bytesRead != sizeof( struct canfd_frame ) ) + { + throw std::runtime_error( "error reading CAN frame" ); + } + id = frame.can_id; + data.assign( frame.data, frame.data + frame.len ); + } + + void + testSuccessful( std::string actuatorName, SignalValueWrapper value, std::vector &data ) + { + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback1; + std::promise callbackPromise; + EXPECT_CALL( callback1, Call( _, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( [&callbackPromise]( CommandStatus status, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) { + EXPECT_EQ( status, CommandStatus::SUCCEEDED ); + EXPECT_EQ( reasonCode, 0x11223344 ); + EXPECT_EQ( reasonDescription, "cat" ); + callbackPromise.set_value(); + } ) ); + auto issuedTimestamp = ClockHandler::getClock()->systemTimeSinceEpochMs(); + dispatcher.setActuatorValue( + actuatorName, value, "ABC", issuedTimestamp, TIMEOUT_MS, callback1.AsStdFunction() ); + uint32_t id; + receiveMessage( id, data ); + EXPECT_EQ( id, mConfig[actuatorName].canRequestId ); + ASSERT_GE( data.size(), 20 ); + EXPECT_EQ( data[0], 'A' ); + EXPECT_EQ( data[1], 'B' ); + EXPECT_EQ( data[2], 'C' ); + EXPECT_EQ( data[3], 0x00 ); + EXPECT_EQ( data[4], static_cast( issuedTimestamp >> 56 ) ); + EXPECT_EQ( data[5], static_cast( issuedTimestamp >> 48 ) ); + EXPECT_EQ( data[6], static_cast( issuedTimestamp >> 40 ) ); + EXPECT_EQ( data[7], static_cast( issuedTimestamp >> 32 ) ); + EXPECT_EQ( data[8], static_cast( issuedTimestamp >> 24 ) ); + EXPECT_EQ( data[9], static_cast( issuedTimestamp >> 16 ) ); + EXPECT_EQ( data[10], static_cast( issuedTimestamp >> 8 ) ); + EXPECT_EQ( data[11], static_cast( issuedTimestamp ) ); + EXPECT_EQ( data[12], static_cast( TIMEOUT_MS >> 56 ) ); + EXPECT_EQ( data[13], static_cast( TIMEOUT_MS >> 48 ) ); + EXPECT_EQ( data[14], static_cast( TIMEOUT_MS >> 40 ) ); + EXPECT_EQ( data[15], static_cast( TIMEOUT_MS >> 32 ) ); + EXPECT_EQ( data[16], static_cast( TIMEOUT_MS >> 24 ) ); + EXPECT_EQ( data[17], static_cast( TIMEOUT_MS >> 16 ) ); + EXPECT_EQ( data[18], static_cast( TIMEOUT_MS >> 8 ) ); + EXPECT_EQ( data[19], static_cast( TIMEOUT_MS ) ); + sendMessage( mConfig[actuatorName].canResponseId, + { 'A', 'B', 'C', 0x00, 0x01, 0x11, 0x22, 0x33, 0x44, 'c', 'a', 't', 0x00 } ); + ASSERT_EQ( std::future_status::ready, + callbackPromise.get_future().wait_for( std::chrono::milliseconds( TIMEOUT_MS ) ) ); + } + + void + testTimeout( std::function sendMessageCallback ) + { + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback1; + std::promise callbackPromise; + EXPECT_CALL( callback1, Call( _, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( [&callbackPromise]( CommandStatus status, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) { + EXPECT_EQ( status, CommandStatus::EXECUTION_TIMEOUT ); + EXPECT_EQ( reasonCode, REASON_CODE_NO_RESPONSE ); + static_cast( reasonDescription ); + callbackPromise.set_value(); + } ) ); + auto actuatorName = "Vehicle.actuator6"; + SignalValueWrapper value; + std::vector data; + value.value = static_cast( 0xAABBCCDD ); + value.type = SignalType::INT32; + auto issuedTimestamp = ClockHandler::getClock()->systemTimeSinceEpochMs(); + dispatcher.setActuatorValue( + actuatorName, value, "ABC", issuedTimestamp, TIMEOUT_MS, callback1.AsStdFunction() ); + uint32_t id; + receiveMessage( id, data ); + EXPECT_EQ( id, mConfig[actuatorName].canRequestId ); + ASSERT_GE( data.size(), 20 ); + EXPECT_EQ( data[0], 'A' ); + EXPECT_EQ( data[1], 'B' ); + EXPECT_EQ( data[2], 'C' ); + EXPECT_EQ( data[3], 0x00 ); + EXPECT_EQ( data[4], static_cast( issuedTimestamp >> 56 ) ); + EXPECT_EQ( data[5], static_cast( issuedTimestamp >> 48 ) ); + EXPECT_EQ( data[6], static_cast( issuedTimestamp >> 40 ) ); + EXPECT_EQ( data[7], static_cast( issuedTimestamp >> 32 ) ); + EXPECT_EQ( data[8], static_cast( issuedTimestamp >> 24 ) ); + EXPECT_EQ( data[9], static_cast( issuedTimestamp >> 16 ) ); + EXPECT_EQ( data[10], static_cast( issuedTimestamp >> 8 ) ); + EXPECT_EQ( data[11], static_cast( issuedTimestamp ) ); + EXPECT_EQ( data[12], static_cast( TIMEOUT_MS >> 56 ) ); + EXPECT_EQ( data[13], static_cast( TIMEOUT_MS >> 48 ) ); + EXPECT_EQ( data[14], static_cast( TIMEOUT_MS >> 40 ) ); + EXPECT_EQ( data[15], static_cast( TIMEOUT_MS >> 32 ) ); + EXPECT_EQ( data[16], static_cast( TIMEOUT_MS >> 24 ) ); + EXPECT_EQ( data[17], static_cast( TIMEOUT_MS >> 16 ) ); + EXPECT_EQ( data[18], static_cast( TIMEOUT_MS >> 8 ) ); + EXPECT_EQ( data[19], static_cast( TIMEOUT_MS ) ); + sendMessageCallback(); + ASSERT_EQ( std::future_status::ready, + callbackPromise.get_future().wait_for( std::chrono::milliseconds( 2 * TIMEOUT_MS ) ) ); + } +}; + +TEST_F( CanCommandDispatcherTest, invalidCanInterfaceName ) +{ + CanCommandDispatcher dispatcher( mConfig, "abc", mRawBufferManagerSpy ); + ASSERT_FALSE( dispatcher.init() ); +} + +TEST_F( CanCommandDispatcherTest, getActuatorNames ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + auto names = dispatcher.getActuatorNames(); + ASSERT_EQ( names.size(), 13 ); +} + +TEST_F( CanCommandDispatcherTest, notSupportedActuator ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_NOT_SUPPORTED, _ ) ).Times( 1 ); + dispatcher.setActuatorValue( "Vehicle.actuator99", + SignalValueWrapper{}, + "CMD123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, wrongArgumentType ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_ARGUMENT_TYPE_MISMATCH, _ ) ).Times( 1 ); + SignalValueWrapper value; + value.value = 1.0; + value.type = SignalType::DOUBLE; + dispatcher.setActuatorValue( "Vehicle.actuator6", + value, + "CMD123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, unsupportedArgumentType ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, _ ) ).Times( 1 ); + SignalValueWrapper value; + value.type = static_cast( -1 ); + dispatcher.setActuatorValue( "Vehicle.actuator13", + value, + "CMD123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, commandIdTooLong ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, _ ) ).Times( 1 ); + SignalValueWrapper value; + value.value = static_cast( 0xAABBCCDD ); + value.type = SignalType::INT32; + dispatcher.setActuatorValue( "Vehicle.actuator6", + value, + "0123456789012345678901234567890123456789012345678901234567890123456789", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, timedOutBeforeDispatch ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_TIMEOUT, REASON_CODE_TIMED_OUT_BEFORE_DISPATCH, _ ) ) + .Times( 1 ); + SignalValueWrapper value; + value.value = static_cast( 0xAABBCCDD ); + value.type = SignalType::INT32; + dispatcher.setActuatorValue( "Vehicle.actuator6", + value, + "CMD123", + ClockHandler::getClock()->systemTimeSinceEpochMs() - 1000, + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, stringBadBorrow ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, _ ) ).Times( 1 ); + SignalValueWrapper value; + value.value.rawDataVal.signalId = 999; + value.value.rawDataVal.handle = 1234; + value.type = SignalType::STRING; + dispatcher.setActuatorValue( "Vehicle.actuator12", + value, + "CMD123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, argumentTooLong ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback; + EXPECT_CALL( callback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, _ ) ).Times( 1 ); + SignalValueWrapper value; + value.value = static_cast( 0xAABBCCDD ); + value.type = SignalType::INT32; + dispatcher.setActuatorValue( "Vehicle.actuator6", + value, + "012345678901234567890123456789012345678901234567890123456789", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback.AsStdFunction() ); +} + +TEST_F( CanCommandDispatcherTest, successfulAllTypes ) +{ + SignalValueWrapper value; + std::vector data; + + // UINT8 + value.value = static_cast( 0xAA ); + value.type = SignalType::UINT8; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator1", value, data ) ); + ASSERT_EQ( data.size(), 21 ); + EXPECT_EQ( data[20], 0xAA ); + + // INT8 + value.value = static_cast( 0xAA ); + value.type = SignalType::INT8; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator2", value, data ) ); + ASSERT_EQ( data.size(), 21 ); + EXPECT_EQ( data[20], 0xAA ); + + // UINT16 + value.value = static_cast( 0xAABB ); + value.type = SignalType::UINT16; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator3", value, data ) ); + ASSERT_EQ( data.size(), 22 ); + EXPECT_EQ( data[20], 0xAA ); + EXPECT_EQ( data[21], 0xBB ); + + // INT16 + value.value = static_cast( 0xAABB ); + value.type = SignalType::INT16; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator4", value, data ) ); + ASSERT_EQ( data.size(), 22 ); + EXPECT_EQ( data[20], 0xAA ); + EXPECT_EQ( data[21], 0xBB ); + + // UINT32 + value.value = static_cast( 0xAABBCCDD ); + value.type = SignalType::UINT32; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator5", value, data ) ); + ASSERT_EQ( data.size(), 24 ); + EXPECT_EQ( data[20], 0xAA ); + EXPECT_EQ( data[21], 0xBB ); + EXPECT_EQ( data[22], 0xCC ); + EXPECT_EQ( data[23], 0xDD ); + + // INT32 + value.value = static_cast( 0xAABBCCDD ); + value.type = SignalType::INT32; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator6", value, data ) ); + ASSERT_EQ( data.size(), 24 ); + EXPECT_EQ( data[20], 0xAA ); + EXPECT_EQ( data[21], 0xBB ); + EXPECT_EQ( data[22], 0xCC ); + EXPECT_EQ( data[23], 0xDD ); + + // UINT64 + value.value = static_cast( 0xAABBCCDD00112233 ); + value.type = SignalType::UINT64; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator7", value, data ) ); + ASSERT_EQ( data.size(), 28 ); + EXPECT_EQ( data[20], 0xAA ); + EXPECT_EQ( data[21], 0xBB ); + EXPECT_EQ( data[22], 0xCC ); + EXPECT_EQ( data[23], 0xDD ); + EXPECT_EQ( data[24], 0x00 ); + EXPECT_EQ( data[25], 0x11 ); + EXPECT_EQ( data[26], 0x22 ); + EXPECT_EQ( data[27], 0x33 ); + + // INT64 + value.value = static_cast( 0xAABBCCDD00112233 ); + value.type = SignalType::INT64; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator8", value, data ) ); + ASSERT_EQ( data.size(), 28 ); + EXPECT_EQ( data[20], 0xAA ); + EXPECT_EQ( data[21], 0xBB ); + EXPECT_EQ( data[22], 0xCC ); + EXPECT_EQ( data[23], 0xDD ); + EXPECT_EQ( data[24], 0x00 ); + EXPECT_EQ( data[25], 0x11 ); + EXPECT_EQ( data[26], 0x22 ); + EXPECT_EQ( data[27], 0x33 ); + + // FLOAT + value.value = static_cast( 123.0 ); + value.type = SignalType::FLOAT; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator9", value, data ) ); + ASSERT_EQ( data.size(), 24 ); + EXPECT_EQ( data[20], 0x42 ); + EXPECT_EQ( data[21], 0xF6 ); + EXPECT_EQ( data[22], 0x00 ); + EXPECT_EQ( data[23], 0x00 ); + + // DOUBLE + value.value = static_cast( 456.0 ); + value.type = SignalType::DOUBLE; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator10", value, data ) ); + ASSERT_EQ( data.size(), 28 ); + EXPECT_EQ( data[20], 0x40 ); + EXPECT_EQ( data[21], 0x7C ); + EXPECT_EQ( data[22], 0x80 ); + EXPECT_EQ( data[23], 0x00 ); + EXPECT_EQ( data[24], 0x00 ); + EXPECT_EQ( data[25], 0x0 ); + EXPECT_EQ( data[26], 0x00 ); + EXPECT_EQ( data[27], 0x00 ); + + // BOOL + value.value = true; + value.type = SignalType::BOOLEAN; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator11", value, data ) ); + ASSERT_EQ( data.size(), 21 ); + EXPECT_EQ( data[20], 0x01 ); + + // STRING + mRawBufferManagerSpy->updateConfig( { { 1, { 1, "", "" } } } ); + std::string stringVal = "dog"; + auto handle = + mRawBufferManagerSpy->push( reinterpret_cast( stringVal.data() ), stringVal.size(), 1234, 1 ); + mRawBufferManagerSpy->increaseHandleUsageHint( 1, handle, RawData::BufferHandleUsageStage::UPLOADING ); + value.value.rawDataVal.handle = handle; + value.value.rawDataVal.signalId = 1; + value.type = SignalType::STRING; + ASSERT_NO_FATAL_FAILURE( testSuccessful( "Vehicle.actuator12", value, data ) ); + ASSERT_EQ( data.size(), 24 ); + EXPECT_EQ( data[20], 'd' ); + EXPECT_EQ( data[21], 'o' ); + EXPECT_EQ( data[22], 'g' ); + EXPECT_EQ( data[23], 0x00 ); +} + +TEST_F( CanCommandDispatcherTest, responseIgnoredNoCommandId ) +{ + testTimeout( [this]() { + sendMessage( mConfig["Vehicle.actuator6"].canResponseId, {} ); + } ); +} + +TEST_F( CanCommandDispatcherTest, responseIgnoredNoStatus ) +{ + testTimeout( [this]() { + sendMessage( mConfig["Vehicle.actuator6"].canResponseId, { 'A', 'B', 'C', 0x00 } ); + } ); +} + +TEST_F( CanCommandDispatcherTest, responseIgnoredNoReasonCode ) +{ + testTimeout( [this]() { + sendMessage( mConfig["Vehicle.actuator6"].canResponseId, { 'A', 'B', 'C', 0x00, 0x01 } ); + } ); +} + +TEST_F( CanCommandDispatcherTest, responseIgnoredNoReasonDescription ) +{ + testTimeout( [this]() { + sendMessage( mConfig["Vehicle.actuator6"].canResponseId, + { 'A', 'B', 'C', 0x00, 0x01, 0x11, 0x22, 0x33, 0x44 } ); + } ); +} + +TEST_F( CanCommandDispatcherTest, responseIgnoredWrongCommandId ) +{ + testTimeout( [this]() { + sendMessage( mConfig["Vehicle.actuator6"].canResponseId, + { 'X', 'Y', 'Z', 0x00, 0x01, 0x11, 0x22, 0x33, 0x44, 'c', 'a', 't', 0x00 } ); + } ); +} + +TEST_F( CanCommandDispatcherTest, responseIgnoredWrongResponseId ) +{ + testTimeout( [this]() { + sendMessage( mConfig["Vehicle.actuator5"].canResponseId, + { 'A', 'B', 'C', 0x00, 0x01, 0x11, 0x22, 0x33, 0x44, 'c', 'a', 't', 0x00 } ); + } ); +} + +TEST_F( CanCommandDispatcherTest, inProgressSuccess ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback1; + std::promise callbackPromise; + EXPECT_CALL( callback1, Call( _, _, _ ) ) + .Times( 2 ) + .WillOnce( Invoke( []( CommandStatus status, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) { + EXPECT_EQ( status, CommandStatus::IN_PROGRESS ); + EXPECT_EQ( reasonCode, 0x11223344 ); + EXPECT_EQ( reasonDescription, "cat" ); + } ) ) + .WillOnce( Invoke( [&callbackPromise]( CommandStatus status, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) { + EXPECT_EQ( status, CommandStatus::SUCCEEDED ); + EXPECT_EQ( reasonCode, 0x55667788 ); + EXPECT_EQ( reasonDescription, "dog" ); + callbackPromise.set_value(); + } ) ); + auto actuatorName = "Vehicle.actuator6"; + SignalValueWrapper value; + value.value = static_cast( 0xAA ); + value.type = SignalType::INT32; + dispatcher.setActuatorValue( actuatorName, + value, + "ABC", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback1.AsStdFunction() ); + // Also attempt duplicate invocation with same command ID + MockNotifyCommandStatusCallback callback2; + EXPECT_CALL( callback2, Call( _, _, _ ) ).Times( 0 ); + dispatcher.setActuatorValue( actuatorName, + value, + "ABC", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback2.AsStdFunction() ); + uint32_t id; + std::vector data; + receiveMessage( id, data ); + EXPECT_EQ( id, mConfig[actuatorName].canRequestId ); + ASSERT_GE( data.size(), 4 ); + EXPECT_EQ( data[0], 'A' ); + EXPECT_EQ( data[1], 'B' ); + EXPECT_EQ( data[2], 'C' ); + EXPECT_EQ( data[3], 0x00 ); + sendMessage( 0x123, {} ); // Send some other message to check it's ignored + sendMessage( mConfig[actuatorName].canResponseId, + { 'A', 'B', 'C', 0x00, 0x0A, 0x11, 0x22, 0x33, 0x44, 'c', 'a', 't', 0x00 } ); + sendMessage( mConfig[actuatorName].canResponseId, + { 'A', 'B', 'C', 0x00, 0x01, 0x55, 0x66, 0x77, 0x88, 'd', 'o', 'g', 0x00 } ); + ASSERT_EQ( std::future_status::ready, + callbackPromise.get_future().wait_for( std::chrono::milliseconds( TIMEOUT_MS ) ) ); +} + +TEST_F( CanCommandDispatcherTest, inProgressTimeout ) +{ + CanCommandDispatcher dispatcher( mConfig, CAN_INTERFACE_NAME, mRawBufferManagerSpy ); + ASSERT_TRUE( dispatcher.init() ); + MockNotifyCommandStatusCallback callback1; + std::promise callbackPromise; + EXPECT_CALL( callback1, Call( _, _, _ ) ) + .Times( 2 ) + .WillOnce( Invoke( []( CommandStatus status, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) { + EXPECT_EQ( status, CommandStatus::IN_PROGRESS ); + EXPECT_EQ( reasonCode, 0x11223344 ); + EXPECT_EQ( reasonDescription, "cat" ); + } ) ) + .WillOnce( Invoke( [&callbackPromise]( CommandStatus status, + CommandReasonCode reasonCode, + const CommandReasonDescription &reasonDescription ) { + EXPECT_EQ( status, CommandStatus::EXECUTION_TIMEOUT ); + EXPECT_EQ( reasonCode, REASON_CODE_NO_RESPONSE ); + static_cast( reasonDescription ); + callbackPromise.set_value(); + } ) ); + auto actuatorName = "Vehicle.actuator6"; + SignalValueWrapper value; + value.value = static_cast( 0xAA ); + value.type = SignalType::INT32; + dispatcher.setActuatorValue( actuatorName, + value, + "ABC", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + TIMEOUT_MS, + callback1.AsStdFunction() ); + uint32_t id; + std::vector data; + receiveMessage( id, data ); + EXPECT_EQ( id, mConfig[actuatorName].canRequestId ); + ASSERT_GE( data.size(), 4 ); + EXPECT_EQ( data[0], 'A' ); + EXPECT_EQ( data[1], 'B' ); + EXPECT_EQ( data[2], 'C' ); + EXPECT_EQ( data[3], 0x00 ); + sendMessage( mConfig[actuatorName].canResponseId, + { 'A', 'B', 'C', 0x00, 0x0A, 0x11, 0x22, 0x33, 0x44, 'c', 'a', 't', 0x00 } ); + ASSERT_EQ( std::future_status::ready, + callbackPromise.get_future().wait_for( std::chrono::milliseconds( 2 * TIMEOUT_MS ) ) ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/CollectionInspectionEngineTest.cpp b/test/unit/CollectionInspectionEngineTest.cpp index 0d26e37e..7f0f5045 100644 --- a/test/unit/CollectionInspectionEngineTest.cpp +++ b/test/unit/CollectionInspectionEngineTest.cpp @@ -4,27 +4,41 @@ #include "CollectionInspectionEngine.h" #include "CANDataTypes.h" #include "CollectionInspectionAPITypes.h" +#include "CollectionSchemeIngestion.h" +#include "CollectionSchemeManagerTest.h" #include "ICollectionScheme.h" +#include "ICollectionSchemeList.h" #include "LogLevel.h" #include "OBDDataTypes.h" +#include "RawDataManager.h" #include "SignalTypes.h" #include "Testing.h" #include "TimeTypes.h" #include #include +#include +#include #include #include +#include #include #include #include #include #include #include +#include +#include #include -#ifdef FWE_FEATURE_VISION_SYSTEM_DATA -#include "RawDataManager.h" -#include +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include "CANInterfaceIDTranslator.h" +#include "CheckinSender.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionSchemeManagerMock.h" +#include "DataFetchManagerAPITypes.h" +#include "IDecoderManifest.h" #endif namespace Aws @@ -32,6 +46,9 @@ namespace Aws namespace IoTFleetWise { +using ::testing::_; +using ::testing::Invoke; +using ::testing::MockFunction; using signalTypes = ::testing::Types; @@ -43,6 +60,32 @@ class CollectionInspectionEngineTest : public ::testing::Test std::shared_ptr consCollectionSchemes; std::vector> expressionNodes; + static void + convertSchemes( std::vector schemes, + std::shared_ptr &result ) + { + std::vector collectionSchemes; + for ( auto scheme : schemes ) + { + auto campaign = std::make_shared( +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + std::make_shared() +#endif + ); + campaign->copyData( std::make_shared( scheme ) ); + ASSERT_TRUE( campaign->build() ); + collectionSchemes.emplace_back( campaign ); + } + result = std::make_shared( collectionSchemes ); + } + + static void + convertScheme( Schemas::CollectionSchemesMsg::CollectionScheme scheme, + std::shared_ptr &result ) + { + convertSchemes( std::vector{ scheme }, result ); + } + bool compareSignalValue( const SignalValueWrapper &signalValueWrapper, T sigVal ) { @@ -71,6 +114,8 @@ class CollectionInspectionEngineTest : public ::testing::Test return static_cast( sigVal ) == signalValueWrapper.value.doubleVal; case SignalType::BOOLEAN: return static_cast( sigVal ) == signalValueWrapper.value.boolVal; + case SignalType::STRING: + return static_cast( sigVal ) == signalValueWrapper.value.uint32Val; case SignalType::UNKNOWN: return false; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA @@ -127,6 +172,50 @@ class CollectionInspectionEngineTest : public ::testing::Test return ( notEqual ); } + std::shared_ptr + getEqualCondition( SignalID id1, SignalID id2 ) + { + expressionNodes.push_back( std::make_shared() ); + auto equal = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto signal1 = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto signal2 = expressionNodes.back(); + + signal1->nodeType = ExpressionNodeType::SIGNAL; + signal1->signalID = id1; + + signal2->nodeType = ExpressionNodeType::SIGNAL; + signal2->signalID = id2; + + equal->nodeType = ExpressionNodeType::OPERATOR_EQUAL; + equal->left = signal1.get(); + equal->right = signal2.get(); + return ( equal ); + } + + std::shared_ptr + getStringCondition( std::string nodeLeftValue, std::string nodeRightValue, ExpressionNodeType expressionOperation ) + { + expressionNodes.push_back( std::make_shared() ); + auto nodeOperation = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto nodeLeft = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto nodeRight = expressionNodes.back(); + + nodeLeft->nodeType = ExpressionNodeType::STRING; + nodeLeft->stringValue = nodeLeftValue; + + nodeRight->nodeType = ExpressionNodeType::STRING; + nodeRight->stringValue = nodeRightValue; + + nodeOperation->nodeType = expressionOperation; + nodeOperation->left = nodeLeft.get(); + nodeOperation->right = nodeRight.get(); + return ( nodeOperation ); + } + std::shared_ptr getTwoSignalsBiggerCondition( SignalID id1, double threshold1, SignalID id2, double threshold2 ) { @@ -172,6 +261,29 @@ class CollectionInspectionEngineTest : public ::testing::Test return boolAnd; } + std::shared_ptr + getOneSignalBiggerCondition( SignalID id1, double threshold1 ) + { + expressionNodes.push_back( std::make_shared() ); + auto bigger1 = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto signal1 = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto value1 = expressionNodes.back(); + + bigger1->nodeType = ExpressionNodeType::OPERATOR_BIGGER; + bigger1->left = signal1.get(); + bigger1->right = value1.get(); + + signal1->nodeType = ExpressionNodeType::SIGNAL; + signal1->signalID = id1; + + value1->nodeType = ExpressionNodeType::FLOAT; + value1->floatingValue = threshold1; + + return bigger1; + } + std::shared_ptr getMultiFixedWindowCondition( SignalID id1 ) { @@ -305,6 +417,22 @@ class CollectionInspectionEngineTest : public ::testing::Test return not1; } + std::shared_ptr + getCustomFunctionCondition( SignalID id1, const std::string &name ) + { + expressionNodes.push_back( std::make_shared() ); + auto customFunctionNode = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto signalNode = expressionNodes.back(); + + signalNode->nodeType = ExpressionNodeType::SIGNAL; + signalNode->signalID = id1; + customFunctionNode->nodeType = ExpressionNodeType::CUSTOM_FUNCTION; + customFunctionNode->function.customFunctionName = name; + customFunctionNode->function.customFunctionParams.push_back( signalNode.get() ); + return customFunctionNode; + } + std::shared_ptr getUnknownCondition( SignalID id1 ) { @@ -443,9 +571,12 @@ TYPED_TEST( CollectionInspectionEngineTest, TwoSignalsInConditionAndOneSignalToC TypeParam testVal1 = 10; TypeParam testVal2 = 20; TypeParam testVal3 = 30; - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, testVal1 ); - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, testVal2 ); - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, testVal3 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, testVal1 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, testVal2 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, testVal3 ); // Signals for condition are not available yet so collectionScheme should not trigger ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); @@ -453,8 +584,8 @@ TYPED_TEST( CollectionInspectionEngineTest, TwoSignalsInConditionAndOneSignalToC ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); timestamp += 1000; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -90.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); // Condition only for first signal is fulfilled (-90 > -100) but second not so boolean and is false ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); @@ -462,7 +593,7 @@ TYPED_TEST( CollectionInspectionEngineTest, TwoSignalsInConditionAndOneSignalToC timestamp += 1000; - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); // Condition is fulfilled so it should trigger ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -475,343 +606,1178 @@ TYPED_TEST( CollectionInspectionEngineTest, TwoSignalsInConditionAndOneSignalToC ASSERT_EQ( collectedData->triggerTime, timestamp.systemTimeMs ); } -TEST_F( CollectionInspectionEngineDoubleTest, EndlessCondition ) +#ifdef FWE_FEATURE_STORE_AND_FORWARD +TYPED_TEST( CollectionInspectionEngineTest, ForwardConditionOnly ) { - CollectionInspectionEngine engine; - // minimumSampleIntervalMs=0 means no subsampling - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 1234; - s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 77777; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); + // 1. Define some signals (type, id) which can exist + InspectionMatrixSignalCollectionInfo s1{ 1, 50, 10, 77777, true, SignalType::DOUBLE, {} }; + InspectionMatrixSignalCollectionInfo s2{ 2, 50, 10, 77777, true, SignalType::DOUBLE, {} }; - ExpressionNode endless; + // 2. Define the campaign and forward condition + const SyncID campaignID = "arn:1/T0123"; + this->collectionSchemes->conditions[0].metadata.collectionSchemeID = campaignID; + this->collectionSchemes->conditions[0].metadata.campaignArn = campaignID; - endless.nodeType = ExpressionNodeType::OPERATOR_LOGICAL_AND; - endless.left = &endless; - endless.right = &endless; + // 2a. Define that the above signals should be inspected for our condition + this->addSignalToCollect( this->collectionSchemes->conditions[0], s1 ); + this->addSignalToCollect( this->collectionSchemes->conditions[0], s2 ); + + // 2b. Define the condition expression: (signalID(1)>-100) && (signalID(2)>-500) + // Condition contains signal + this->collectionSchemes->conditions[0].isStaticCondition = false; + ConditionForForward forwardCondition; + forwardCondition.condition = this->getTwoSignalsBiggerCondition( s1.signalID, -100.0, s2.signalID, -500.0 ).get(); + this->collectionSchemes->conditions[0].forwardConditions.push_back( forwardCondition ); + // 3. Boot up the inspection engine TimePoint timestamp = { 160000000, 100 }; + CollectionInspectionEngine engine; + engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - // Condition is static by default - collectionSchemes->conditions[0].condition = &endless; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + // 3a. Verify our campaign and forward condition is present and has not triggered + auto currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + auto campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // 4. Send in some signals which should not cause the condition to trigger + timestamp += 1000; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + + // 4a. Verify our forward condition still has not triggered + currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // 5. Send in some signals which should cause the condition to trigger + timestamp += 1000; + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + // 5a. Verify the forward condition has now triggered + currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), true ); + + // 6. Send in some signals which should cause the condition to go back to not triggered + timestamp += 1000; + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -520.0 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - uint32_t waitTimeMs = 0; - EXPECT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + // 6a. Verify our forward condition is now not triggered + currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); } -TEST_F( CollectionInspectionEngineDoubleTest, TooBigForSignalBuffer ) +TYPED_TEST( CollectionInspectionEngineTest, OneCampaignWithForwardAndCollectionConditions ) { - CollectionInspectionEngine engine; - // minimumSampleIntervalMs=0 means no subsampling - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 3072; - s1.sampleBufferSize = - 500000000; // this number of samples should exceed the maximum buffer size defined in MAX_SAMPLE_MEMORY - s1.minimumSampleIntervalMs = 5; - s1.fixedWindowPeriod = 77777; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); - // Condition is static be default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + // 1. Define some signals (type, id) which can exist + InspectionMatrixSignalCollectionInfo s1{ 1, 50, 10, 77777, false, SignalType::DOUBLE, {} }; + InspectionMatrixSignalCollectionInfo s2{ 2, 50, 10, 77777, true, SignalType::DOUBLE, {} }; + + // Define the campaign id to use + const SyncID campaignID = "arn:1/T0123"; + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + this->collectionSchemes->conditions[0].metadata.collectionSchemeID = campaignID; + this->collectionSchemes->conditions[0].metadata.campaignArn = campaignID; + this->addSignalToCollect( this->collectionSchemes->conditions[0], s1 ); + this->addSignalToCollect( this->collectionSchemes->conditions[0], s2 ); + // Condition contains signal + this->collectionSchemes->conditions[0].isStaticCondition = false; + ConditionForForward forwardCondition0; + forwardCondition0.condition = this->getTwoSignalsBiggerCondition( s1.signalID, -100.0, s2.signalID, -500.0 ).get(); + this->collectionSchemes->conditions[0].forwardConditions.push_back( forwardCondition0 ); + + this->collectionSchemes->conditions[0].condition = this->getOneSignalBiggerCondition( s1.signalID, -100.0 ).get(); + + // Boot up the inspection engine TimePoint timestamp = { 160000000, 100 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + CollectionInspectionEngine engine; + engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - engine.addNewSignal( - s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); // All signal come at the same timestamp - engine.addNewSignal( s1.signalID, timestamp + 1000, timestamp.monotonicTimeMs + 1000, 0.2 ); - engine.addNewSignal( s1.signalID, timestamp + 2000, timestamp.monotonicTimeMs + 2000, 0.3 ); + // Verify the forward condition is present and has not triggered + auto currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); - ASSERT_TRUE( engine.evaluateConditions( timestamp + 3000 ) ); + ASSERT_EQ( currentForwardCampaigns.size(), 2 ); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + auto campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates[0], false ); + // Verify the collection condition has not collected anything uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp + 5000, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Send in some signals which should cause collection to trigger, but not forward + timestamp += 1000; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + + // Verify our forward condition still has not triggered + currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates[0], false ); + + // Verify we have collected the signal + auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; ASSERT_NE( collectedData, nullptr ); - ASSERT_EQ( collectedData->signals.size(), 0 ); + ASSERT_EQ( collectedData->signals.size(), 1 ); + ASSERT_EQ( collectedData->signals[0].signalID, s1.signalID ); + ASSERT_EQ( collectedData->signals[0].getValue().value.doubleVal, -90.0 ); + ASSERT_EQ( collectedData->triggerTime, timestamp.systemTimeMs ); + + // 5. Send in some signals which should cause the condition to trigger + timestamp += 1000; + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + + // 5a. Verify the forward condition has now triggered + currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates[0], true ); + + // 6. Send in some signals which should cause the condition to go back to not triggered + timestamp += 1000; + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -520.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -110.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + + // 6a. Verify our forward condition is now not triggered + currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + ASSERT_EQ( currentForwardCampaigns[campaignID].size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates[0], false ); } -TEST_F( CollectionInspectionEngineDoubleTest, TooBigForSignalBufferOverflow ) +TYPED_TEST( CollectionInspectionEngineTest, ForwardConditionDefaultsToFalse ) { - CollectionInspectionEngine engine; - // minimumSampleIntervalMs=0 means no subsampling - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 1234; - s1.sampleBufferSize = 536870912; - s1.minimumSampleIntervalMs = 5; - s1.fixedWindowPeriod = 77777; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); - InspectionMatrixCanFrameCollectionInfo c1; - c1.frameID = 0x380; - c1.channelID = 3; - c1.sampleBufferSize = 536870912; - c1.minimumSampleIntervalMs = 0; - collectionSchemes->conditions[0].canFrames.push_back( c1 ); + // 1. Define some signals (type, id) which can exist + InspectionMatrixSignalCollectionInfo s1{ 1, 50, 10, 77777, false, SignalType::DOUBLE, {} }; + InspectionMatrixSignalCollectionInfo s2{ 2, 50, 10, 77777, true, SignalType::DOUBLE, {} }; - collectionSchemes->conditions[0].minimumPublishIntervalMs = 500; - // Condition is static by default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + // Define the campaign id to use + const SyncID campaignID = "arn:1/T0123"; - TimePoint timestamp = { 160000000, 100 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + this->collectionSchemes->conditions[0].metadata.collectionSchemeID = campaignID; + this->collectionSchemes->conditions[0].metadata.campaignArn = campaignID; + this->addSignalToCollect( this->collectionSchemes->conditions[0], s1 ); + this->addSignalToCollect( this->collectionSchemes->conditions[0], s2 ); + ConditionForForward forwardCondition0; + forwardCondition0.condition = this->getTwoSignalsBiggerCondition( s1.signalID, -100.0, s2.signalID, -500.0 ).get(); + this->collectionSchemes->conditions[0].forwardConditions.push_back( forwardCondition0 ); - // All signal come at the same timestamp - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); - engine.addNewSignal( s1.signalID, timestamp + 1000, timestamp.monotonicTimeMs + 1000, 0.2 ); - engine.addNewSignal( s1.signalID, timestamp + 2000, timestamp.monotonicTimeMs + 2000, 0.3 ); + this->collectionSchemes->conditions[0].condition = this->getOneSignalBiggerCondition( s1.signalID, -100.0 ).get(); - ASSERT_TRUE( engine.evaluateConditions( timestamp + 3000 ) ); + // Boot up the inspection engine + TimePoint timestamp = { 160000000, 100 }; + CollectionInspectionEngine engine; + engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); + + // engine.evaluateConditions hasn't been called, ensure forward conditions exist and are not triggered + auto currentForwardCampaigns = engine.forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 2 ); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + auto campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates[0], false ); + // Verify the collection condition has not collected anything uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp + 4000, waitTimeMs ).triggeredCollectionSchemeData; - ASSERT_NE( collectedData, nullptr ); - ASSERT_EQ( collectedData->signals.size(), 0 ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); } -TEST_F( CollectionInspectionEngineDoubleTest, SignalBufferErasedAfterNewConditions ) +TYPED_TEST( CollectionInspectionEngineTest, RealCampaignWithForwardAndCollectionConditions ) { - CollectionInspectionEngine engine; - // minimumSampleIntervalMs=0 means no subsampling - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 1234; - s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 77777; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); - // Condition is static by default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + const SignalID signalID1 = 1; + const SignalID signalID2 = 2; + uint32_t sampleBufferSize = 50; + uint32_t minimumSampleIntervalMs = 10; + uint32_t fixedWindowPeriod = 77777; - TimePoint timestamp = { 160000000, 100 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + const uint32_t defaultPartitionID = 0; - // All signal come at the same timestamp - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); - engine.addNewSignal( s1.signalID, timestamp + 1, timestamp.monotonicTimeMs + 1, 0.2 ); - engine.addNewSignal( s1.signalID, timestamp + 2, timestamp.monotonicTimeMs + 2, 0.3 ); + InspectionMatrixSignalCollectionInfo s1{ + signalID1, sampleBufferSize, minimumSampleIntervalMs, fixedWindowPeriod, false, SignalType::DOUBLE, {} }; + InspectionMatrixSignalCollectionInfo s2{ + signalID2, sampleBufferSize, minimumSampleIntervalMs, fixedWindowPeriod, true, SignalType::DOUBLE, {} }; - // This call will flush the signal history buffer even when - // exactly the same conditions are handed over. - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp + 3 ); + std::shared_ptr clock = ClockHandler::getClock(); - engine.addNewSignal( s1.signalID, timestamp + 3, timestamp.monotonicTimeMs + 3, 0.4 ); + const SyncID campaignID = "arn:1/T0123"; + std::shared_ptr campaignWithSinglePartition; + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; - ASSERT_TRUE( engine.evaluateConditions( timestamp + 3 ) ); + scheme.set_campaign_sync_id( campaignID ); + scheme.set_campaign_arn( campaignID ); + scheme.set_decoder_manifest_sync_id( "DM1" ); + scheme.set_expiry_time_ms_epoch( clock->systemTimeSinceEpochMs() + 1000 * 60 ); - uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp + 3, waitTimeMs ).triggeredCollectionSchemeData; - ASSERT_NE( collectedData, nullptr ); - ASSERT_EQ( collectedData->signals.size(), 1 ); + // Define the collection condition: (signalID(1)>-100) + { + Schemas::CollectionSchemesMsg::ConditionBasedCollectionScheme *message = + scheme.mutable_condition_based_collection_scheme(); + message->set_condition_minimum_interval_ms( 650 ); + message->set_condition_language_version( 20 ); + message->set_condition_trigger_mode( + Schemas::CollectionSchemesMsg::ConditionBasedCollectionScheme_ConditionTriggerMode_TRIGGER_ALWAYS ); + + auto *root = new Schemas::CommonTypesMsg::ConditionNode(); + message->set_allocated_condition_tree( root ); + auto *rootOp = new Schemas::CommonTypesMsg::ConditionNode_NodeOperator(); + root->set_allocated_node_operator( rootOp ); + rootOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + //---------- + + auto *left = new Schemas::CommonTypesMsg::ConditionNode(); + rootOp->set_allocated_left_child( left ); + left->set_node_signal_id( signalID1 ); + + auto *right = new Schemas::CommonTypesMsg::ConditionNode(); + rootOp->set_allocated_right_child( right ); + right->set_node_double_value( -100 ); + } - EXPECT_EQ( collectedData->signals[0].value.value.doubleVal, 0.4 ); -} + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); -TEST_F( CollectionInspectionEngineDoubleTest, CollectBurstWithoutSubsampling ) -{ - CollectionInspectionEngine engine; - // minimumSampleIntervalMs=0 means no subsampling - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 1234; - s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 77777; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); - // Condition is static by default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + // partition 0 + auto *partition = store_and_forward_configuration->add_partition_configuration(); + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); - TimePoint timestamp = { 160000000, 100 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + auto *forwardOptions = new Schemas::CollectionSchemesMsg::UploadOptions(); + partition->set_allocated_upload_options( forwardOptions ); + { + auto *root = forwardOptions->mutable_condition_tree(); + auto *rootOp = root->mutable_node_operator(); + rootOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_LOGICAL_AND ); - // All signal come at the same timestamp - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.2 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.3 ); + //---------- - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + auto *left = rootOp->mutable_left_child(); + auto *leftOp = left->mutable_node_operator(); + leftOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + auto *right = rootOp->mutable_right_child(); + auto *rightOp = right->mutable_node_operator(); + rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + //---------- + + auto *left_left = leftOp->mutable_left_child(); + left_left->set_node_signal_id( signalID1 ); + + auto *left_right = leftOp->mutable_right_child(); + left_right->set_node_double_value( -100 ); + + auto *right_left = rightOp->mutable_left_child(); + right_left->set_node_signal_id( signalID2 ); + + auto *right_right = rightOp->mutable_right_child(); + right_right->set_node_double_value( -500 ); + } + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( signalID1 ); + signalInformation->set_data_partition_id( defaultPartitionID ); + signalInformation->set_sample_buffer_size( sampleBufferSize ); + signalInformation->set_minimum_sample_period_ms( minimumSampleIntervalMs ); + signalInformation->set_fixed_window_period_ms( fixedWindowPeriod ); + signalInformation->set_condition_only_signal( false ); + + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( signalID2 ); + signalInformation->set_data_partition_id( defaultPartitionID ); + signalInformation->set_sample_buffer_size( sampleBufferSize ); + signalInformation->set_minimum_sample_period_ms( minimumSampleIntervalMs ); + signalInformation->set_fixed_window_period_ms( fixedWindowPeriod ); + signalInformation->set_condition_only_signal( true ); + + this->convertScheme( scheme, campaignWithSinglePartition ); + } + // create inspection matrix based on campaign config + const std::string decoderManifestID = "DM1"; + CANInterfaceIDTranslator canIDTranslator; + auto collectionSchemeManager = std::make_shared( + nullptr, canIDTranslator, std::make_shared( nullptr ), decoderManifestID ); + IDecoderManifestPtr DM1 = std::make_shared( decoderManifestID ); + collectionSchemeManager->onDecoderManifestUpdate( DM1 ); + collectionSchemeManager->onCollectionSchemeUpdate( campaignWithSinglePartition ); + collectionSchemeManager->updateAvailable(); + ASSERT_TRUE( collectionSchemeManager->getmProcessCollectionScheme() ); + collectionSchemeManager->updateMapsandTimeLine( clock->timeSinceEpoch() ); + std::shared_ptr matrix; + std::shared_ptr fetchMatrix; + collectionSchemeManager->generateInspectionMatrix( matrix, fetchMatrix ); + + // Boot up the inspection engine + TimePoint timestamp = { 160000000, 100 }; + std::shared_ptr engine = std::make_shared(); + engine->onChangeInspectionMatrix( matrix, timestamp ); + engine->evaluateConditions( timestamp ); + + // Verify the forward condition is present and has not triggered + auto currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + auto campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // Verify the collection condition has not collected anything uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_EQ( engine->collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Send in some signals which should cause collection to trigger, but not forward + timestamp += 1000; + engine->addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine->addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + ASSERT_TRUE( engine->evaluateConditions( timestamp ) ); + + // Verify our forward condition still has not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // Verify we have collected the signal + auto collectedData = engine->collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; ASSERT_NE( collectedData, nullptr ); - ASSERT_EQ( collectedData->signals.size(), 3 ); + ASSERT_EQ( collectedData->signals.size(), 1 ); + ASSERT_EQ( collectedData->signals[0].signalID, s1.signalID ); + ASSERT_EQ( collectedData->signals[0].getValue().value.doubleVal, -90.0 ); + ASSERT_EQ( collectedData->triggerTime, timestamp.systemTimeMs ); - EXPECT_EQ( collectedData->signals[0].value.value.doubleVal, 0.3 ); - EXPECT_EQ( collectedData->signals[1].value.value.doubleVal, 0.2 ); - EXPECT_EQ( collectedData->signals[2].value.value.doubleVal, 0.1 ); + // 5. Send in some signals which should cause the condition to trigger + timestamp += 1000; + engine->addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine->evaluateConditions( timestamp ); + + // 5a. Verify the forward condition has now triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), true ); + + // 6. Send in some signals which should cause the condition to go back to not triggered + timestamp += 1000; + engine->addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -520.0 ); + engine->evaluateConditions( timestamp ); + + // 6a. Verify our forward condition is now not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + ASSERT_EQ( currentForwardCampaigns.count( campaignID ), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID]; + ASSERT_EQ( campaignForwardConditionStates.size(), 1 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); } -TEST_F( CollectionInspectionEngineDoubleTest, IllegalSignalID ) +TYPED_TEST( CollectionInspectionEngineTest, MultipleCampaignsAndPartitionsWithForwardAndTimedCollectionConditions ) { - CollectionInspectionEngine engine; - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 0xFFFFFFFF; - s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 77777; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); + const SignalID signalID1 = 1; + const SignalID signalID2 = 2; + uint32_t sampleBufferSize = 50; + uint32_t minimumSampleIntervalMs = 10; + uint32_t fixedWindowPeriod = 77777; - // Condition is static by default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + const uint32_t defaultPartitionID = 0; + const uint32_t partitionID1 = 1; + + InspectionMatrixSignalCollectionInfo s1{ + signalID1, sampleBufferSize, minimumSampleIntervalMs, fixedWindowPeriod, false, SignalType::DOUBLE, {} }; + InspectionMatrixSignalCollectionInfo s2{ + signalID2, sampleBufferSize, minimumSampleIntervalMs, fixedWindowPeriod, true, SignalType::DOUBLE, {} }; + + std::shared_ptr clock = ClockHandler::getClock(); + + const SyncID campaignID1 = "arn:1/T0123"; + Schemas::CollectionSchemesMsg::CollectionScheme scheme1; + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + + scheme.set_campaign_sync_id( campaignID1 ); + scheme.set_campaign_arn( campaignID1 ); + scheme.set_decoder_manifest_sync_id( "DM1" ); + scheme.set_expiry_time_ms_epoch( clock->systemTimeSinceEpochMs() + 1000 * 60 ); + + Schemas::CollectionSchemesMsg::TimeBasedCollectionScheme *message = + scheme.mutable_time_based_collection_scheme(); + message->set_time_based_collection_scheme_period_ms( 100 ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + // partition 0 + auto *partition = store_and_forward_configuration->add_partition_configuration(); + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + auto *forwardOptions = new Schemas::CollectionSchemesMsg::UploadOptions(); + partition->set_allocated_upload_options( forwardOptions ); + { + auto *root = forwardOptions->mutable_condition_tree(); + auto *rootOp = root->mutable_node_operator(); + rootOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_LOGICAL_AND ); + + //---------- + + auto *left = rootOp->mutable_left_child(); + auto *leftOp = left->mutable_node_operator(); + leftOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + auto *right = rootOp->mutable_right_child(); + auto *rightOp = right->mutable_node_operator(); + rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + //---------- + + auto *left_left = leftOp->mutable_left_child(); + left_left->set_node_signal_id( signalID1 ); + + auto *left_right = leftOp->mutable_right_child(); + left_right->set_node_double_value( -100 ); + + auto *right_left = rightOp->mutable_left_child(); + right_left->set_node_signal_id( signalID2 ); + + auto *right_right = rightOp->mutable_right_child(); + right_right->set_node_double_value( -500 ); + } + + // partition 2 + partition = store_and_forward_configuration->add_partition_configuration(); + storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition1" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + forwardOptions = new Schemas::CollectionSchemesMsg::UploadOptions(); + partition->set_allocated_upload_options( forwardOptions ); + { + auto *root = forwardOptions->mutable_condition_tree(); + auto *rootOp = root->mutable_node_operator(); + rootOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_LOGICAL_AND ); + + //---------- + + auto *left = rootOp->mutable_left_child(); + auto *leftOp = left->mutable_node_operator(); + leftOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + auto *right = rootOp->mutable_right_child(); + auto *rightOp = right->mutable_node_operator(); + rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + //---------- + + auto *left_left = leftOp->mutable_left_child(); + left_left->set_node_signal_id( signalID1 ); + + auto *left_right = leftOp->mutable_right_child(); + left_right->set_node_double_value( -100 ); + + auto *right_left = rightOp->mutable_left_child(); + right_left->set_node_signal_id( signalID2 ); + + auto *right_right = rightOp->mutable_right_child(); + right_right->set_node_double_value( -500 ); + } + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( signalID1 ); + signalInformation->set_data_partition_id( defaultPartitionID ); + signalInformation->set_sample_buffer_size( sampleBufferSize ); + signalInformation->set_minimum_sample_period_ms( minimumSampleIntervalMs ); + signalInformation->set_fixed_window_period_ms( fixedWindowPeriod ); + signalInformation->set_condition_only_signal( false ); + + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( signalID2 ); + signalInformation->set_data_partition_id( partitionID1 ); + signalInformation->set_sample_buffer_size( sampleBufferSize ); + signalInformation->set_minimum_sample_period_ms( minimumSampleIntervalMs ); + signalInformation->set_fixed_window_period_ms( fixedWindowPeriod ); + signalInformation->set_condition_only_signal( true ); + + scheme.add_raw_can_frames_to_collect(); + + scheme1 = scheme; + } + + const SyncID campaignID2 = "arn:2/T0456"; + Schemas::CollectionSchemesMsg::CollectionScheme scheme2; + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + + scheme.set_campaign_sync_id( campaignID2 ); + scheme.set_campaign_arn( campaignID2 ); + scheme.set_decoder_manifest_sync_id( "DM1" ); + scheme.set_expiry_time_ms_epoch( clock->systemTimeSinceEpochMs() + 1000 * 60 ); + + Schemas::CollectionSchemesMsg::TimeBasedCollectionScheme *message = + scheme.mutable_time_based_collection_scheme(); + message->set_time_based_collection_scheme_period_ms( 100 ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + // partition 0 + auto *partition = store_and_forward_configuration->add_partition_configuration(); + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + auto *forwardOptions = new Schemas::CollectionSchemesMsg::UploadOptions(); + partition->set_allocated_upload_options( forwardOptions ); + { + auto *root = forwardOptions->mutable_condition_tree(); + auto *rootOp = root->mutable_node_operator(); + rootOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_LOGICAL_AND ); + + //---------- + + auto *left = rootOp->mutable_left_child(); + auto *leftOp = left->mutable_node_operator(); + leftOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + auto *right = rootOp->mutable_right_child(); + auto *rightOp = right->mutable_node_operator(); + rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + //---------- + + auto *left_left = leftOp->mutable_left_child(); + left_left->set_node_signal_id( signalID1 ); + + auto *left_right = leftOp->mutable_right_child(); + left_right->set_node_double_value( -100 ); + + auto *right_left = rightOp->mutable_left_child(); + right_left->set_node_signal_id( signalID2 ); + + auto *right_right = rightOp->mutable_right_child(); + right_right->set_node_double_value( -500 ); + } + + // partition 2 + partition = store_and_forward_configuration->add_partition_configuration(); + storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition1" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // Define the forward condition: (signalID(1)>-100) && (signalID(2)>-500) + forwardOptions = new Schemas::CollectionSchemesMsg::UploadOptions(); + partition->set_allocated_upload_options( forwardOptions ); + { + auto *root = forwardOptions->mutable_condition_tree(); + auto *rootOp = root->mutable_node_operator(); + rootOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_LOGICAL_AND ); + + //---------- + + auto *left = rootOp->mutable_left_child(); + auto *leftOp = left->mutable_node_operator(); + leftOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + + auto *right = rootOp->mutable_right_child(); + auto *rightOp = right->mutable_node_operator(); + rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER ); + //---------- + + auto *left_left = leftOp->mutable_left_child(); + left_left->set_node_signal_id( signalID1 ); + + auto *left_right = leftOp->mutable_right_child(); + left_right->set_node_double_value( -100 ); + + auto *right_left = rightOp->mutable_left_child(); + right_left->set_node_signal_id( signalID2 ); + + auto *right_right = rightOp->mutable_right_child(); + right_right->set_node_double_value( -500 ); + } + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( signalID1 ); + signalInformation->set_data_partition_id( defaultPartitionID ); + signalInformation->set_sample_buffer_size( sampleBufferSize ); + signalInformation->set_minimum_sample_period_ms( minimumSampleIntervalMs ); + signalInformation->set_fixed_window_period_ms( fixedWindowPeriod ); + signalInformation->set_condition_only_signal( false ); + + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( signalID2 ); + signalInformation->set_data_partition_id( partitionID1 ); + signalInformation->set_sample_buffer_size( sampleBufferSize ); + signalInformation->set_minimum_sample_period_ms( minimumSampleIntervalMs ); + signalInformation->set_fixed_window_period_ms( fixedWindowPeriod ); + signalInformation->set_condition_only_signal( true ); + + scheme.add_raw_can_frames_to_collect(); + + scheme2 = scheme; + } + + // Boot up the inspection engine TimePoint timestamp = { 160000000, 100 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + std::shared_ptr engine = std::make_shared(); + + // add decoder manifest + const std::string decoderManifestID = "DM1"; + CANInterfaceIDTranslator canIDTranslator; + auto collectionSchemeManager = std::make_shared( + nullptr, canIDTranslator, std::make_shared( nullptr ), decoderManifestID ); + IDecoderManifestPtr DM1 = std::make_shared( decoderManifestID ); + collectionSchemeManager->onDecoderManifestUpdate( DM1 ); + + // generate inspection matrix based on campaigns + std::shared_ptr campaigns; + this->convertSchemes( { scheme1, scheme2 }, campaigns ); + collectionSchemeManager->onCollectionSchemeUpdate( campaigns ); + collectionSchemeManager->updateAvailable(); + ASSERT_TRUE( collectionSchemeManager->getmProcessCollectionScheme() ); + collectionSchemeManager->updateMapsandTimeLine( clock->timeSinceEpoch() ); + std::shared_ptr matrix; + std::shared_ptr fetchMatrix; + collectionSchemeManager->generateInspectionMatrix( matrix, fetchMatrix ); + // add matrix to the inspection engine + engine->onChangeInspectionMatrix( matrix, timestamp ); + engine->evaluateConditions( timestamp ); + + // Verify the forward condition is present and has not triggered + auto currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 2 ); + auto campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID2]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // Verify the collection condition has not collected anything + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine->collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Send in some signals which should cause collection to trigger, but not forward + timestamp += 1000; + engine->addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine->addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + engine->evaluateConditions( timestamp ); + + // Verify our forward condition still has not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 2 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID2]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // Verify we have collected the signal + auto collectedData = engine->collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->signals.size(), 1 ); + ASSERT_EQ( collectedData->signals[0].signalID, s1.signalID ); + ASSERT_EQ( collectedData->signals[0].getValue().value.doubleVal, -90.0 ); + ASSERT_EQ( collectedData->triggerTime, timestamp.systemTimeMs ); + + // 5. Send in some signals which should cause the condition to trigger + timestamp += 1000; + engine->addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine->evaluateConditions( timestamp ); + + // 5a. Verify the forward condition has now triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 2 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), true ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID2]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), true ); + + // 6. Send in some signals which should cause the condition to go back to not triggered + timestamp += 1000; + engine->addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -520.0 ); + engine->evaluateConditions( timestamp ); + + // 6a. Verify our forward condition is now not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 2 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID2]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // remove a campaign from matrix + + this->convertSchemes( { scheme1 }, campaigns ); + collectionSchemeManager->onCollectionSchemeUpdate( campaigns ); + collectionSchemeManager->updateAvailable(); + ASSERT_TRUE( collectionSchemeManager->getmProcessCollectionScheme() ); + collectionSchemeManager->updateMapsandTimeLine( clock->timeSinceEpoch() ); + collectionSchemeManager->generateInspectionMatrix( matrix, fetchMatrix ); + // add matrix to the inspection engine + engine->onChangeInspectionMatrix( matrix, timestamp ); + engine->evaluateConditions( timestamp ); + + // Verify the forward condition is present and has not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // Verify the collection condition has not collected anything + waitTimeMs = 0; + ASSERT_EQ( engine->collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Send in some signals which should cause collection to trigger, but not forward + timestamp += 1000; + engine->addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine->addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + engine->evaluateConditions( timestamp ); + + // Verify our forward condition still has not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); + + // Verify we have collected the signal + collectedData = engine->collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->signals.size(), 1 ); + ASSERT_EQ( collectedData->signals[0].signalID, s1.signalID ); + ASSERT_EQ( collectedData->signals[0].getValue().value.doubleVal, -90.0 ); + ASSERT_EQ( collectedData->triggerTime, timestamp.systemTimeMs ); + + // 5. Send in some signals which should cause the condition to trigger + timestamp += 1000; + engine->addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine->evaluateConditions( timestamp ); + + // 5a. Verify the forward condition has now triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), true ); + + // 6. Send in some signals which should cause the condition to go back to not triggered + timestamp += 1000; + engine->addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -520.0 ); + engine->evaluateConditions( timestamp ); + + // 6a. Verify our forward condition is now not triggered + currentForwardCampaigns = engine->forwardConditionForCampaignPartitions(); + ASSERT_EQ( currentForwardCampaigns.size(), 1 ); + campaignForwardConditionStates = currentForwardCampaigns[campaignID1]; + ASSERT_EQ( campaignForwardConditionStates.size(), 2 ); + ASSERT_EQ( campaignForwardConditionStates.front(), false ); } +#endif -TEST_F( CollectionInspectionEngineDoubleTest, IllegalSampleSize ) +TEST_F( CollectionInspectionEngineDoubleTest, EndlessCondition ) { CollectionInspectionEngine engine; + // minimumSampleIntervalMs=0 means no subsampling InspectionMatrixSignalCollectionInfo s1{}; s1.signalID = 1234; - s1.sampleBufferSize = 0; // Not allowed + s1.sampleBufferSize = 50; s1.minimumSampleIntervalMs = 0; s1.fixedWindowPeriod = 77777; s1.signalType = SignalType::DOUBLE; addSignalToCollect( collectionSchemes->conditions[0], s1 ); - // Condition is static by default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + ExpressionNode endless; + + endless.nodeType = ExpressionNodeType::OPERATOR_LOGICAL_AND; + endless.left = &endless; + endless.right = &endless; TimePoint timestamp = { 160000000, 100 }; + + // Condition is static by default + collectionSchemes->conditions[0].condition = &endless; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + + uint32_t waitTimeMs = 0; + EXPECT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); } -TEST_F( CollectionInspectionEngineDoubleTest, ZeroSignalsOnlyDTCCollection ) +TEST_F( CollectionInspectionEngineDoubleTest, TooBigForSignalBuffer ) { CollectionInspectionEngine engine; - collectionSchemes->conditions[0].includeActiveDtcs = true; - // Condition is static by default + // minimumSampleIntervalMs=0 means no subsampling + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 3072; + s1.sampleBufferSize = + 500000000; // this number of samples should exceed the maximum buffer size defined in MAX_SAMPLE_MEMORY + s1.minimumSampleIntervalMs = 5; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + // Condition is static be default collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - DTCInfo dtcInfo; - dtcInfo.mDTCCodes.push_back( "B1217" ); - dtcInfo.mSID = SID::STORED_DTC; - dtcInfo.receiveTime = timestamp.systemTimeMs; - engine.setActiveDTCs( dtcInfo ); - timestamp += 1000; - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + engine.addNewSignal( s1.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp, + timestamp.monotonicTimeMs, + 0.1 ); // All signal come at the same timestamp + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 1000, timestamp.monotonicTimeMs + 1000, 0.2 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 2000, timestamp.monotonicTimeMs + 2000, 0.3 ); + + ASSERT_TRUE( engine.evaluateConditions( timestamp + 3000 ) ); + uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + auto collectedData = engine.collectNextDataToSend( timestamp + 5000, waitTimeMs ).triggeredCollectionSchemeData; ASSERT_NE( collectedData, nullptr ); - EXPECT_EQ( collectedData->mDTCInfo.mDTCCodes[0], "B1217" ); + ASSERT_EQ( collectedData->signals.size(), 0 ); } -TEST_F( CollectionInspectionEngineDoubleTest, CollectRawCanFrames ) +TEST_F( CollectionInspectionEngineDoubleTest, TooBigForSignalBufferOverflow ) { CollectionInspectionEngine engine; // minimumSampleIntervalMs=0 means no subsampling + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 536870912; + s1.minimumSampleIntervalMs = 5; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); InspectionMatrixCanFrameCollectionInfo c1; c1.frameID = 0x380; c1.channelID = 3; - c1.sampleBufferSize = 10; + c1.sampleBufferSize = 536870912; c1.minimumSampleIntervalMs = 0; collectionSchemes->conditions[0].canFrames.push_back( c1 ); + + collectionSchemes->conditions[0].minimumPublishIntervalMs = 500; // Condition is static by default collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - std::array buf = { 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp, buf, sizeof( buf ) ); + // All signal come at the same timestamp + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 1000, timestamp.monotonicTimeMs + 1000, 0.2 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 2000, timestamp.monotonicTimeMs + 2000, 0.3 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + ASSERT_TRUE( engine.evaluateConditions( timestamp + 3000 ) ); uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + auto collectedData = engine.collectNextDataToSend( timestamp + 4000, waitTimeMs ).triggeredCollectionSchemeData; ASSERT_NE( collectedData, nullptr ); - ASSERT_EQ( collectedData->canFrames.size(), 1 ); - - EXPECT_EQ( collectedData->canFrames[0].frameID, c1.frameID ); - EXPECT_EQ( collectedData->canFrames[0].channelId, c1.channelID ); - EXPECT_EQ( collectedData->canFrames[0].size, sizeof( buf ) ); - EXPECT_TRUE( 0 == std::memcmp( collectedData->canFrames[0].data.data(), buf.data(), sizeof( buf ) ) ); + ASSERT_EQ( collectedData->signals.size(), 0 ); } -TEST_F( CollectionInspectionEngineDoubleTest, CollectRawCanFDFrames ) +TEST_F( CollectionInspectionEngineDoubleTest, SignalBufferErasedAfterNewConditions ) { CollectionInspectionEngine engine; // minimumSampleIntervalMs=0 means no subsampling - InspectionMatrixCanFrameCollectionInfo c1; - c1.frameID = 0x380; - c1.channelID = 3; - c1.sampleBufferSize = 10; - c1.minimumSampleIntervalMs = 0; - collectionSchemes->conditions[0].canFrames.push_back( c1 ); + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); // Condition is static by default collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - std::array buf = { - 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0, 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp, buf, sizeof( buf ) ); + // All signal come at the same timestamp + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 1, timestamp.monotonicTimeMs + 1, 0.2 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 2, timestamp.monotonicTimeMs + 2, 0.3 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + // This call will flush the signal history buffer even when + // exactly the same conditions are handed over. + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp + 3 ); + + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 3, timestamp.monotonicTimeMs + 3, 0.4 ); + + ASSERT_TRUE( engine.evaluateConditions( timestamp + 3 ) ); uint32_t waitTimeMs = 0; - auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + auto collectedData = engine.collectNextDataToSend( timestamp + 3, waitTimeMs ).triggeredCollectionSchemeData; ASSERT_NE( collectedData, nullptr ); - ASSERT_EQ( collectedData->canFrames.size(), 1 ); + ASSERT_EQ( collectedData->signals.size(), 1 ); - EXPECT_EQ( collectedData->canFrames[0].frameID, c1.frameID ); - EXPECT_EQ( collectedData->canFrames[0].channelId, c1.channelID ); - EXPECT_EQ( collectedData->canFrames[0].size, sizeof( buf ) ); - EXPECT_TRUE( 0 == std::memcmp( collectedData->canFrames[0].data.data(), buf.data(), sizeof( buf ) ) ); + EXPECT_EQ( collectedData->signals[0].value.value.doubleVal, 0.4 ); } -TEST_F( CollectionInspectionEngineDoubleTest, MultipleCanSubsampling ) +TEST_F( CollectionInspectionEngineDoubleTest, CollectBurstWithoutSubsampling ) { CollectionInspectionEngine engine; - InspectionMatrixCanFrameCollectionInfo c1; - c1.frameID = 0x380; - c1.channelID = 3; - c1.sampleBufferSize = 10; - c1.minimumSampleIntervalMs = 100; - collectionSchemes->conditions[0].canFrames.push_back( c1 ); - - InspectionMatrixCanFrameCollectionInfo c2; - c2.frameID = 0x380; - c2.channelID = 3; - c2.sampleBufferSize = 10; - // Same frame id but different subsampling Interval - c2.minimumSampleIntervalMs = 500; - collectionSchemes->conditions[0].canFrames.push_back( c2 ); - - // Add to a second collectionScheme. this should reuse the same buffers - collectionSchemes->conditions[1].canFrames.push_back( c1 ); - collectionSchemes->conditions[1].canFrames.push_back( c2 ); - + // minimumSampleIntervalMs=0 means no subsampling + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); // Condition is static by default collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - std::array buf = { 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; - engine.addNewRawCanFrame( c1.frameID, - c1.channelID, - timestamp, - buf, - sizeof( buf ) ); // this message in goes with both subsampling - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 100, buf, sizeof( buf ) ); // 100 ms subsampling - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 200, buf, sizeof( buf ) ); // 100 ms subsampling - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 250, buf, sizeof( buf ) ); // ignored - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 300, buf, sizeof( buf ) ); // 100 ms subsampling - engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 400, buf, sizeof( buf ) ); // 100 ms subsampling - engine.addNewRawCanFrame( c1.frameID, - c1.channelID, - timestamp + 500, - buf, + // All signal come at the same timestamp + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.2 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.3 ); + + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + + uint32_t waitTimeMs = 0; + auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->signals.size(), 3 ); + + EXPECT_EQ( collectedData->signals[0].value.value.doubleVal, 0.3 ); + EXPECT_EQ( collectedData->signals[1].value.value.doubleVal, 0.2 ); + EXPECT_EQ( collectedData->signals[2].value.value.doubleVal, 0.1 ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, IllegalSignalID ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 0xFFFFFFFF; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, IllegalSampleSize ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 0; // Not allowed + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, ZeroSignalsOnlyDTCCollection ) +{ + CollectionInspectionEngine engine; + collectionSchemes->conditions[0].includeActiveDtcs = true; + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + DTCInfo dtcInfo; + dtcInfo.mDTCCodes.push_back( "B1217" ); + dtcInfo.mSID = SID::STORED_DTC; + dtcInfo.receiveTime = timestamp.systemTimeMs; + engine.setActiveDTCs( dtcInfo ); + timestamp += 1000; + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + EXPECT_EQ( collectedData->mDTCInfo.mDTCCodes[0], "B1217" ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, CollectRawCanFrames ) +{ + CollectionInspectionEngine engine; + // minimumSampleIntervalMs=0 means no subsampling + InspectionMatrixCanFrameCollectionInfo c1; + c1.frameID = 0x380; + c1.channelID = 3; + c1.sampleBufferSize = 10; + c1.minimumSampleIntervalMs = 0; + collectionSchemes->conditions[0].canFrames.push_back( c1 ); + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + std::array buf = { 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp, buf, sizeof( buf ) ); + + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + + uint32_t waitTimeMs = 0; + auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->canFrames.size(), 1 ); + + EXPECT_EQ( collectedData->canFrames[0].frameID, c1.frameID ); + EXPECT_EQ( collectedData->canFrames[0].channelId, c1.channelID ); + EXPECT_EQ( collectedData->canFrames[0].size, sizeof( buf ) ); + EXPECT_TRUE( 0 == std::memcmp( collectedData->canFrames[0].data.data(), buf.data(), sizeof( buf ) ) ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, CollectRawCanFDFrames ) +{ + CollectionInspectionEngine engine; + // minimumSampleIntervalMs=0 means no subsampling + InspectionMatrixCanFrameCollectionInfo c1; + c1.frameID = 0x380; + c1.channelID = 3; + c1.sampleBufferSize = 10; + c1.minimumSampleIntervalMs = 0; + collectionSchemes->conditions[0].canFrames.push_back( c1 ); + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + std::array buf = { + 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0, 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp, buf, sizeof( buf ) ); + + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + + uint32_t waitTimeMs = 0; + auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->canFrames.size(), 1 ); + + EXPECT_EQ( collectedData->canFrames[0].frameID, c1.frameID ); + EXPECT_EQ( collectedData->canFrames[0].channelId, c1.channelID ); + EXPECT_EQ( collectedData->canFrames[0].size, sizeof( buf ) ); + EXPECT_TRUE( 0 == std::memcmp( collectedData->canFrames[0].data.data(), buf.data(), sizeof( buf ) ) ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, MultipleCanSubsampling ) +{ + CollectionInspectionEngine engine; + InspectionMatrixCanFrameCollectionInfo c1; + c1.frameID = 0x380; + c1.channelID = 3; + c1.sampleBufferSize = 10; + c1.minimumSampleIntervalMs = 100; + collectionSchemes->conditions[0].canFrames.push_back( c1 ); + + InspectionMatrixCanFrameCollectionInfo c2; + c2.frameID = 0x380; + c2.channelID = 3; + c2.sampleBufferSize = 10; + // Same frame id but different subsampling Interval + c2.minimumSampleIntervalMs = 500; + collectionSchemes->conditions[0].canFrames.push_back( c2 ); + + // Add to a second collectionScheme. this should reuse the same buffers + collectionSchemes->conditions[1].canFrames.push_back( c1 ); + collectionSchemes->conditions[1].canFrames.push_back( c2 ); + + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + std::array buf = { 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; + engine.addNewRawCanFrame( c1.frameID, + c1.channelID, + timestamp, + buf, + sizeof( buf ) ); // this message in goes with both subsampling + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 100, buf, sizeof( buf ) ); // 100 ms subsampling + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 200, buf, sizeof( buf ) ); // 100 ms subsampling + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 250, buf, sizeof( buf ) ); // ignored + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 300, buf, sizeof( buf ) ); // 100 ms subsampling + engine.addNewRawCanFrame( c1.frameID, c1.channelID, timestamp + 400, buf, sizeof( buf ) ); // 100 ms subsampling + engine.addNewRawCanFrame( c1.frameID, + c1.channelID, + timestamp + 500, + buf, sizeof( buf ) ); // this message in goes with both subsampling ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -886,10 +1852,14 @@ TEST_F( CollectionInspectionEngineDoubleTest, MultipleSubsamplingOfSameSignalUse for ( int i = 0; i < 100; i++ ) { - engine.addNewSignal( s1.signalID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, i * 2 ); - engine.addNewSignal( s3.signalID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, i * 2 + 1 ); - engine.addNewSignal( s5.signalID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, 55555 ); - engine.addNewSignal( s6.signalID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, 77777 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, i * 2 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, i * 2 + 1 ); + engine.addNewSignal( + s5.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, 55555 ); + engine.addNewSignal( + s6.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + i * 10, timestamp.monotonicTimeMs + i * 10, 77777 ); } ASSERT_TRUE( engine.evaluateConditions( timestamp + 10000 ) ); @@ -974,12 +1944,21 @@ TEST_F( CollectionInspectionEngineDoubleTest, MultipleFixedWindowsOfSameSignalUs for ( int i = 0; i < 300; i++ ) { - engine.addNewSignal( - s1.signalID, timestamp + 10000 + i * 10, timestamp.monotonicTimeMs + 10000 + i * 10, i * 2 ); - engine.addNewSignal( - s5.signalID, timestamp + 10000 + i * 10, timestamp.monotonicTimeMs + 10000 + i * 10, 55555 ); - engine.addNewSignal( - s6.signalID, timestamp + 10000 + i * 10, timestamp.monotonicTimeMs + 10000 + i * 10, 77777 ); + engine.addNewSignal( s1.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp + 10000 + i * 10, + timestamp.monotonicTimeMs + 10000 + i * 10, + i * 2 ); + engine.addNewSignal( s5.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp + 10000 + i * 10, + timestamp.monotonicTimeMs + 10000 + i * 10, + 55555 ); + engine.addNewSignal( s6.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp + 10000 + i * 10, + timestamp.monotonicTimeMs + 10000 + i * 10, + 77777 ); } ASSERT_TRUE( engine.evaluateConditions( timestamp + 10000 ) ); @@ -1035,12 +2014,16 @@ TEST_F( CollectionInspectionEngineDoubleTest, Subsampling ) TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); - engine.addNewSignal( s1.signalID, timestamp + 1, timestamp.monotonicTimeMs + 1, 0.2 ); - engine.addNewSignal( s1.signalID, timestamp + 9, timestamp.monotonicTimeMs + 9, 0.3 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 1, timestamp.monotonicTimeMs + 1, 0.2 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 9, timestamp.monotonicTimeMs + 9, 0.3 ); // As subsampling is 10 this value should be sampled again - engine.addNewSignal( s1.signalID, timestamp + 10, timestamp.monotonicTimeMs + 10, 0.4 ); - engine.addNewSignal( s1.signalID, timestamp + 40, timestamp.monotonicTimeMs + 40, 0.5 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 10, timestamp.monotonicTimeMs + 10, 0.4 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 40, timestamp.monotonicTimeMs + 40, 0.5 ); ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -1075,7 +2058,7 @@ TEST_F( CollectionInspectionEngineDoubleTest, SendOutEverySignalOnlyOnce ) TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); uint32_t waitTimeMs = 0; @@ -1087,7 +2070,7 @@ TEST_F( CollectionInspectionEngineDoubleTest, SendOutEverySignalOnlyOnce ) ASSERT_NE( res2, nullptr ); EXPECT_EQ( res2->signals.size(), 1 ); // Very old element in queue gets pushed - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); ASSERT_TRUE( engine.evaluateConditions( timestamp + 20000 ) ); auto res3 = engine.collectNextDataToSend( timestamp + 20000, waitTimeMs ).triggeredCollectionSchemeData; @@ -1114,25 +2097,28 @@ TYPED_TEST( CollectionInspectionEngineTest, HeartbeatInterval ) TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 11 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 11 ); engine.evaluateConditions( timestamp ); uint32_t waitTimeMs = 0; // it should not trigger because less than 10 seconds passed EXPECT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - engine.addNewSignal( s1.signalID, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 11 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 11 ); ASSERT_TRUE( engine.evaluateConditions( timestamp + 10000 ) ); // Triggers after 10s EXPECT_NE( engine.collectNextDataToSend( timestamp + 10000, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - engine.addNewSignal( s1.signalID, timestamp + 15000, timestamp.monotonicTimeMs + 15000, 11 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 15000, timestamp.monotonicTimeMs + 15000, 11 ); ASSERT_FALSE( engine.evaluateConditions( timestamp + 15000 ) ); // Not triggers after 15s EXPECT_EQ( engine.collectNextDataToSend( timestamp + 15000, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - engine.addNewSignal( s1.signalID, timestamp + 20000, timestamp.monotonicTimeMs + 20000, 11 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 20000, timestamp.monotonicTimeMs + 20000, 11 ); ASSERT_TRUE( engine.evaluateConditions( timestamp + 20000 ) ); // Triggers after 20s @@ -1171,7 +2157,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, RawBufferHandleUsageHints ) timestamp++; uint8_t dummyData[] = { 0xDE, 0xAD }; auto handle = rawDataManager->push( dummyData, sizeof( dummyData ), timestamp.systemTimeMs, s1.signalID ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, handle ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, handle ); } auto statistics = rawDataManager->getStatistics( s1.signalID ); EXPECT_EQ( statistics.numOfSamplesCurrentlyInMemory, 10 ); @@ -1182,7 +2169,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, RawBufferHandleUsageHints ) timestamp++; uint8_t dummyData[] = { 0xDE, 0xAD }; auto handle = rawDataManager->push( dummyData, sizeof( dummyData ), timestamp.systemTimeMs, s1.signalID ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, handle ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, handle ); } auto statistics2 = rawDataManager->getStatistics( s1.signalID ); EXPECT_EQ( statistics2.numOfSamplesCurrentlyInMemory, 10 ); // @@ -1216,8 +2204,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, HearbeatIntervalWithVisionSystemDa TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 10 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 9000 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 10 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 9000 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); uint32_t waitTimeMs = 0; auto collected = engine.collectNextDataToSend( timestamp, waitTimeMs ); @@ -1225,8 +2213,10 @@ TEST_F( CollectionInspectionEngineDoubleTest, HearbeatIntervalWithVisionSystemDa ASSERT_EQ( collected.triggeredCollectionSchemeData, nullptr ); ASSERT_EQ( collected.triggeredVisionSystemData, nullptr ); - engine.addNewSignal( s1.signalID, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 11 ); - engine.addNewSignal( s2.signalID, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 9001 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 11 ); + engine.addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 9001 ); ASSERT_TRUE( engine.evaluateConditions( timestamp + 10000 ) ); // Triggers after 10s @@ -1288,8 +2278,10 @@ TEST_F( CollectionInspectionEngineDoubleTest, TwoCollectionSchemesWithDifferentN for ( int i = 0; i < 10000; i++ ) { timestamp++; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, i * i ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, i + 2 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, i * i ); + engine.addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, i + 2 ); } ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -1344,9 +2336,9 @@ TEST_F( CollectionInspectionEngineDoubleTest, TwoSignalsInConditionAndOneSignalT TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, 1000.0 ); - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, 2000.0 ); - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, 3000.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 1000.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 2000.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 3000.0 ); // Signals for condition are not available yet so collectionScheme should not trigger ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); @@ -1355,8 +2347,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, TwoSignalsInConditionAndOneSignalT timestamp += 1000; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -90.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); // Condition only for first signal is fulfilled (-90 > -100) but second not so boolean and is false ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); @@ -1364,7 +2356,7 @@ TEST_F( CollectionInspectionEngineDoubleTest, TwoSignalsInConditionAndOneSignalT timestamp += 1000; - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); // Condition is fulfilled so it should trigger ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -1406,8 +2398,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, RisingEdgeTrigger ) TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 1000.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 2000.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 1000.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 2000.0 ); // Condition evaluates to true but data is not collected (not rising edge) ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -1416,15 +2408,15 @@ TEST_F( CollectionInspectionEngineDoubleTest, RisingEdgeTrigger ) timestamp += 1000; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 0.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.0 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); timestamp += 1000; - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); // Condition is fulfilled so it should trigger (rising edge false->true) ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); @@ -1456,7 +2448,7 @@ TYPED_TEST( CollectionInspectionEngineTest, SendOutEverySignalOnlyOncePerCollect TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 10 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 10 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); uint32_t waitTimeMs = 0; @@ -1468,7 +2460,8 @@ TYPED_TEST( CollectionInspectionEngineTest, SendOutEverySignalOnlyOncePerCollect ASSERT_NE( res2, nullptr ); EXPECT_EQ( res2->signals.size(), 1 ); - engine.addNewSignal( s1.signalID, timestamp + 15000, timestamp.monotonicTimeMs + 15000, 10 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 15000, timestamp.monotonicTimeMs + 15000, 10 ); ASSERT_TRUE( engine.evaluateConditions( timestamp + 20000 ) ); auto res3 = engine.collectNextDataToSend( timestamp + 20000, waitTimeMs ).triggeredCollectionSchemeData; @@ -1478,7 +2471,7 @@ TYPED_TEST( CollectionInspectionEngineTest, SendOutEverySignalOnlyOncePerCollect TEST_F( CollectionInspectionEngineDoubleTest, SendOutEverySignalNotOnlyOncePerCollectionScheme ) { - CollectionInspectionEngine engine( false ); + CollectionInspectionEngine engine( 1000, false ); InspectionMatrixSignalCollectionInfo s1{}; s1.signalID = 1234; s1.sampleBufferSize = 50; @@ -1495,7 +2488,7 @@ TEST_F( CollectionInspectionEngineDoubleTest, SendOutEverySignalNotOnlyOncePerCo TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); uint32_t waitTimeMs = 0; @@ -1507,7 +2500,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, SendOutEverySignalNotOnlyOncePerCo ASSERT_NE( res2, nullptr ); EXPECT_EQ( res2->signals.size(), 1 ); - engine.addNewSignal( s1.signalID, timestamp + 15000, timestamp.monotonicTimeMs + 15000, 0.1 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 15000, timestamp.monotonicTimeMs + 15000, 0.1 ); ASSERT_TRUE( engine.evaluateConditions( timestamp + 20000 ) ); auto res3 = engine.collectNextDataToSend( timestamp + 20000, waitTimeMs ).triggeredCollectionSchemeData; @@ -1536,7 +2530,7 @@ TEST_F( CollectionInspectionEngineDoubleTest, MoreCollectionSchemesThanSupported engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); uint32_t waitTimeMs = 0; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.1 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.1 ); ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; timestamp += 10000; @@ -1588,9 +2582,9 @@ TYPED_TEST( CollectionInspectionEngineTest, CollectWithAfterTime ) engine.onChangeInspectionMatrix( this->consCollectionSchemes, timestamp ); TypeParam val1 = 10; - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, val1 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -90.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, val1 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -1000.0 ); // Condition not fulfilled so should not trigger ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); uint32_t waitTimeMs = 0; @@ -1600,9 +2594,9 @@ TYPED_TEST( CollectionInspectionEngineTest, CollectWithAfterTime ) timestamp += 1000; TimePoint timestamp0 = timestamp; TypeParam val2 = 20; - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, val2 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -90.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -480.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, val2 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -480.0 ); // Condition fulfilled so should trigger but afterTime not over yet ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); @@ -1611,9 +2605,9 @@ TYPED_TEST( CollectionInspectionEngineTest, CollectWithAfterTime ) timestamp += 1000; TimePoint timestamp1 = timestamp; TypeParam val3 = 30; - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, val3 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -95.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -485.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, val3 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -95.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -485.0 ); // Condition still fulfilled but already triggered. After time still not over ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); @@ -1622,9 +2616,9 @@ TYPED_TEST( CollectionInspectionEngineTest, CollectWithAfterTime ) timestamp += 500; TimePoint timestamp2 = timestamp; TypeParam val4 = 40; - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, val4 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -9000.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -9000.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, val4 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -9000.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -9000.0 ); // Condition not fulfilled anymore but still waiting for afterTime ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); @@ -1632,9 +2626,9 @@ TYPED_TEST( CollectionInspectionEngineTest, CollectWithAfterTime ) timestamp += 500; TimePoint timestamp3 = timestamp; TypeParam val5 = 50; - engine.addNewSignal( s3.signalID, timestamp, timestamp.monotonicTimeMs, val5 ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -9100.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, -9100.0 ); + engine.addNewSignal( s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, val5 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -9100.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -9100.0 ); // Condition not fulfilled. After Time is over so data should be sent out ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData; @@ -1679,7 +2673,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, AvgWindowCondition ) { timestamp++; currentValue += increasePerSample; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, currentValue ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, currentValue ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); } @@ -1691,7 +2686,8 @@ TEST_F( CollectionInspectionEngineDoubleTest, AvgWindowCondition ) { timestamp++; currentValue += increasePerSample; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, currentValue ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, currentValue ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); } @@ -1705,465 +2701,1277 @@ TEST_F( CollectionInspectionEngineDoubleTest, PrevLastAvgWindowCondition ) CollectionInspectionEngine engine; // fixedWindowPeriod means that we collect data over 300 seconds InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 1234; - s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 300000; - s1.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); + s1.signalID = 1234; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 300000; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // function is: PREV_LAST_FIXED_WINDOW_AVG(SignalID(1234)) > -50.0 + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getPrevLastAvgWindowBiggerCondition( s1.signalID, -50.0 ).get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + uint32_t waitTimeMs = 0; + // Fill the prev last window with 0.0 + for ( uint32_t i = 0; i < s1.fixedWindowPeriod; i++ ) + { + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + } + // No samples arrive for two window2: + timestamp += s1.fixedWindowPeriod * 2; + // One more arrives, prev last average is still 0 which is > -50 + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -100.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, MultiWindowCondition ) +{ + CollectionInspectionEngine engine; + // fixedWindowPeriod means that we collect data over 300 seconds + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // function is: (((LAST_FIXED_WINDOW_MAX(SignalID(1234)) - PREV_LAST_FIXED_WINDOW_MAX(SignalID(1234))) + // + PREV_LAST_FIXED_WINDOW_MAX(SignalID(1234))) + // < (LAST_FIXED_WINDOW_MAX(SignalID(1234)) * PREV_LAST_FIXED_WINDOW_MAX(SignalID(1234)))) + // || (LAST_FIXED_WINDOW_MIN(SignalID(1234)) == PREV_LAST_FIXED_WINDOW_MIN(SignalID(1234))) + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getMultiFixedWindowCondition( s1.signalID ).get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, -95.0 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 50, timestamp.monotonicTimeMs + 50, 100.0 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 70, timestamp.monotonicTimeMs + 70, 110.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp + 70 ) ); + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine.collectNextDataToSend( timestamp + 70, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 100, timestamp.monotonicTimeMs + 100, -205.0 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 150, timestamp.monotonicTimeMs + 150, -300.0 ); + engine.addNewSignal( + s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 200, timestamp.monotonicTimeMs + 200, +30.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp + 200 ) ); + ASSERT_NE( engine.collectNextDataToSend( timestamp + 200, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TestNotEqualOperator ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + s1.signalType = SignalType::DOUBLE; + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 456; + s2.sampleBufferSize = 50; + s2.minimumSampleIntervalMs = 0; + s2.fixedWindowPeriod = 100; + s2.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + addSignalToCollect( collectionSchemes->conditions[0], s2 ); + + // function is: !(!(SignalID(123) <= 0.001) && !((SignalID(123) / SignalID(456)) >= 0.5)) + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getNotEqualCondition( s1.signalID, s2.signalID ).get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be false because they are equal + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Expression should be true: because they are not equal + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 50 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Expression should be false because they are equal again + timestamp++; + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 50.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TestStringOperations ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // function is: ( "string" + "_1" ) == "string_1" && "string_1" != "string_2" + auto conditionNodePlus = getStringCondition( "string", "_1", ExpressionNodeType::OPERATOR_ARITHMETIC_PLUS ).get(); + + expressionNodes.push_back( std::make_shared() ); + auto nodeRightString = expressionNodes.back(); + nodeRightString->nodeType = ExpressionNodeType::STRING; + nodeRightString->stringValue = "string_1"; + + expressionNodes.push_back( std::make_shared() ); + auto nodeOperationEqual = expressionNodes.back(); + nodeOperationEqual->nodeType = ExpressionNodeType::OPERATOR_EQUAL; + nodeOperationEqual->left = conditionNodePlus; + nodeOperationEqual->right = nodeRightString.get(); + + auto conditionNodeRight = + getStringCondition( "string_1", "string_2", ExpressionNodeType::OPERATOR_NOT_EQUAL ).get(); + + expressionNodes.push_back( std::make_shared() ); + auto nodeOperation = expressionNodes.back(); + nodeOperation->nodeType = ExpressionNodeType::OPERATOR_LOGICAL_AND; + nodeOperation->left = nodeOperationEqual.get(); + nodeOperation->right = conditionNodeRight; + // Condition is static be default + collectionSchemes->conditions[0].condition = nodeOperation.get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be true + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TestTypeMismatch ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // function is: "string_1" == 2.0 + expressionNodes.push_back( std::make_shared() ); + auto nodeLeft = expressionNodes.back(); + nodeLeft->nodeType = ExpressionNodeType::STRING; + nodeLeft->stringValue = "string_1"; + + expressionNodes.push_back( std::make_shared() ); + auto nodeRight = expressionNodes.back(); + nodeRight->nodeType = ExpressionNodeType::FLOAT; + nodeRight->floatingValue = 2.0; + + expressionNodes.push_back( std::make_shared() ); + auto nodeOperation = expressionNodes.back(); + nodeOperation->nodeType = ExpressionNodeType::OPERATOR_EQUAL; + nodeOperation->left = nodeLeft.get(); + nodeOperation->right = nodeRight.get(); + // Condition is static be default + collectionSchemes->conditions[0].condition = nodeOperation.get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be false because of type mismatch + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TestBoolToDoubleImplicitCast ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // expression is: True + 1.0 == 2.0 + expressionNodes.push_back( std::make_shared() ); + auto nodeLeft = expressionNodes.back(); + nodeLeft->nodeType = ExpressionNodeType::BOOLEAN; + nodeLeft->booleanValue = true; + + expressionNodes.push_back( std::make_shared() ); + auto nodeRight = expressionNodes.back(); + nodeRight->nodeType = ExpressionNodeType::FLOAT; + nodeRight->floatingValue = 1.0; + + expressionNodes.push_back( std::make_shared() ); + auto nodeOperation = expressionNodes.back(); + nodeOperation->nodeType = ExpressionNodeType::OPERATOR_ARITHMETIC_PLUS; + nodeOperation->left = nodeLeft.get(); + nodeOperation->right = nodeRight.get(); + + expressionNodes.push_back( std::make_shared() ); + auto nodeRight2 = expressionNodes.back(); + nodeRight2->nodeType = ExpressionNodeType::FLOAT; + nodeRight2->floatingValue = 2.0; + + expressionNodes.push_back( std::make_shared() ); + auto nodeOperation2 = expressionNodes.back(); + nodeOperation2->nodeType = ExpressionNodeType::OPERATOR_EQUAL; + nodeOperation2->left = nodeOperation.get(); + nodeOperation2->right = nodeRight2.get(); + // Condition is static be default + collectionSchemes->conditions[0].condition = nodeOperation2.get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be true + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TestDoubleToBoolImplicitCast ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // expression is: 42.0 && True + expressionNodes.push_back( std::make_shared() ); + auto nodeLeft = expressionNodes.back(); + nodeLeft->nodeType = ExpressionNodeType::FLOAT; + nodeLeft->floatingValue = 42.0; + + expressionNodes.push_back( std::make_shared() ); + auto nodeRight = expressionNodes.back(); + nodeRight->nodeType = ExpressionNodeType::BOOLEAN; + nodeRight->booleanValue = true; + + expressionNodes.push_back( std::make_shared() ); + auto nodeOperation = expressionNodes.back(); + nodeOperation->nodeType = ExpressionNodeType::OPERATOR_LOGICAL_AND; + nodeOperation->left = nodeLeft.get(); + nodeOperation->right = nodeRight.get(); + // Condition is static be default + collectionSchemes->conditions[0].condition = nodeOperation.get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be true + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TwoSignalsRatioCondition ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + s1.signalType = SignalType::DOUBLE; + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 456; + s2.sampleBufferSize = 50; + s2.minimumSampleIntervalMs = 0; + s2.fixedWindowPeriod = 100; + s2.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + addSignalToCollect( collectionSchemes->conditions[0], s2 ); + + // function is: !(!(SignalID(123) <= 0.001) && !((SignalID(123) / SignalID(456)) >= 0.5)) + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = + getTwoSignalsRatioCondition( s1.signalID, 0.001, s2.signalID, 0.5 ).get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be false: (1.0 <= 0.001) || (1.0 / 100.0) >= 0.5) + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 1.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Expression should be true: (0.001 <= 0.001) || (0.001 / 100.0) >= 0.5) + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 0.001 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Expression should be false: (1.0 <= 0.001) || (1.0 / 100.0) >= 0.5) + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 1.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Expression should be true: (50.0 <= 0.001) || (50.0 / 100.0) >= 0.5) + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 50.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, UnknownExpressionNode ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 123; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + + // function is: (SignalID(123) <= Unknown) + // Condition is not static + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getUnknownCondition( s1.signalID ).get(); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // Expression should be false: (1.0 <= Unknown) + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 1.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, RequestTooMuchMemorySignals ) +{ + CollectionInspectionEngine engine; + // for each of the .sampleBufferSize=1000000 multiple bytes have to be allocated + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 1000000; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 300000; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + // Condition is static by default + collectionSchemes->conditions[1].condition = getAlwaysTrueCondition().get(); + TimePoint timestamp = { 0, 0 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, RequestTooMuchMemoryFrames ) +{ + CollectionInspectionEngine engine; + InspectionMatrixCanFrameCollectionInfo c1; + c1.frameID = 0x380; + c1.channelID = 3; + c1.sampleBufferSize = 1000000; + c1.minimumSampleIntervalMs = 0; + collectionSchemes->conditions[0].canFrames.push_back( c1 ); + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + TimePoint timestamp = { 0, 0 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); +} + +/* + * This test is also used for performance analysis. The add new signal takes > 100ns + * The performance varies if number of signal, number of conditions is changed. + */ +TEST_F( CollectionInspectionEngineDoubleTest, RandomDataTest ) +{ + const int NUMBER_OF_COLLECTION_SCHEMES = 200; + const int NUMBER_OF_SIGNALS = 20000; + + // Keep the seed static so multiple runs are comparable + std::default_random_engine eng{ static_cast( 1620336094 ) }; + std::mt19937 gen{ eng() }; + std::uniform_int_distribution<> uniformDistribution( 0, NUMBER_OF_COLLECTION_SCHEMES - 1 ); + std::normal_distribution<> normalDistribution{ 10, 4 }; + + for ( int i = 0; i < NUMBER_OF_COLLECTION_SCHEMES; i++ ) + { + + ConditionWithCollectedData collectionScheme; + collectionSchemes->conditions.resize( NUMBER_OF_COLLECTION_SCHEMES ); + // Condition is static by default + collectionSchemes->conditions[i].condition = getAlwaysTrueCondition().get(); + } + + CollectionInspectionEngine engine; + // for each of the .sampleBufferSize=1000000 multiple bytes have to be allocated + std::normal_distribution<> fixedWindowSizeGenerator( 300000, 100000 ); + int minWindow = std::numeric_limits::max(); + int maxWindow = std::numeric_limits::min(); + const int MINIMUM_WINDOW_SIZE = 500000; // should be 500000 + int withoutWindow = 0; + for ( int i = 0; i < NUMBER_OF_SIGNALS; i++ ) + { + + int windowSize = static_cast( fixedWindowSizeGenerator( gen ) ); + if ( windowSize < MINIMUM_WINDOW_SIZE ) + { // most signals dont have a window + windowSize = 0; + withoutWindow++; + } + else + { + minWindow = std::min( windowSize, minWindow ); + maxWindow = std::max( windowSize, maxWindow ); + } + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = i; + s1.sampleBufferSize = 10; + s1.minimumSampleIntervalMs = 1; + s1.fixedWindowPeriod = windowSize; + s1.signalType = SignalType::DOUBLE; + for ( int j = 0; j < normalDistribution( gen ); j++ ) + { + auto &collectionScheme = collectionSchemes->conditions[uniformDistribution( gen )]; + collectionScheme.signals.push_back( s1 ); + + // exactly two signals are added now so add them to condition: + if ( collectionScheme.signals.size() == 2 ) + { + // Condition contains signals + collectionScheme.isStaticCondition = false; + if ( uniformDistribution( gen ) < NUMBER_OF_COLLECTION_SCHEMES / 2 ) + { + collectionScheme.condition = getLastAvgWindowBiggerCondition( s1.signalID, 15 ).get(); + } + else + { + collectionScheme.condition = + getTwoSignalsBiggerCondition( s1.signalID, 17.5, collectionScheme.signals[0].signalID, 17.5 ) + .get(); + } + } + } + } + std::cout << "\nFinished generation " << NUMBER_OF_SIGNALS << " signals and " + << ( NUMBER_OF_SIGNALS - withoutWindow ) + << " of them with window with a window sampling size varies from " << minWindow << " to " << maxWindow + << std::endl; + + TimePoint START_TIMESTAMP = { 160000000, 100 }; + TimePoint timestamp = START_TIMESTAMP; + + auto originalLogLevel = Aws::IoTFleetWise::gSystemWideLogLevel; + try + { + // Temporarily change the log level since we have too many signals, which would make the test + // output too noisy with Trace level. + Aws::IoTFleetWise::gSystemWideLogLevel = Aws::IoTFleetWise::LogLevel::Info; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + } + catch ( ... ) + { + Aws::IoTFleetWise::gSystemWideLogLevel = originalLogLevel; + throw; + } + Aws::IoTFleetWise::gSystemWideLogLevel = originalLogLevel; + + const int TIME_TO_SIMULATE_IN_MS = 5000; + const int SIGNALS_PER_MS = 20; + uint32_t counter = 0; + uint32_t dataCollected = 0; + + std::default_random_engine eng2{ static_cast( 1620337567 ) }; + std::mt19937 gen2{ eng() }; + std::uniform_int_distribution<> signalIDGenerator( 0, NUMBER_OF_SIGNALS - 1 ); + for ( int i = 0; i < TIME_TO_SIMULATE_IN_MS * SIGNALS_PER_MS; i++ ) + { + if ( i % ( ( TIME_TO_SIMULATE_IN_MS * SIGNALS_PER_MS ) / 100 ) == 0 ) + { + std::cout << "." << std::flush; + } + counter++; + bool tickIncreased = false; + if ( counter == SIGNALS_PER_MS ) + { + timestamp++; + counter = 0; + tickIncreased = true; + } + engine.addNewSignal( signalIDGenerator( gen2 ), + DEFAULT_FETCH_REQUEST_ID, + timestamp, + timestamp.monotonicTimeMs, + normalDistribution( gen ) ); + if ( tickIncreased ) + { + uint32_t waitTimeMs = 0; + engine.evaluateConditions( timestamp ); + while ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData != nullptr ) + { + dataCollected++; + } + } + } + std::cout << "\nSimulated " << TIME_TO_SIMULATE_IN_MS / 1000 << " seconds with " << SIGNALS_PER_MS + << " signals arriving per millisecond. Avg " + << ( static_cast( dataCollected ) ) / ( static_cast( TIME_TO_SIMULATE_IN_MS / 1000 ) ) + << " collected data every second" << std::endl; +} + +TEST_F( CollectionInspectionEngineDoubleTest, NoCollectionSchemes ) +{ + CollectionInspectionEngine engine; + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, CollectStringSignal ) +{ + CollectionInspectionEngine engine; + TimePoint timestamp = { 160000000, 100 }; + uint32_t waitTimeMs = 0; + std::string stringData = "1BDD00"; + InspectionMatrixSignalCollectionInfo s1{}; + CollectedSignal stringSignal; + RawData::SignalUpdateConfig signalUpdateConfig1; + RawData::SignalBufferOverrides signalOverrides1; + std::vector overridesPerSignal; + std::unordered_map updatedSignals; + + s1.signalID = 101; + s1.sampleBufferSize = 10; + s1.minimumSampleIntervalMs = 100; + s1.fixedWindowPeriod = 1000; + s1.signalType = SignalType::STRING; + collectionSchemes->conditions[0].signals.push_back( s1 ); + // Condition is static be default + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + stringSignal.signalID = 101; + stringSignal.value.type = SignalType::STRING; + stringSignal.receiveTime = timestamp.systemTimeMs; + + signalUpdateConfig1.typeId = stringSignal.signalID; + signalUpdateConfig1.interfaceId = "interface1"; + signalUpdateConfig1.messageId = "VEHICLE.DTC_INFO"; + + signalOverrides1.interfaceId = signalUpdateConfig1.interfaceId; + signalOverrides1.messageId = signalUpdateConfig1.messageId; + signalOverrides1.maxNumOfSamples = 20; + signalOverrides1.maxBytesPerSample = 5_MiB; + signalOverrides1.reservedBytes = 5_MiB; + signalOverrides1.maxBytes = 100_MiB; + + overridesPerSignal = { signalOverrides1 }; + + boost::optional rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::make_optional( (size_t)20 ), boost::none, boost::none, overridesPerSignal ); + + updatedSignals = { { signalUpdateConfig1.typeId, signalUpdateConfig1 } }; + + std::shared_ptr rawDataBufferManager = + std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + engine.setRawDataBufferManager( rawDataBufferManager ); + + auto handle = rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal.signalID ); + + stringSignal.value.value.uint32Val = static_cast( handle ); + engine.addNewSignal( stringSignal.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp, + timestamp.monotonicTimeMs, + stringSignal.value.value.uint32Val ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + + auto collectedData = engine.collectNextDataToSend( timestamp, waitTimeMs ); + + ASSERT_NE( collectedData.triggeredCollectionSchemeData, nullptr ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 1 ); + EXPECT_EQ( collectedData.triggeredCollectionSchemeData->signals[0].signalID, stringSignal.signalID ); + + auto loanedRawDataFrame = rawDataBufferManager->borrowFrame( + stringSignal.signalID, + static_cast( + collectedData.triggeredCollectionSchemeData->signals[0].value.value.uint32Val ) ); + auto data = loanedRawDataFrame.getData(); + + EXPECT_TRUE( 0 == std::memcmp( data, stringData.c_str(), stringData.length() ) ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TriggerOnStringSignal ) +{ + CollectionInspectionEngine engine; + TimePoint timestamp = { 160000000, 100 }; + uint32_t waitTimeMs = 0; + std::string stringData = "1BDD00"; + + std::vector overridesPerSignal; + std::unordered_map updatedSignals; + + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 101; + s1.sampleBufferSize = 10; + s1.minimumSampleIntervalMs = 100; + s1.fixedWindowPeriod = 1000; + s1.signalType = SignalType::STRING; + + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 102; + s2.sampleBufferSize = 10; + s2.minimumSampleIntervalMs = 100; + s2.fixedWindowPeriod = 1000; + s2.signalType = SignalType::STRING; + + collectionSchemes->conditions[0].signals.push_back( s1 ); + collectionSchemes->conditions[0].signals.push_back( s2 ); + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getEqualCondition( s1.signalID, s2.signalID ).get(); + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + CollectedSignal stringSignal1; + stringSignal1.signalID = 101; + stringSignal1.value.type = SignalType::STRING; + stringSignal1.receiveTime = timestamp.systemTimeMs; + + CollectedSignal stringSignal2; + stringSignal2.signalID = 102; + stringSignal2.value.type = SignalType::STRING; + stringSignal2.receiveTime = timestamp.systemTimeMs; + + RawData::SignalUpdateConfig signalUpdateConfig1; + signalUpdateConfig1.typeId = stringSignal1.signalID; + signalUpdateConfig1.interfaceId = "interface1"; + signalUpdateConfig1.messageId = "VEHICLE.DTC_INFO_1"; + + RawData::SignalUpdateConfig signalUpdateConfig2; + signalUpdateConfig2.typeId = stringSignal2.signalID; + signalUpdateConfig2.interfaceId = "interface1"; + signalUpdateConfig2.messageId = "VEHICLE.DTC_INFO_2"; + + RawData::SignalBufferOverrides signalOverrides1; + signalOverrides1.interfaceId = signalUpdateConfig1.interfaceId; + signalOverrides1.messageId = signalUpdateConfig1.messageId; + signalOverrides1.maxNumOfSamples = 20; + signalOverrides1.maxBytesPerSample = 5_MiB; + signalOverrides1.reservedBytes = 5_MiB; + signalOverrides1.maxBytes = 100_MiB; + + RawData::SignalBufferOverrides signalOverrides2; + signalOverrides2.interfaceId = signalUpdateConfig2.interfaceId; + signalOverrides2.messageId = signalUpdateConfig2.messageId; + signalOverrides2.maxNumOfSamples = 20; + signalOverrides2.maxBytesPerSample = 5_MiB; + signalOverrides2.reservedBytes = 5_MiB; + signalOverrides2.maxBytes = 100_MiB; + + overridesPerSignal = { signalOverrides1, signalOverrides2 }; + + boost::optional rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::make_optional( (size_t)20 ), boost::none, boost::none, overridesPerSignal ); + + updatedSignals = { { signalUpdateConfig1.typeId, signalUpdateConfig1 }, + { signalUpdateConfig2.typeId, signalUpdateConfig2 } }; + + std::shared_ptr rawDataBufferManager = + std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + engine.setRawDataBufferManager( rawDataBufferManager ); + + auto handle1 = rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal1.signalID ); + stringSignal1.value.value.uint32Val = static_cast( handle1 ); + engine.addNewSignal( stringSignal1.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp, + timestamp.monotonicTimeMs, + stringSignal1.value.value.uint32Val ); + + // Second signal is still not available for the inspection + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + + auto handle2 = rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal2.signalID ); + stringSignal2.value.value.uint32Val = static_cast( handle2 ); + engine.addNewSignal( stringSignal2.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp + 10, + timestamp.monotonicTimeMs + 10, + stringSignal2.value.value.uint32Val ); + + // Now condition should trigger + ASSERT_TRUE( engine.evaluateConditions( timestamp + 20 ) ); + + auto collectedData = engine.collectNextDataToSend( timestamp + 20, waitTimeMs ); + + ASSERT_NE( collectedData.triggeredCollectionSchemeData, nullptr ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 2 ); + + auto loanedRawDataFrame = rawDataBufferManager->borrowFrame( + stringSignal1.signalID, + static_cast( + collectedData.triggeredCollectionSchemeData->signals[0].value.value.uint32Val ) ); + auto data = loanedRawDataFrame.getData(); + + EXPECT_EQ( collectedData.triggeredCollectionSchemeData->signals[0].signalID, stringSignal1.signalID ); + EXPECT_TRUE( 0 == std::memcmp( data, stringData.c_str(), stringData.length() ) ); + EXPECT_EQ( collectedData.triggeredCollectionSchemeData->signals[1].signalID, stringSignal2.signalID ); + EXPECT_TRUE( 0 == std::memcmp( data, stringData.c_str(), stringData.length() ) ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, TriggerOnStringSignalBufferHandleDeleted ) +{ + CollectionInspectionEngine engine; + TimePoint timestamp = { 160000000, 100 }; + uint32_t waitTimeMs = 0; + std::string stringData = "1BDD00"; + + std::vector overridesPerSignal; + std::unordered_map updatedSignals; + + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 101; + s1.sampleBufferSize = 10; + s1.minimumSampleIntervalMs = 100; + s1.fixedWindowPeriod = 1000; + s1.signalType = SignalType::STRING; - // function is: PREV_LAST_FIXED_WINDOW_AVG(SignalID(1234)) > -50.0 + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 102; + s2.sampleBufferSize = 10; + s2.minimumSampleIntervalMs = 100; + s2.fixedWindowPeriod = 1000; + s2.signalType = SignalType::STRING; + + collectionSchemes->conditions[0].signals.push_back( s1 ); + collectionSchemes->conditions[0].signals.push_back( s2 ); // Condition contains signals collectionSchemes->conditions[0].isStaticCondition = false; - collectionSchemes->conditions[0].condition = getPrevLastAvgWindowBiggerCondition( s1.signalID, -50.0 ).get(); - - TimePoint timestamp = { 160000000, 100 }; + collectionSchemes->conditions[0].condition = getEqualCondition( s1.signalID, s2.signalID ).get(); engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - uint32_t waitTimeMs = 0; - // Fill the prev last window with 0.0 - for ( uint32_t i = 0; i < s1.fixedWindowPeriod; i++ ) - { - timestamp++; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.0 ); - ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - } - // No samples arrive for two window2: - timestamp += s1.fixedWindowPeriod * 2; - // One more arrives, prev last average is still 0 which is > -50 - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -100.0 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); - ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + CollectedSignal stringSignal1; + stringSignal1.signalID = 101; + stringSignal1.value.type = SignalType::STRING; + stringSignal1.receiveTime = timestamp.systemTimeMs; + + CollectedSignal stringSignal2; + stringSignal2.signalID = 102; + stringSignal2.value.type = SignalType::STRING; + stringSignal2.receiveTime = timestamp.systemTimeMs; + + RawData::SignalUpdateConfig signalUpdateConfig1; + signalUpdateConfig1.typeId = stringSignal1.signalID; + signalUpdateConfig1.interfaceId = "interface1"; + signalUpdateConfig1.messageId = "VEHICLE.DTC_INFO_1"; + + RawData::SignalUpdateConfig signalUpdateConfig2; + signalUpdateConfig2.typeId = stringSignal2.signalID; + signalUpdateConfig2.interfaceId = "interface1"; + signalUpdateConfig2.messageId = "VEHICLE.DTC_INFO_2"; + + RawData::SignalBufferOverrides signalOverrides1; + signalOverrides1.interfaceId = signalUpdateConfig1.interfaceId; + signalOverrides1.messageId = signalUpdateConfig1.messageId; + // Force deletion of data + signalOverrides1.maxNumOfSamples = 1; + signalOverrides1.maxBytesPerSample = 5_MiB; + signalOverrides1.reservedBytes = 5_MiB; + signalOverrides1.maxBytes = 100_MiB; + + RawData::SignalBufferOverrides signalOverrides2; + signalOverrides2.interfaceId = signalUpdateConfig2.interfaceId; + signalOverrides2.messageId = signalUpdateConfig2.messageId; + signalOverrides2.maxNumOfSamples = 20; + signalOverrides2.maxBytesPerSample = 5_MiB; + signalOverrides2.reservedBytes = 5_MiB; + signalOverrides2.maxBytes = 100_MiB; + + overridesPerSignal = { signalOverrides1, signalOverrides2 }; + + boost::optional rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::make_optional( (size_t)20 ), boost::none, boost::none, overridesPerSignal ); + + updatedSignals = { { signalUpdateConfig1.typeId, signalUpdateConfig1 }, + { signalUpdateConfig2.typeId, signalUpdateConfig2 } }; + + std::shared_ptr rawDataBufferManager = + std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + engine.setRawDataBufferManager( rawDataBufferManager ); + + auto handle1 = rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal1.signalID ); + stringSignal1.value.value.uint32Val = static_cast( handle1 ); + engine.addNewSignal( stringSignal1.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp, + timestamp.monotonicTimeMs, + stringSignal1.value.value.uint32Val ); + + auto handle2 = rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal2.signalID ); + stringSignal2.value.value.uint32Val = static_cast( handle2 ); + engine.addNewSignal( stringSignal2.signalID, + DEFAULT_FETCH_REQUEST_ID, + timestamp + 10, + timestamp.monotonicTimeMs + 10, + stringSignal2.value.value.uint32Val ); + + // This should force handle1 data to be deleted before new handle is accessible by CIE + rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal1.signalID ); + // Buffer handle was deleted + ASSERT_FALSE( engine.evaluateConditions( timestamp + 20 ) ); + + auto collectedData = engine.collectNextDataToSend( timestamp + 20, waitTimeMs ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData, nullptr ); } -TEST_F( CollectionInspectionEngineDoubleTest, MultiWindowCondition ) +TEST_F( CollectionInspectionEngineDoubleTest, MultipleFetchRequests ) { + // Collection Scheme 1 collects signal with two different fetch request IDs + // They should be collected in the same signal buffer CollectionInspectionEngine engine; - // fixedWindowPeriod means that we collect data over 300 seconds InspectionMatrixSignalCollectionInfo s1{}; s1.signalID = 1234; s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 100; + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; + s1.fetchRequestIDs = { 1, 3 }; s1.signalType = SignalType::DOUBLE; addSignalToCollect( collectionSchemes->conditions[0], s1 ); + collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); - // function is: (((LAST_FIXED_WINDOW_MAX(SignalID(1234)) - PREV_LAST_FIXED_WINDOW_MAX(SignalID(1234))) - // + PREV_LAST_FIXED_WINDOW_MAX(SignalID(1234))) - // < (LAST_FIXED_WINDOW_MAX(SignalID(1234)) * PREV_LAST_FIXED_WINDOW_MAX(SignalID(1234)))) - // || (LAST_FIXED_WINDOW_MIN(SignalID(1234)) == PREV_LAST_FIXED_WINDOW_MIN(SignalID(1234))) - // Condition contains signals - collectionSchemes->conditions[0].isStaticCondition = false; - collectionSchemes->conditions[0].condition = getMultiFixedWindowCondition( s1.signalID ).get(); + // Collection Scheme 2 collects the same signal with the same minimumSampleIntervalMs + // but the fetch request id is different + // This data should land in a different signal buffer + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = s1.signalID; + s2.sampleBufferSize = 50; + s2.minimumSampleIntervalMs = s1.minimumSampleIntervalMs; + s2.fixedWindowPeriod = 77777; + s2.fetchRequestIDs = { 2 }; + s2.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[1], s2 ); + // Condition is static by default + collectionSchemes->conditions[1].condition = getAlwaysTrueCondition().get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, -95.0 ); - engine.addNewSignal( s1.signalID, timestamp + 50, timestamp.monotonicTimeMs + 50, 100.0 ); - engine.addNewSignal( s1.signalID, timestamp + 70, timestamp.monotonicTimeMs + 70, 110.0 ); - // Condition still fulfilled but already triggered. After time still not over - ASSERT_FALSE( engine.evaluateConditions( timestamp + 70 ) ); + engine.addNewSignal( s1.signalID, 1, timestamp, timestamp.monotonicTimeMs, 10 ); + engine.addNewSignal( s2.signalID, 2, timestamp, timestamp.monotonicTimeMs, 20 ); + // Consider sample interval + engine.addNewSignal( s1.signalID, 3, timestamp + 10, timestamp.monotonicTimeMs + 10, 30 ); + + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; - ASSERT_EQ( engine.collectNextDataToSend( timestamp + 70, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + auto collectedData = engine.collectNextDataToSend( timestamp + 50, waitTimeMs ).triggeredCollectionSchemeData; + auto collectedData2 = engine.collectNextDataToSend( timestamp + 50, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_EQ( engine.collectNextDataToSend( timestamp + 50, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - engine.addNewSignal( s1.signalID, timestamp + 100, timestamp.monotonicTimeMs + 100, -205.0 ); - engine.addNewSignal( s1.signalID, timestamp + 150, timestamp.monotonicTimeMs + 150, -300.0 ); - engine.addNewSignal( s1.signalID, timestamp + 200, timestamp.monotonicTimeMs + 200, +30.0 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp + 200 ) ); - ASSERT_NE( engine.collectNextDataToSend( timestamp + 200, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + ASSERT_NE( collectedData, nullptr ); + ASSERT_NE( collectedData2, nullptr ); + + // First campaign should collect signal values from one buffer + // Second campaign should collect signal values from another buffer + ASSERT_EQ( collectedData->signals.size(), 2 ); + ASSERT_EQ( collectedData2->signals.size(), 1 ); + + ASSERT_EQ( collectedData->signals[0].value.value.doubleVal, 30 ); + ASSERT_EQ( collectedData->signals[1].value.value.doubleVal, 10 ); + ASSERT_EQ( collectedData2->signals[0].value.value.doubleVal, 20 ); } -TEST_F( CollectionInspectionEngineDoubleTest, TestNotEqualOperator ) +TEST_F( CollectionInspectionEngineDoubleTest, TriggerOnConditionWithCustomFetchLogic ) { + // Collection Scheme 1 collects signal without custom fetch logic CollectionInspectionEngine engine; InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 123; + s1.signalID = 1234; s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 100; + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysFalseCondition().get(); + + // Collection Scheme 2 collects same signal with custom fetch logic + // and evaluates on it InspectionMatrixSignalCollectionInfo s2{}; - s2.signalID = 456; + s2.signalID = 1234; s2.sampleBufferSize = 50; - s2.minimumSampleIntervalMs = 0; - s2.fixedWindowPeriod = 100; + s2.minimumSampleIntervalMs = 10; + s2.fixedWindowPeriod = 77777; + s2.fetchRequestIDs = { 1 }; s2.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); - addSignalToCollect( collectionSchemes->conditions[0], s2 ); + addSignalToCollect( collectionSchemes->conditions[1], s2 ); - // function is: !(!(SignalID(123) <= 0.001) && !((SignalID(123) / SignalID(456)) >= 0.5)) - // Condition contains signals - collectionSchemes->conditions[0].isStaticCondition = false; - collectionSchemes->conditions[0].condition = getNotEqualCondition( s1.signalID, s2.signalID ).get(); + expressionNodes.push_back( std::make_shared() ); + auto equal = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto signal1 = expressionNodes.back(); + expressionNodes.push_back( std::make_shared() ); + auto value = expressionNodes.back(); + + signal1->nodeType = ExpressionNodeType::SIGNAL; + signal1->signalID = s2.signalID; + + value->nodeType = ExpressionNodeType::FLOAT; + value->floatingValue = 22.0; + + equal->nodeType = ExpressionNodeType::OPERATOR_EQUAL; + equal->left = signal1.get(); + equal->right = value.get(); + + // Condition contains signal + collectionSchemes->conditions[1].isStaticCondition = false; + collectionSchemes->conditions[1].condition = equal.get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - // Expression should be false because they are equal - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 100.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 100.0 ); - ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 22.0 ); + engine.addNewSignal( s2.signalID, 1, timestamp, timestamp.monotonicTimeMs, 25.0 ); + uint32_t waitTimeMs = 0; + // Should evaluate to false. Inspected signal value for campaign 2 is 25. + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - // Expression should be true: because they are not equal - timestamp++; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 50 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); - ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + engine.addNewSignal( s2.signalID, 1, timestamp + 10, timestamp.monotonicTimeMs + 10, 22.0 ); + // Should evaluate to true. Inspected signal value for campaign 2 is 22. + ASSERT_TRUE( engine.evaluateConditions( timestamp + 20 ) ); - // Expression should be false because they are equal again - timestamp++; - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 50.0 ); - ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + auto collectedData = engine.collectNextDataToSend( timestamp + 50, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_EQ( engine.collectNextDataToSend( timestamp + 60, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + ASSERT_NE( collectedData, nullptr ); + + ASSERT_EQ( collectedData->signals.size(), 2 ); + + ASSERT_EQ( collectedData->signals[0].value.value.doubleVal, 22.0 ); + ASSERT_EQ( collectedData->signals[1].value.value.doubleVal, 25.0 ); } -TEST_F( CollectionInspectionEngineDoubleTest, TestBoolToDoubleImplicitCast ) +TEST_F( CollectionInspectionEngineDoubleTest, TriggerOnConditionWithCustomFetchLogicIsNullFunction ) { + // Collection Scheme 1 collects signal without custom fetch logic CollectionInspectionEngine engine; InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 123; + s1.signalID = 1234; s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 100; + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; + s1.signalType = SignalType::DOUBLE; addSignalToCollect( collectionSchemes->conditions[0], s1 ); + // Condition is static by default + collectionSchemes->conditions[0].condition = getAlwaysFalseCondition().get(); - // expression is: True + 1.0 == 2.0 - expressionNodes.push_back( std::make_shared() ); - auto nodeLeft = expressionNodes.back(); - nodeLeft->nodeType = ExpressionNodeType::BOOLEAN; - nodeLeft->booleanValue = true; + // Collection Scheme 2 collects same signal with custom fetch logic + // and evaluates on it + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 1234; + s2.sampleBufferSize = 50; + s2.minimumSampleIntervalMs = 10; + s2.fixedWindowPeriod = 77777; + s2.fetchRequestIDs = { 1 }; + s2.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[1], s2 ); expressionNodes.push_back( std::make_shared() ); - auto nodeRight = expressionNodes.back(); - nodeRight->nodeType = ExpressionNodeType::FLOAT; - nodeRight->floatingValue = 1.0; - + auto logicalNot = expressionNodes.back(); expressionNodes.push_back( std::make_shared() ); - auto nodeOperation = expressionNodes.back(); - nodeOperation->nodeType = ExpressionNodeType::OPERATOR_ARITHMETIC_PLUS; - nodeOperation->left = nodeLeft.get(); - nodeOperation->right = nodeRight.get(); - + auto isNull = expressionNodes.back(); expressionNodes.push_back( std::make_shared() ); - auto nodeRight2 = expressionNodes.back(); - nodeRight2->nodeType = ExpressionNodeType::FLOAT; - nodeRight2->floatingValue = 2.0; + auto signal1 = expressionNodes.back(); - expressionNodes.push_back( std::make_shared() ); - auto nodeOperation2 = expressionNodes.back(); - nodeOperation2->nodeType = ExpressionNodeType::OPERATOR_EQUAL; - nodeOperation2->left = nodeOperation.get(); - nodeOperation2->right = nodeRight2.get(); - // Condition is static be default - collectionSchemes->conditions[0].condition = nodeOperation2.get(); + signal1->nodeType = ExpressionNodeType::SIGNAL; + signal1->signalID = s2.signalID; + + isNull->nodeType = ExpressionNodeType::IS_NULL_FUNCTION; + isNull->left = signal1.get(); + + logicalNot->nodeType = ExpressionNodeType::OPERATOR_LOGICAL_NOT; + logicalNot->left = isNull.get(); + + // Condition contains signal + collectionSchemes->conditions[1].isStaticCondition = false; + // Condition contains isNull function + collectionSchemes->conditions[1].alwaysEvaluateCondition = true; + collectionSchemes->conditions[1].condition = logicalNot.get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - // Expression should be true - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 100.0 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 22.0 ); + uint32_t waitTimeMs = 0; - ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + // Should evaluate to false. There is no signals for campaign 2; + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + engine.addNewSignal( s2.signalID, 1, timestamp + 10, timestamp.monotonicTimeMs + 10, 22.0 ); + // Should evaluate to true. There is a signal for campaign 2; + ASSERT_TRUE( engine.evaluateConditions( timestamp + 20 ) ); + + auto collectedData = engine.collectNextDataToSend( timestamp + 50, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_EQ( engine.collectNextDataToSend( timestamp + 60, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->signals.size(), 1 ); + ASSERT_EQ( collectedData->signals[0].value.value.doubleVal, 22.0 ); + + // Should evaluate to false. There is no unconsumed signal for campaign 2; + ASSERT_FALSE( engine.evaluateConditions( timestamp + 70 ) ); + engine.addNewSignal( s2.signalID, 1, timestamp + 80, timestamp.monotonicTimeMs + 10, 22.0 ); + // Should evaluate to true. There is an unconsumed signal for campaign 2; + ASSERT_TRUE( engine.evaluateConditions( timestamp + 90 ) ); } -TEST_F( CollectionInspectionEngineDoubleTest, TestDoubleToBoolImplicitCast ) +TEST_F( CollectionInspectionEngineDoubleTest, TriggerOnIsNullFunctionSignalMissing ) { CollectionInspectionEngine engine; InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 123; + s1.signalID = 1234; s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 100; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; + s1.fetchRequestIDs = { 1 }; + s1.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[1], s1 ); - // expression is: 42.0 && True expressionNodes.push_back( std::make_shared() ); - auto nodeLeft = expressionNodes.back(); - nodeLeft->nodeType = ExpressionNodeType::FLOAT; - nodeLeft->floatingValue = 42.0; - + auto isNull = expressionNodes.back(); expressionNodes.push_back( std::make_shared() ); - auto nodeRight = expressionNodes.back(); - nodeRight->nodeType = ExpressionNodeType::BOOLEAN; - nodeRight->booleanValue = true; + auto signal1 = expressionNodes.back(); - expressionNodes.push_back( std::make_shared() ); - auto nodeOperation = expressionNodes.back(); - nodeOperation->nodeType = ExpressionNodeType::OPERATOR_LOGICAL_AND; - nodeOperation->left = nodeLeft.get(); - nodeOperation->right = nodeRight.get(); - // Condition is static be default - collectionSchemes->conditions[0].condition = nodeOperation.get(); + signal1->nodeType = ExpressionNodeType::SIGNAL; + signal1->signalID = 1; // Non-existing signal + + isNull->nodeType = ExpressionNodeType::IS_NULL_FUNCTION; + isNull->left = signal1.get(); + + // Condition contains signal + collectionSchemes->conditions[1].isStaticCondition = false; + // Condition contains isNull function + collectionSchemes->conditions[1].alwaysEvaluateCondition = true; + collectionSchemes->conditions[1].condition = isNull.get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - // Expression should be true - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 100.0 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 22.0 ); + uint32_t waitTimeMs = 0; - ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + // Should evaluate to false. There is no signal to evaluate on; + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); } -TEST_F( CollectionInspectionEngineDoubleTest, TwoSignalsRatioCondition ) +TEST_F( CollectionInspectionEngineDoubleTest, TriggerOnIsNullFunctionNoSignal ) { CollectionInspectionEngine engine; InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 123; + s1.signalID = 1234; s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 100; + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; + s1.fetchRequestIDs = { 1 }; s1.signalType = SignalType::DOUBLE; - InspectionMatrixSignalCollectionInfo s2{}; - s2.signalID = 456; - s2.sampleBufferSize = 50; - s2.minimumSampleIntervalMs = 0; - s2.fixedWindowPeriod = 100; - s2.signalType = SignalType::DOUBLE; - addSignalToCollect( collectionSchemes->conditions[0], s1 ); - addSignalToCollect( collectionSchemes->conditions[0], s2 ); + addSignalToCollect( collectionSchemes->conditions[1], s1 ); - // function is: !(!(SignalID(123) <= 0.001) && !((SignalID(123) / SignalID(456)) >= 0.5)) - // Condition contains signals - collectionSchemes->conditions[0].isStaticCondition = false; - collectionSchemes->conditions[0].condition = - getTwoSignalsRatioCondition( s1.signalID, 0.001, s2.signalID, 0.5 ).get(); + expressionNodes.push_back( std::make_shared() ); + auto isNull = expressionNodes.back(); + isNull->nodeType = ExpressionNodeType::IS_NULL_FUNCTION; + + // Condition contains signal + collectionSchemes->conditions[1].isStaticCondition = false; + // Condition contains isNull function + collectionSchemes->conditions[1].alwaysEvaluateCondition = true; + collectionSchemes->conditions[1].condition = isNull.get(); TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - // Expression should be false: (1.0 <= 0.001) || (1.0 / 100.0) >= 0.5) - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 1.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 100.0 ); - ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - uint32_t waitTimeMs = 0; - ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - - // Expression should be true: (0.001 <= 0.001) || (0.001 / 100.0) >= 0.5) - timestamp++; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 0.001 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); - ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 22.0 ); - // Expression should be false: (1.0 <= 0.001) || (1.0 / 100.0) >= 0.5) - timestamp++; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 1.0 ); + uint32_t waitTimeMs = 0; + // Should evaluate to false. There is no signal to evaluate on; ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - - // Expression should be true: (50.0 <= 0.001) || (50.0 / 100.0) >= 0.5) - timestamp++; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 50.0 ); - ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); - ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); } -TEST_F( CollectionInspectionEngineDoubleTest, UnknownExpressionNode ) +TEST_F( CollectionInspectionEngineDoubleTest, FetchConfigOnRisingEdgeTest ) { CollectionInspectionEngine engine; InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = 123; + s1.signalID = 1234; s1.sampleBufferSize = 50; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 100; + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; + s1.fetchRequestIDs = { 1 }; s1.signalType = SignalType::DOUBLE; addSignalToCollect( collectionSchemes->conditions[0], s1 ); - // function is: (SignalID(123) <= Unknown) - // Condition is not static + // Every 10 seconds send data out + collectionSchemes->conditions[0].minimumPublishIntervalMs = 10000; + collectionSchemes->conditions[0].triggerOnlyOnRisingEdge = true; + // Condition contains signals collectionSchemes->conditions[0].isStaticCondition = false; - collectionSchemes->conditions[0].condition = getUnknownCondition( s1.signalID ).get(); + collectionSchemes->conditions[0].condition = getOneSignalBiggerCondition( s1.signalID, 0.0 ).get(); + + ConditionForFetch fetchCondition1{}; + fetchCondition1.condition = getOneSignalBiggerCondition( s1.signalID, -100.0 ).get(); + fetchCondition1.fetchRequestID = 1; + fetchCondition1.triggerOnlyOnRisingEdge = true; + collectionSchemes->conditions[0].fetchConditions = { fetchCondition1 }; TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - // Expression should be false: (1.0 <= Unknown) - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 1.0 ); + // None of the condition should evaluate to true ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + + // Test fetch condition trigger but no campaign condition + engine.addNewSignal( s1.signalID, 1, timestamp + 10000, timestamp.monotonicTimeMs + 10000, -50.0 ); + + // Fetch condition should evaluate to true, upload condition to false + // New data will be eventually pushed to the signal buffer + ASSERT_TRUE( engine.evaluateConditions( timestamp + 10000 ) ); + uint32_t waitTimeMs = 0; - ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + EXPECT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + ; + + // Simulate fetch logic + engine.addNewSignal( s1.signalID, 1, timestamp + 11000, timestamp.monotonicTimeMs + 11000, 11 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp + 11000 ) ); + + // Triggers after 10s + EXPECT_NE( engine.collectNextDataToSend( timestamp + 11000, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // None of the condition should evaluate to true due to rising edge setup + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); } -TEST_F( CollectionInspectionEngineDoubleTest, RequestTooMuchMemorySignals ) +TEST_F( CollectionInspectionEngineDoubleTest, FetchConfigTriggerOnDifferentSignalTest ) { CollectionInspectionEngine engine; - // for each of the .sampleBufferSize=1000000 multiple bytes have to be allocated InspectionMatrixSignalCollectionInfo s1{}; s1.signalID = 1234; - s1.sampleBufferSize = 1000000; - s1.minimumSampleIntervalMs = 0; - s1.fixedWindowPeriod = 300000; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 10; + s1.fixedWindowPeriod = 77777; + s1.fetchRequestIDs = { 1 }; s1.signalType = SignalType::DOUBLE; + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 456; + s2.sampleBufferSize = 50; + s2.minimumSampleIntervalMs = 0; + s2.fixedWindowPeriod = 100; + s2.isConditionOnlySignal = true; + s2.signalType = SignalType::DOUBLE; + InspectionMatrixSignalCollectionInfo s3{}; + s3.signalID = 789; + s3.sampleBufferSize = 50; + s3.minimumSampleIntervalMs = 0; + s3.fixedWindowPeriod = 100; + s3.isConditionOnlySignal = true; addSignalToCollect( collectionSchemes->conditions[0], s1 ); - // Condition is static by default - collectionSchemes->conditions[1].condition = getAlwaysTrueCondition().get(); - TimePoint timestamp = { 0, 0 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); -} - -TEST_F( CollectionInspectionEngineDoubleTest, RequestTooMuchMemoryFrames ) -{ - CollectionInspectionEngine engine; - InspectionMatrixCanFrameCollectionInfo c1; - c1.frameID = 0x380; - c1.channelID = 3; - c1.sampleBufferSize = 1000000; - c1.minimumSampleIntervalMs = 0; - collectionSchemes->conditions[0].canFrames.push_back( c1 ); - // Condition is static by default - collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); - TimePoint timestamp = { 0, 0 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); -} - -/* - * This test is also used for performance analysis. The add new signal takes > 100ns - * The performance varies if number of signal, number of conditions is changed. - */ -TEST_F( CollectionInspectionEngineDoubleTest, RandomDataTest ) -{ - const int NUMBER_OF_COLLECTION_SCHEMES = 200; - const int NUMBER_OF_SIGNALS = 20000; - - // Keep the seed static so multiple runs are comparable - std::default_random_engine eng{ static_cast( 1620336094 ) }; - std::mt19937 gen{ eng() }; - std::uniform_int_distribution<> uniformDistribution( 0, NUMBER_OF_COLLECTION_SCHEMES - 1 ); - std::normal_distribution<> normalDistribution{ 10, 4 }; + addSignalToCollect( collectionSchemes->conditions[0], s2 ); + addSignalToCollect( collectionSchemes->conditions[0], s3 ); - for ( int i = 0; i < NUMBER_OF_COLLECTION_SCHEMES; i++ ) - { + collectionSchemes->conditions[0].minimumPublishIntervalMs = 100; + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getOneSignalBiggerCondition( s2.signalID, 20.0 ).get(); - ConditionWithCollectedData collectionScheme; - collectionSchemes->conditions.resize( NUMBER_OF_COLLECTION_SCHEMES ); - // Condition is static by default - collectionSchemes->conditions[i].condition = getAlwaysTrueCondition().get(); - } + ConditionForFetch fetchCondition1{}; + fetchCondition1.condition = getNotEqualCondition( s2.signalID, s3.signalID ).get(); + fetchCondition1.fetchRequestID = 1; + fetchCondition1.triggerOnlyOnRisingEdge = false; + collectionSchemes->conditions[0].fetchConditions = { fetchCondition1 }; - CollectionInspectionEngine engine; - // for each of the .sampleBufferSize=1000000 multiple bytes have to be allocated - std::normal_distribution<> fixedWindowSizeGenerator( 300000, 100000 ); - int minWindow = std::numeric_limits::max(); - int maxWindow = std::numeric_limits::min(); - const int MINIMUM_WINDOW_SIZE = 500000; // should be 500000 - int withoutWindow = 0; - for ( int i = 0; i < NUMBER_OF_SIGNALS; i++ ) - { + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - int windowSize = static_cast( fixedWindowSizeGenerator( gen ) ); - if ( windowSize < MINIMUM_WINDOW_SIZE ) - { // most signals dont have a window - windowSize = 0; - withoutWindow++; - } - else - { - minWindow = std::min( windowSize, minWindow ); - maxWindow = std::max( windowSize, maxWindow ); - } - InspectionMatrixSignalCollectionInfo s1{}; - s1.signalID = i; - s1.sampleBufferSize = 10; - s1.minimumSampleIntervalMs = 1; - s1.fixedWindowPeriod = windowSize; - s1.signalType = SignalType::DOUBLE; - for ( int j = 0; j < normalDistribution( gen ); j++ ) - { - auto &collectionScheme = collectionSchemes->conditions[uniformDistribution( gen )]; - collectionScheme.signals.push_back( s1 ); + // Fetch and normal condition should not trigger + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); - // exactly two signals are added now so add them to condition: - if ( collectionScheme.signals.size() == 2 ) - { - // Condition contains signals - collectionScheme.isStaticCondition = false; - if ( uniformDistribution( gen ) < NUMBER_OF_COLLECTION_SCHEMES / 2 ) - { - collectionScheme.condition = getLastAvgWindowBiggerCondition( s1.signalID, 15 ).get(); - } - else - { - collectionScheme.condition = - getTwoSignalsBiggerCondition( s1.signalID, 17.5, collectionScheme.signals[0].signalID, 17.5 ) - .get(); - } - } - } - } - std::cout << "\nFinished generation " << NUMBER_OF_SIGNALS << " signals and " - << ( NUMBER_OF_SIGNALS - withoutWindow ) - << " of them with window with a window sampling size varies from " << minWindow << " to " << maxWindow - << std::endl; + engine.addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 100, timestamp.monotonicTimeMs + 100, 100 ); + // Now condition evaluation would evaluate to true but no signal was yet fetched + ASSERT_TRUE( engine.evaluateConditions( timestamp + 150 ) ); + uint32_t waitTimeMs = 0; + // Expect a non-null but an empty object + auto collectedData = engine.collectNextDataToSend( timestamp + 150, waitTimeMs ).triggeredCollectionSchemeData; + ASSERT_NE( collectedData, nullptr ); + ASSERT_EQ( collectedData->signals.size(), 0 ); - TimePoint START_TIMESTAMP = { 160000000, 100 }; - TimePoint timestamp = START_TIMESTAMP; + engine.addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 200, timestamp.monotonicTimeMs + 200, 11 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 200, timestamp.monotonicTimeMs + 200, 11 ); + // Fetch condition should still evaluate to false and campaign condition is false now too + ASSERT_FALSE( engine.evaluateConditions( timestamp + 250 ) ); + EXPECT_EQ( engine.collectNextDataToSend( timestamp + 250, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); - auto originalLogLevel = Aws::IoTFleetWise::gSystemWideLogLevel; - try - { - // Temporarily change the log level since we have too many signals, which would make the test - // output too noisy with Trace level. - Aws::IoTFleetWise::gSystemWideLogLevel = Aws::IoTFleetWise::LogLevel::Info; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - } - catch ( ... ) - { - Aws::IoTFleetWise::gSystemWideLogLevel = originalLogLevel; - throw; - } - Aws::IoTFleetWise::gSystemWideLogLevel = originalLogLevel; + engine.addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 300, timestamp.monotonicTimeMs + 300, 22 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 300, timestamp.monotonicTimeMs + 300, 33 ); + // Fetch condition should evaluate to true + ASSERT_TRUE( engine.evaluateConditions( timestamp + 350 ) ); - const int TIME_TO_SIMULATE_IN_MS = 5000; - const int SIGNALS_PER_MS = 20; - uint32_t counter = 0; - uint32_t dataCollected = 0; + // Simulate fetch logic + engine.addNewSignal( s1.signalID, 1, timestamp + 10000, timestamp.monotonicTimeMs + 10000, 11 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp + 10000 ) ); - std::default_random_engine eng2{ static_cast( 1620337567 ) }; - std::mt19937 gen2{ eng() }; - std::uniform_int_distribution<> signalIDGenerator( 0, NUMBER_OF_SIGNALS - 1 ); - for ( int i = 0; i < TIME_TO_SIMULATE_IN_MS * SIGNALS_PER_MS; i++ ) - { - if ( i % ( ( TIME_TO_SIMULATE_IN_MS * SIGNALS_PER_MS ) / 100 ) == 0 ) - { - std::cout << "." << std::flush; - } - counter++; - bool tickIncreased = false; - if ( counter == SIGNALS_PER_MS ) - { - timestamp++; - counter = 0; - tickIncreased = true; - } - engine.addNewSignal( - signalIDGenerator( gen2 ), timestamp, timestamp.monotonicTimeMs, normalDistribution( gen ) ); - if ( tickIncreased ) - { - uint32_t waitTimeMs = 0; - engine.evaluateConditions( timestamp ); - while ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData != nullptr ) - { - dataCollected++; - } - } - } - std::cout << "\nSimulated " << TIME_TO_SIMULATE_IN_MS / 1000 << " seconds with " << SIGNALS_PER_MS - << " signals arriving per millisecond. Avg " - << ( static_cast( dataCollected ) ) / ( static_cast( TIME_TO_SIMULATE_IN_MS / 1000 ) ) - << " collected data every second" << std::endl; -} + // Triggers after 10s + EXPECT_NE( engine.collectNextDataToSend( timestamp + 10000, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); -TEST_F( CollectionInspectionEngineDoubleTest, NoCollectionSchemes ) -{ - CollectionInspectionEngine engine; - TimePoint timestamp = { 160000000, 100 }; - engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + engine.addNewSignal( + s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 10100, timestamp.monotonicTimeMs + 10100, 33 ); + engine.addNewSignal( + s3.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp + 10100, timestamp.monotonicTimeMs + 10100, 44 ); + // Fetch condition should evaluate to true again + ASSERT_TRUE( engine.evaluateConditions( timestamp + 10100 ) ); } // Test to assert that a signal buffer is not allocated for a signal not known to DM @@ -2179,11 +3987,10 @@ TEST_F( CollectionInspectionEngineDoubleTest, UnknownSignalNoSignalBuffer ) // Condition is static by default collectionSchemes->conditions[0].condition = getAlwaysTrueCondition().get(); - collectionSchemes->conditions[0].metadata.collectionSchemeID = "Test Campaign"; TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 100.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 100.0 ); ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); // Condition will evaluate to true but no data will be collected uint32_t waitTimeMs = 0; @@ -2212,24 +4019,128 @@ TEST_F( CollectionInspectionEngineDoubleTest, UnknownSignalInExpression ) collectionSchemes->conditions[0].isStaticCondition = false; collectionSchemes->conditions[0].condition = getNotEqualCondition( s1.signalID, s2.signalID ).get(); collectionSchemes->conditions[0].minimumPublishIntervalMs = 5000; - collectionSchemes->conditions[0].metadata.collectionSchemeID = "Test Campaign"; TimePoint timestamp = { 160000000, 100 }; engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); // Condition evaluates to false - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 90.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 90.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 90.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 90.0 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); uint32_t waitTimeMs = 0; ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); // Condition still evaluates to false after time increment timestamp += 5000; - engine.addNewSignal( s1.signalID, timestamp, timestamp.monotonicTimeMs, 110.0 ); - engine.addNewSignal( s2.signalID, timestamp, timestamp.monotonicTimeMs, 110.0 ); + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 110.0 ); + engine.addNewSignal( s2.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 110.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); +} + +TEST_F( CollectionInspectionEngineDoubleTest, CustomFunction ) +{ + CollectionInspectionEngine engine; + InspectionMatrixSignalCollectionInfo s1{}; + s1.signalID = 1234; + s1.sampleBufferSize = 50; + s1.minimumSampleIntervalMs = 0; + s1.fixedWindowPeriod = 100; + s1.signalType = SignalType::DOUBLE; + InspectionMatrixSignalCollectionInfo s2{}; + s2.signalID = 5678; + s2.sampleBufferSize = 50; + s2.minimumSampleIntervalMs = 0; + s2.fixedWindowPeriod = 100; + s2.signalType = SignalType::DOUBLE; + addSignalToCollect( collectionSchemes->conditions[0], s1 ); + addSignalToCollect( collectionSchemes->conditions[0], s2 ); + addSignalToCollect( collectionSchemes->conditions[1], s1 ); + addSignalToCollect( collectionSchemes->conditions[1], s2 ); + + // function is: custom_function(name, signal) + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getCustomFunctionCondition( s1.signalID, "ABC" ).get(); + collectionSchemes->conditions[0].alwaysEvaluateCondition = true; + // Not implemented function: + collectionSchemes->conditions[1].isStaticCondition = false; + collectionSchemes->conditions[1].condition = getCustomFunctionCondition( s1.signalID, "DEF" ).get(); + collectionSchemes->conditions[1].alwaysEvaluateCondition = true; + MockFunction & )> invoke; + EXPECT_CALL( invoke, Call( _, _ ) ) + .Times( 4 ) + .WillOnce( Invoke( []( CustomFunctionInvocationID invocationId, + const std::vector &args ) -> CustomFunctionInvokeResult { + EXPECT_EQ( invocationId, 0 ); + EXPECT_EQ( args.size(), 1 ); + EXPECT_TRUE( args[0].isUndefined() ); + return ExpressionErrorCode::SUCCESSFUL; + } ) ) + .WillRepeatedly( Invoke( [s1]( CustomFunctionInvocationID invocationId, + const std::vector &args ) -> CustomFunctionInvokeResult { + EXPECT_EQ( invocationId, 0 ); + EXPECT_EQ( args.size(), 1 ); + EXPECT_TRUE( args[0].isBoolOrDouble() ); + EXPECT_EQ( args[0].signalID, s1.signalID ); + if ( args[0].asDouble() >= 300.0 ) + { + return ExpressionErrorCode::TYPE_MISMATCH; + } + return { ExpressionErrorCode::SUCCESSFUL, args[0].asDouble() > 200.0 }; + } ) ); + MockFunction &, Timestamp, CollectionInspectionEngineOutput & )> + conditionEnd; + EXPECT_CALL( conditionEnd, Call( _, _, _ ) ) + .Times( 8 ) + .WillRepeatedly( Invoke( [s1, s2]( const std::unordered_set &collectedSignalIds, + Timestamp timestamp, + CollectionInspectionEngineOutput &collectedData ) { + EXPECT_EQ( collectedSignalIds.size(), 2 ); + EXPECT_NE( collectedSignalIds.find( s1.signalID ), collectedSignalIds.end() ); + EXPECT_NE( collectedSignalIds.find( s2.signalID ), collectedSignalIds.end() ); + if ( !collectedData.triggeredCollectionSchemeData ) + { + return; + } + collectedData.triggeredCollectionSchemeData->signals.push_back( + CollectedSignal{ s2.signalID, timestamp, 7890.0, SignalType::DOUBLE } ); + } ) ); + MockFunction cleanup; + EXPECT_CALL( cleanup, Call( _ ) ).Times( 1 ); + engine.registerCustomFunction( + "ABC", + CustomFunctionCallbacks{ invoke.AsStdFunction(), conditionEnd.AsStdFunction(), cleanup.AsStdFunction() } ); + + TimePoint timestamp = { 160000000, 100 }; + engine.onChangeInspectionMatrix( consCollectionSchemes, timestamp ); + + // No signal data yet available: + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + uint32_t waitTimeMs = 0; + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // First value, not triggered: + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 123.0 ); + ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); + ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Trigger: + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 250.0 ); + ASSERT_TRUE( engine.evaluateConditions( timestamp ) ); + ASSERT_NE( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Out of range: + timestamp++; + engine.addNewSignal( s1.signalID, DEFAULT_FETCH_REQUEST_ID, timestamp, timestamp.monotonicTimeMs, 500.0 ); ASSERT_FALSE( engine.evaluateConditions( timestamp ) ); ASSERT_EQ( engine.collectNextDataToSend( timestamp, waitTimeMs ).triggeredCollectionSchemeData, nullptr ); + + // Cleanup + timestamp++; + engine.onChangeInspectionMatrix( std::make_shared(), timestamp ); } + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/CollectionInspectionWorkerThreadTest.cpp b/test/unit/CollectionInspectionWorkerThreadTest.cpp index b9c2b937..c0f549d6 100644 --- a/test/unit/CollectionInspectionWorkerThreadTest.cpp +++ b/test/unit/CollectionInspectionWorkerThreadTest.cpp @@ -23,6 +23,18 @@ #include #include +#ifdef FWE_FEATURE_STORE_AND_FORWARD +#include "CANInterfaceIDTranslator.h" +#include "DataSenderProtoWriter.h" +#include "RateLimiter.h" +#include "StreamForwarder.h" +#include "StreamManager.h" +#include "StreamManagerMock.h" +#include +#include +#include +#endif + namespace Aws { namespace IoTFleetWise @@ -37,10 +49,25 @@ class CollectionInspectionWorkerThreadTest : public ::testing::Test SignalBufferPtr signalBuffer; std::shared_ptr mClock = ClockHandler::getClock(); std::shared_ptr outputCollectedData; +#ifdef FWE_FEATURE_STORE_AND_FORWARD + std::shared_ptr streamForwarder; + std::shared_ptr<::testing::StrictMock> streamManager; + std::shared_ptr rateLimiter; +#endif + void initAndStartWorker( CollectionInspectionWorkerThread &worker ) { - bool res = worker.init( signalBuffer, outputCollectedData, 1000, nullptr ); + bool res = worker.init( signalBuffer, + outputCollectedData, + 1000, + nullptr +#ifdef FWE_FEATURE_STORE_AND_FORWARD + , + streamForwarder, + streamManager +#endif + ); ASSERT_TRUE( res ); ASSERT_TRUE( worker.start() ); } @@ -108,6 +135,20 @@ class CollectionInspectionWorkerThreadTest : public ::testing::Test signalBuffer.reset( new SignalBuffer( 1000, "Signal Buffer" ) ); // Init the output buffer outputCollectedData = std::make_shared( 3, "Collected Data" ); + +#ifdef FWE_FEATURE_STORE_AND_FORWARD + CANInterfaceIDTranslator canIDTranslator; + auto protoWriter = std::make_shared( canIDTranslator, nullptr ); + streamManager = std::make_shared<::testing::StrictMock>( protoWriter ); + // by default, forward data to DataSenderQueue + EXPECT_CALL( *streamManager, appendToStreams( ::testing::_ ) ) + .Times( ::testing::AnyNumber() ) + .WillRepeatedly( ::testing::Return( Store::StreamManager::ReturnCode::STREAM_NOT_FOUND ) ); + + rateLimiter = std::make_shared(); + streamForwarder = + std::make_shared( streamManager, nullptr, rateLimiter ); +#endif } void @@ -454,5 +495,68 @@ TEST_F( CollectionInspectionWorkerThreadTest, StartWithoutInit ) worker.onChangeInspectionMatrix( consCollectionSchemes ); } +#ifdef FWE_FEATURE_STORE_AND_FORWARD +TEST_F( CollectionInspectionWorkerThreadTest, PutStoreAndForwardDataIntoStream ) +{ + std::mutex mutex; + std::condition_variable appendComplete; + bool done = false; + EXPECT_CALL( *streamManager, appendToStreams( ::testing::_ ) ) + .WillOnce( ::testing::Invoke( [&]( const TriggeredCollectionSchemeData & ) -> Store::StreamManager::ReturnCode { + std::lock_guard lock( mutex ); + done = true; + appendComplete.notify_one(); + return Store::StreamManager::ReturnCode::SUCCESS; + } ) ); + + CollectionInspectionEngine engine; + CollectionInspectionWorkerThread inspectionWorker( engine ); + initAndStartWorker( inspectionWorker ); + + DTCInfo dtcInfo; + dtcInfo.mDTCCodes.emplace_back( "P0143" ); + dtcInfo.mDTCCodes.emplace_back( "C0196" ); + dtcInfo.mSID = SID::STORED_DTC; + dtcInfo.receiveTime = mClock->systemTimeSinceEpochMs(); + ASSERT_TRUE( dtcInfo.hasItems() ); + ASSERT_TRUE( signalBuffer->push( CollectedDataFrame( std::make_shared( dtcInfo ) ) ) ); + InspectionMatrixSignalCollectionInfo signal{}; + signal.signalID = 1234; + signal.sampleBufferSize = 50; + signal.minimumSampleIntervalMs = 0; + signal.fixedWindowPeriod = 77777; + signal.isConditionOnlySignal = false; + signal.signalType = SignalType::DOUBLE; + collectionSchemes->conditions[0].signals.push_back( signal ); + // Condition contains signals + collectionSchemes->conditions[0].isStaticCondition = false; + collectionSchemes->conditions[0].condition = getSignalsBiggerCondition( signal.signalID, 1 ).get(); + collectionSchemes->conditions[0].includeActiveDtcs = false; + inspectionWorker.onChangeInspectionMatrix( consCollectionSchemes ); + Timestamp timestamp = mClock->systemTimeSinceEpochMs(); + + // Push the signals so that the condition is met + CollectedSignalsGroup collectedSignalsGroup; + collectedSignalsGroup.push_back( CollectedSignal( signal.signalID, timestamp, 0.1, SignalType::DOUBLE ) ); + collectedSignalsGroup.push_back( CollectedSignal( signal.signalID, timestamp, 0.2, SignalType::DOUBLE ) ); + collectedSignalsGroup.push_back( CollectedSignal( signal.signalID, timestamp, 1.5, SignalType::DOUBLE ) ); + ASSERT_TRUE( signalBuffer->push( CollectedDataFrame( collectedSignalsGroup ) ) ); + + // verify data was appended to stream + { + std::unique_lock lock( mutex ); + EXPECT_TRUE( appendComplete.wait_for( lock, std::chrono::seconds( 2 ), [&done] { + return done; + } ) ); + } + + // verify data was not forwarded to the queue + std::shared_ptr collectedData; + ASSERT_FALSE( popCollectedData( collectedData ) ); + + inspectionWorker.stop(); +} +#endif + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/CollectionSchemeManagerTest.cpp b/test/unit/CollectionSchemeManagerTest.cpp index 5eb79180..57fb8692 100644 --- a/test/unit/CollectionSchemeManagerTest.cpp +++ b/test/unit/CollectionSchemeManagerTest.cpp @@ -22,6 +22,10 @@ #include #include +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateTypes.h" +#endif + namespace Aws { namespace IoTFleetWise @@ -85,6 +89,14 @@ class CollectionSchemeManagerTest : public ::testing::Test [&]( const std::shared_ptr &inspectionMatrix ) { mReceivedInspectionMatrices.emplace_back( inspectionMatrix ); } ); + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + mCollectionSchemeManager.subscribeToStateTemplatesChange( + [&]( std::shared_ptr stateTemplates ) { + std::lock_guard lock( mReceivedStateTemplatesMutex ); + mReceivedStateTemplates.emplace_back( *stateTemplates ); + } ); +#endif } void @@ -97,6 +109,26 @@ class CollectionSchemeManagerTest : public ::testing::Test CollectionSchemeManagerWrapper mCollectionSchemeManager; std::vector> mReceivedInspectionMatrices; std::shared_ptr mTestClock = ClockHandler::getClock(); + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + std::vector + getReceivedStateTemplates() + { + std::vector sortedStateTemplates; + std::lock_guard lock( mReceivedStateTemplatesMutex ); + for ( auto stateTemplateList : mReceivedStateTemplates ) + { + sort( stateTemplateList.begin(), stateTemplateList.end(), []( const auto &a, const auto &b ) { + return a->id < b->id; + } ); + sortedStateTemplates.emplace_back( stateTemplateList ); + } + return sortedStateTemplates; + }; + + std::mutex mReceivedStateTemplatesMutex; + std::vector mReceivedStateTemplates; +#endif }; std::vector @@ -140,6 +172,46 @@ TEST_F( CollectionSchemeManagerTest, StopMain ) std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); mCollectionSchemeManager.myInvokeCollectionScheme(); std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + auto lastKnownStateIngestionMock = std::make_shared(); + mCollectionSchemeManager.mLastKnownStateIngestionTest = lastKnownStateIngestionMock; + // Build failed: + EXPECT_CALL( *lastKnownStateIngestionMock, build() ).WillRepeatedly( Return( false ) ); + mCollectionSchemeManager.myInvokeStateTemplates(); + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); + // Build successful, DM out of sync: + EXPECT_CALL( *lastKnownStateIngestionMock, build() ).WillRepeatedly( Return( true ) ); + + { + auto stateTemplate = std::make_shared(); + stateTemplate->id = "LKS1"; + stateTemplate->decoderManifestID = "DM2"; + auto stateTemplatesDiff = std::make_shared(); + stateTemplatesDiff->stateTemplatesToAdd.emplace_back( stateTemplate ); + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillRepeatedly( Return( stateTemplatesDiff ) ); + mCollectionSchemeManager.myInvokeStateTemplates(); + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); + + // DM in sync: + { + auto stateTemplate = std::make_shared(); + stateTemplate->id = "LKS2"; + stateTemplate->decoderManifestID = "DM1"; + auto stateTemplatesDiff = std::make_shared(); + stateTemplatesDiff->stateTemplatesToAdd.emplace_back( stateTemplate ); + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillRepeatedly( Return( stateTemplatesDiff ) ); + mCollectionSchemeManager.myInvokeStateTemplates(); + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); + // Ignore with the same sync id: + mCollectionSchemeManager.myInvokeStateTemplates(); + std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); +#endif /* stopping main thread servicing a collectionScheme ending in 25 seconds */ WAIT_ASSERT_TRUE( mCollectionSchemeManager.disconnect() ); @@ -186,6 +258,230 @@ TEST_F( CollectionSchemeManagerTest, DecoderManifestUpdateCallBack ) ASSERT_TRUE( mCollectionSchemeManager.getmProcessDecoderManifest() ); } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +TEST_F( CollectionSchemeManagerTest, StateTemplatesUpdate ) +{ + auto decoderManifestIngestionMock = std::make_shared(); + EXPECT_CALL( *decoderManifestIngestionMock, build() ).WillRepeatedly( Return( true ) ); + EXPECT_CALL( *decoderManifestIngestionMock, getID() ).WillRepeatedly( Return( "decoder1" ) ); + auto lastKnownStateIngestionMock = std::make_shared(); + EXPECT_CALL( *lastKnownStateIngestionMock, build() ).WillRepeatedly( Return( true ) ); + + ASSERT_TRUE( mCollectionSchemeManager.connect() ); + + mCollectionSchemeManager.onDecoderManifestUpdate( decoderManifestIngestionMock ); + + // This should be an empty list, triggered by the decoder manifest update + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 1U ); + auto receivedStateTemplates = getReceivedStateTemplates(); + auto stateTemplates = receivedStateTemplates[0]; + ASSERT_EQ( stateTemplates.size(), 0U ); + + auto stateTemplateToAdd = std::make_shared( + StateTemplateInformation{ "stateTemplate1", + "decoder1", + { LastKnownStateSignalInformation{ 1 }, LastKnownStateSignalInformation{ 2 } }, + LastKnownStateUpdateStrategy::PERIODIC, + 500 } ); + + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, { stateTemplateToAdd }, {} } ) ) ); + + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 2U ); + receivedStateTemplates = getReceivedStateTemplates(); + + stateTemplates = receivedStateTemplates[1]; + ASSERT_EQ( stateTemplates.size(), 1U ); + ASSERT_EQ( stateTemplates[0]->id, "stateTemplate1" ); + ASSERT_EQ( stateTemplates[0]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[0]->signals.size(), 2U ); + ASSERT_EQ( stateTemplates[0]->signals[0].signalID, 1 ); + ASSERT_EQ( stateTemplates[0]->signals[1].signalID, 2 ); + ASSERT_EQ( stateTemplates[0]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[0]->periodMs, 500U ); + + stateTemplateToAdd = std::make_shared( + StateTemplateInformation{ "stateTemplate2", + "decoder1", + { LastKnownStateSignalInformation{ 7 } }, + LastKnownStateUpdateStrategy::PERIODIC, + 400 } ); + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, { stateTemplateToAdd }, {} } ) ) ); + + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 3U ); + receivedStateTemplates = getReceivedStateTemplates(); + + stateTemplates = receivedStateTemplates[2]; + ASSERT_EQ( stateTemplates.size(), 2U ); + + ASSERT_EQ( stateTemplates[0]->id, "stateTemplate1" ); + ASSERT_EQ( stateTemplates[0]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[0]->signals.size(), 2U ); + ASSERT_EQ( stateTemplates[0]->signals[0].signalID, 1 ); + ASSERT_EQ( stateTemplates[0]->signals[1].signalID, 2 ); + ASSERT_EQ( stateTemplates[0]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[0]->periodMs, 500U ); + + ASSERT_EQ( stateTemplates[1]->id, "stateTemplate2" ); + ASSERT_EQ( stateTemplates[1]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[1]->signals.size(), 1U ); + ASSERT_EQ( stateTemplates[1]->signals[0].signalID, 7 ); + ASSERT_EQ( stateTemplates[1]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[1]->periodMs, 400U ); + + std::vector stateTemplatesToRemove{ "stateTemplate1" }; + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, {}, stateTemplatesToRemove } ) ) ); + + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 4U ); + receivedStateTemplates = getReceivedStateTemplates(); + + stateTemplates = receivedStateTemplates[3]; + ASSERT_EQ( stateTemplates.size(), 1U ); + + ASSERT_EQ( stateTemplates[0]->id, "stateTemplate2" ); + ASSERT_EQ( stateTemplates[0]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[0]->signals.size(), 1U ); + ASSERT_EQ( stateTemplates[0]->signals[0].signalID, 7 ); + ASSERT_EQ( stateTemplates[0]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[0]->periodMs, 400U ); +} + +TEST_F( CollectionSchemeManagerTest, StateTemplatesUpdateRejectDiffWithLowerVersion ) +{ + auto decoderManifestIngestionMock = std::make_shared(); + EXPECT_CALL( *decoderManifestIngestionMock, build() ).WillRepeatedly( Return( true ) ); + EXPECT_CALL( *decoderManifestIngestionMock, getID() ).WillRepeatedly( Return( "decoder1" ) ); + auto lastKnownStateIngestionMock = std::make_shared(); + EXPECT_CALL( *lastKnownStateIngestionMock, build() ).WillRepeatedly( Return( true ) ); + + ASSERT_TRUE( mCollectionSchemeManager.connect() ); + + mCollectionSchemeManager.onDecoderManifestUpdate( decoderManifestIngestionMock ); + + // This should be an empty list, triggered by the decoder manifest update + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 1U ); + + auto stateTemplateToAdd = std::make_shared( + StateTemplateInformation{ "stateTemplate1", + "decoder1", + { LastKnownStateSignalInformation{ 1 }, LastKnownStateSignalInformation{ 2 } }, + LastKnownStateUpdateStrategy::PERIODIC, + 500 } ); + + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, { stateTemplateToAdd }, {} } ) ) ); + + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 2U ); + auto receivedStateTemplates = getReceivedStateTemplates(); + + auto stateTemplates = receivedStateTemplates[1]; + ASSERT_EQ( stateTemplates.size(), 1U ); + ASSERT_EQ( stateTemplates[0]->id, "stateTemplate1" ); + + stateTemplateToAdd = std::make_shared( + StateTemplateInformation{ "stateTemplate2", + "decoder1", + { LastKnownStateSignalInformation{ 7 } }, + LastKnownStateUpdateStrategy::PERIODIC, + 400 } ); + // This has a smaller version than the previous received message, it should be ignored. + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 455, { stateTemplateToAdd }, {} } ) ) ); + auto previousSize = receivedStateTemplates.size(); + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + DELAY_ASSERT_TRUE( getReceivedStateTemplates().size() == previousSize ); + + // This has the same version of the last accepted message, it should be processed normally. + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, { stateTemplateToAdd }, {} } ) ) ); + + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 3U ); + receivedStateTemplates = getReceivedStateTemplates(); + + stateTemplates = receivedStateTemplates[2]; + ASSERT_EQ( stateTemplates.size(), 2U ); + + ASSERT_EQ( stateTemplates[0]->id, "stateTemplate1" ); + ASSERT_EQ( stateTemplates[0]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[0]->signals.size(), 2U ); + ASSERT_EQ( stateTemplates[0]->signals[0].signalID, 1 ); + ASSERT_EQ( stateTemplates[0]->signals[1].signalID, 2 ); + ASSERT_EQ( stateTemplates[0]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[0]->periodMs, 500U ); + + ASSERT_EQ( stateTemplates[1]->id, "stateTemplate2" ); + ASSERT_EQ( stateTemplates[1]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[1]->signals.size(), 1U ); + ASSERT_EQ( stateTemplates[1]->signals[0].signalID, 7 ); + ASSERT_EQ( stateTemplates[1]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[1]->periodMs, 400U ); + + std::vector stateTemplatesToRemove{ "stateTemplate1" }; + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, {}, stateTemplatesToRemove } ) ) ); + + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 4U ); + receivedStateTemplates = getReceivedStateTemplates(); + + stateTemplates = receivedStateTemplates[3]; + ASSERT_EQ( stateTemplates.size(), 1U ); + + ASSERT_EQ( stateTemplates[0]->id, "stateTemplate2" ); + ASSERT_EQ( stateTemplates[0]->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplates[0]->signals.size(), 1U ); + ASSERT_EQ( stateTemplates[0]->signals[0].signalID, 7 ); + ASSERT_EQ( stateTemplates[0]->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplates[0]->periodMs, 400U ); +} + +TEST_F( CollectionSchemeManagerTest, StateTemplatesUpdateRemoveNonExistingTemplate ) +{ + auto lastKnownStateIngestionMock = std::make_shared(); + + ASSERT_TRUE( mCollectionSchemeManager.connect() ); + + auto decoderManifestIngestionMock = std::make_shared(); + EXPECT_CALL( *decoderManifestIngestionMock, build() ).WillRepeatedly( Return( true ) ); + EXPECT_CALL( *decoderManifestIngestionMock, getID() ).WillRepeatedly( Return( "decoder1" ) ); + mCollectionSchemeManager.onDecoderManifestUpdate( decoderManifestIngestionMock ); + + // This should be an empty list, triggered by the decoder manifest update + WAIT_ASSERT_EQ( getReceivedStateTemplates().size(), 1U ); + + std::vector stateTemplatesToRemove{ "stateTemplate1" }; + EXPECT_CALL( *lastKnownStateIngestionMock, build() ).WillRepeatedly( Return( true ) ); + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillOnce( + Return( std::make_shared( StateTemplatesDiff{ 456, {}, stateTemplatesToRemove } ) ) ); + + auto previousSize = getReceivedStateTemplates().size(); + mCollectionSchemeManager.onStateTemplatesChanged( lastKnownStateIngestionMock ); + + DELAY_ASSERT_TRUE( getReceivedStateTemplates().size() == previousSize ); +} +#endif + TEST_F( CollectionSchemeManagerTest, MockProducer ) { /* @@ -387,6 +683,56 @@ TEST_F( CollectionSchemeManagerTest, SendCheckinPeriodically ) ASSERT_EQ( sentDocuments[1], "COLLECTIONSCHEME2" ); ASSERT_EQ( sentDocuments[2], "DM1" ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + auto lastKnownStateIngestionMock = std::make_shared(); + collectionSchemeManager.mLastKnownStateIngestionTest = lastKnownStateIngestionMock; + auto stateTemplate1 = std::make_shared(); + stateTemplate1->id = "LKS1"; + stateTemplate1->decoderManifestID = "DM1"; + EXPECT_CALL( *lastKnownStateIngestionMock, build() ).WillRepeatedly( Return( true ) ); + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillRepeatedly( + Return( std::make_shared( StateTemplatesDiff{ 123, { stateTemplate1 }, {} } ) ) ); + collectionSchemeManager.myInvokeStateTemplates(); + + WAIT_ASSERT_EQ( schemaListenerMock->getLastSentDocuments( sentDocuments ), 4 ); + sort( sentDocuments.begin(), sentDocuments.end() ); + ASSERT_EQ( sentDocuments[0], "COLLECTIONSCHEME1" ); + ASSERT_EQ( sentDocuments[1], "COLLECTIONSCHEME2" ); + ASSERT_EQ( sentDocuments[2], "DM1" ); + ASSERT_EQ( sentDocuments[3], "LKS1" ); + + // Add another template + auto stateTemplate2 = std::make_shared(); + stateTemplate2->id = "LKS2"; + stateTemplate2->decoderManifestID = "DM1"; + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillRepeatedly( + Return( std::make_shared( StateTemplatesDiff{ 123, { stateTemplate2 }, {} } ) ) ); + collectionSchemeManager.myInvokeStateTemplates(); + + WAIT_ASSERT_EQ( schemaListenerMock->getLastSentDocuments( sentDocuments ), 5 ); + sort( sentDocuments.begin(), sentDocuments.end() ); + ASSERT_EQ( sentDocuments[0], "COLLECTIONSCHEME1" ); + ASSERT_EQ( sentDocuments[1], "COLLECTIONSCHEME2" ); + ASSERT_EQ( sentDocuments[2], "DM1" ); + ASSERT_EQ( sentDocuments[3], "LKS1" ); + ASSERT_EQ( sentDocuments[4], "LKS2" ); + + // Remove an existing template + EXPECT_CALL( *lastKnownStateIngestionMock, getStateTemplatesDiff() ) + .WillRepeatedly( + Return( std::make_shared( StateTemplatesDiff{ 123, {}, { stateTemplate1->id } } ) ) ); + collectionSchemeManager.myInvokeStateTemplates(); + + WAIT_ASSERT_EQ( schemaListenerMock->getLastSentDocuments( sentDocuments ), 4 ); + sort( sentDocuments.begin(), sentDocuments.end() ); + ASSERT_EQ( sentDocuments[0], "COLLECTIONSCHEME1" ); + ASSERT_EQ( sentDocuments[1], "COLLECTIONSCHEME2" ); + ASSERT_EQ( sentDocuments[2], "DM1" ); + ASSERT_EQ( sentDocuments[3], "LKS2" ); +#endif + auto numOfMessagesSent = schemaListenerMock->getSentDocuments().size(); std::this_thread::sleep_for( std::chrono::milliseconds( checkinIntervalMs * 5 ) ); diff --git a/test/unit/CommandSchemaTest.cpp b/test/unit/CommandSchemaTest.cpp new file mode 100644 index 00000000..55accd73 --- /dev/null +++ b/test/unit/CommandSchemaTest.cpp @@ -0,0 +1,593 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CommandSchema.h" +#include "AwsIotConnectivityModule.h" +#include "AwsIotReceiver.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "ICommandDispatcher.h" +#include "MqttClientWrapper.h" +#include "QueueTypes.h" +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "Testing.h" +#include "TopicConfig.h" +#include "WaitUntil.h" +#include "command_request.pb.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::Gt; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrictMock; + +class CommandSchemaTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + TopicConfigArgs topicConfigArgs; + mTopicConfig = std::make_unique( "thing-name", topicConfigArgs ); + mAwsIotModule = std::make_unique( "", "", nullptr, *mTopicConfig ); + + std::shared_ptr nullMqttClient; + + mReceiverCommandRequest = std::make_shared( mAwsIotModule.get(), nullMqttClient, "topic" ); + + mCommandResponses = std::make_shared( 100, "Command Responses" ); + + mRawBufferManagerSpy = std::make_shared>( + RawData::BufferManagerConfig::create().get() ); + mCommandSchema = + std::make_unique( mReceiverCommandRequest, mCommandResponses, mRawBufferManagerSpy ); + mCommandSchema->subscribeToActuatorCommandRequestReceived( [&]( const ActuatorCommandRequest &commandRequest ) { + mReceivedActuatorCommandRequests.emplace_back( commandRequest ); + } ); + mCommandSchema->subscribeToLastKnownStateCommandRequestReceived( + [&]( const LastKnownStateCommandRequest &commandRequest ) { + mReceivedLastKnownStateCommandRequests.emplace_back( commandRequest ); + } ); + } + + static Aws::Crt::Mqtt5::PublishReceivedEventData + createPublishEvent( const std::string &protoSerializedBuffer ) + { + auto publishPacket = std::make_shared(); + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + eventData.publishPacket = publishPacket; + publishPacket->WithPayload( Aws::Crt::ByteCursorFromArray( + reinterpret_cast( protoSerializedBuffer.data() ), protoSerializedBuffer.length() ) ); + + return eventData; + } + + static void + setSignalValue( Schemas::Commands::ActuatorCommand *protoActuatorCommand, + double signalValue, + SignalType signalType ) + { + // Note: protobuf smallest integer is 32 bits, that is why we cast the smaller types to int32 and uint32 below + switch ( signalType ) + { + case SignalType::UINT8: + protoActuatorCommand->set_uint8_value( static_cast( signalValue ) ); + break; + case SignalType::INT8: + protoActuatorCommand->set_int8_value( static_cast( signalValue ) ); + break; + case SignalType::UINT16: + protoActuatorCommand->set_uint16_value( static_cast( signalValue ) ); + break; + case SignalType::INT16: + protoActuatorCommand->set_int16_value( static_cast( signalValue ) ); + break; + case SignalType::UINT32: + protoActuatorCommand->set_uint32_value( static_cast( signalValue ) ); + break; + case SignalType::INT32: + protoActuatorCommand->set_int32_value( static_cast( signalValue ) ); + break; + case SignalType::UINT64: + protoActuatorCommand->set_uint64_value( static_cast( signalValue ) ); + break; + case SignalType::INT64: + protoActuatorCommand->set_int64_value( static_cast( signalValue ) ); + break; + case SignalType::FLOAT: + protoActuatorCommand->set_float_value( static_cast( signalValue ) ); + break; + case SignalType::DOUBLE: + protoActuatorCommand->set_double_value( signalValue ); + break; + case SignalType::BOOLEAN: + protoActuatorCommand->set_boolean_value( static_cast( signalValue ) ); + break; + default: + FAIL() << "Unsupported signal type"; + } + } + + bool + popCommandResponse( std::shared_ptr &commandResponse ) + { + std::shared_ptr senderData; + auto succeeded = mCommandResponses->pop( senderData ); + commandResponse = std::dynamic_pointer_cast( senderData ); + return succeeded; + } + + std::unique_ptr mTopicConfig; + std::unique_ptr mAwsIotModule; + std::shared_ptr mReceiverCommandRequest; + std::shared_ptr mCommandResponses; + std::shared_ptr> mRawBufferManagerSpy; + std::unique_ptr mCommandSchema; + + std::vector mReceivedActuatorCommandRequests; + std::vector mReceivedLastKnownStateCommandRequests; +}; + +TEST_F( CommandSchemaTest, ingestEmptyCommandRequest ) +{ + std::string protoSerializedBuffer; + + mReceiverCommandRequest->onDataReceived( createPublishEvent( protoSerializedBuffer ) ); + + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 0 ); +} + +TEST_F( CommandSchemaTest, ingestCommandRequestLargerThanLimit ) +{ + std::string protoSerializedBuffer( COMMAND_REQUEST_BYTE_SIZE_LIMIT + 1, 'X' ); + + mReceiverCommandRequest->onDataReceived( createPublishEvent( protoSerializedBuffer ) ); + + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 0 ); +} + +TEST_F( CommandSchemaTest, ingestActuatorCommandRequest ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + protoCommandRequest.set_timeout_ms( 5000 ); + auto issuedTimestampMs = ClockHandler::getClock()->systemTimeSinceEpochMs(); + protoCommandRequest.set_issued_timestamp_ms( issuedTimestampMs ); + + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + protoActuatorCommand->set_signal_id( 12345 ); + protoActuatorCommand->set_double_value( 12.5 ); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 1 ); + auto commandRequest = mReceivedActuatorCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.signalID, 12345 ); + ASSERT_EQ( commandRequest.signalValueWrapper.type, SignalType::DOUBLE ); + ASSERT_EQ( commandRequest.signalValueWrapper.value.doubleVal, 12.5 ); + ASSERT_EQ( commandRequest.executionTimeoutMs, 5000 ); + ASSERT_EQ( commandRequest.issuedTimestampMs, issuedTimestampMs ); +} + +TEST_F( CommandSchemaTest, ingestActuatorCommandRequestWithInvalidValue ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + // Don't set any value, only the signal ID + protoActuatorCommand->set_signal_id( 12345 ); + std::string protoSerializedBuffer; + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command123" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_COMMAND_REQUEST_PARSING_FAILED ); + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); + + // Now set a string value with an unsupported signal id + protoActuatorCommand->set_signal_id( 12345 ); + protoActuatorCommand->set_string_value( "some string value" ); + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command123" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_REJECTED ); + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); + + // Now just as sanity check set a valid value to ensure it was not failing for some other reason + protoActuatorCommand->set_signal_id( 12345 ); + protoActuatorCommand->set_double_value( 12.5 ); + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 1U ); + auto commandRequest = mReceivedActuatorCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.signalID, 12345 ); + ASSERT_EQ( commandRequest.signalValueWrapper.type, SignalType::DOUBLE ); + ASSERT_EQ( commandRequest.signalValueWrapper.value.doubleVal, 12.5 ); +} + +struct TestSignal +{ + TestSignal( double value, SignalType type ) + : value( value ) + , type( type ) + { + } + + double value{ 0.0 }; + SignalType type{ SignalType::DOUBLE }; + // This is a dummy variable just to prevent the compiler from adding padding. Since this struct is being + // used as a test parameter, gtest tries to print its bytes. If there is any padding added by the compiler, + // gtest will try to read it even though it is normally uninitialized, which will be detected by valgrind. + // More details: https://github.com/google/googletest/issues/3805 + uint32_t padding{ 0 }; + + static std::string + toString( const ::testing::TestParamInfo &info ) + { + // Test names can only contain alphanumeric characters, so we can't just convert a double to string + auto doubleValueAsString = std::regex_replace( std::to_string( info.param.value ), std::regex( "\\." ), "d" ); + doubleValueAsString = std::regex_replace( doubleValueAsString, std::regex( "-" ), "n" ); + return doubleValueAsString + signalTypeParamInfoToString( { info.param.type, info.index } ); + } +}; + +class CommandSchemaTestWithAllSignalTypes : public CommandSchemaTest, public testing::WithParamInterface +{ +}; + +INSTANTIATE_TEST_SUITE_P( MultipleSignals, + CommandSchemaTestWithAllSignalTypes, + testing::Values( TestSignal{ static_cast( UINT8_MAX ), SignalType::UINT8 }, + TestSignal{ static_cast( INT8_MIN ), SignalType::INT8 }, + TestSignal{ static_cast( INT8_MAX ), SignalType::INT8 }, + TestSignal{ static_cast( UINT16_MAX ), SignalType::UINT16 }, + TestSignal{ static_cast( INT16_MIN ), SignalType::INT16 }, + TestSignal{ static_cast( INT16_MAX ), SignalType::INT16 }, + TestSignal{ static_cast( UINT32_MAX ), SignalType::UINT32 }, + TestSignal{ static_cast( INT32_MIN ), SignalType::INT32 }, + TestSignal{ static_cast( INT32_MAX ), SignalType::INT32 }, + TestSignal{ static_cast( UINT64_MAX ), SignalType::UINT64 }, + TestSignal{ static_cast( INT64_MIN ), SignalType::INT64 }, + TestSignal{ static_cast( INT64_MAX ), SignalType::INT64 }, + TestSignal{ static_cast( FLT_MIN ), SignalType::FLOAT }, + TestSignal{ static_cast( -FLT_MAX ), SignalType::FLOAT }, + TestSignal{ static_cast( FLT_MAX ), SignalType::FLOAT }, + TestSignal{ static_cast( DBL_MIN ), SignalType::DOUBLE }, + TestSignal{ static_cast( -DBL_MAX ), SignalType::DOUBLE }, + TestSignal{ static_cast( DBL_MAX ), SignalType::DOUBLE }, + TestSignal{ static_cast( 0.0 ), SignalType::BOOLEAN }, + TestSignal{ static_cast( 1.0 ), SignalType::BOOLEAN } ), + TestSignal::toString ); + +TEST_P( CommandSchemaTestWithAllSignalTypes, ingestActuatorCommandRequestWithAllSignalTypes ) +{ + TestSignal signal = GetParam(); + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + protoActuatorCommand->set_signal_id( 12345 ); + setSignalValue( protoActuatorCommand, signal.value, signal.type ); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 1U ); + auto commandRequest = mReceivedActuatorCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.signalID, 12345 ); + ASSERT_EQ( commandRequest.signalValueWrapper.type, signal.type ); + ASSERT_NO_FATAL_FAILURE( assertSignalValue( commandRequest.signalValueWrapper, signal.value, signal.type ) ); +} + +class CommandSchemaTestWithOutOfRangeSignals : public CommandSchemaTest, public testing::WithParamInterface +{ +}; + +INSTANTIATE_TEST_SUITE_P( MultipleSignals, + CommandSchemaTestWithOutOfRangeSignals, + testing::Values( TestSignal{ static_cast( UINT8_MAX + 1 ), SignalType::UINT8 }, + TestSignal{ static_cast( INT8_MIN - 1 ), SignalType::INT8 }, + TestSignal{ static_cast( INT8_MAX + 1 ), SignalType::INT8 }, + TestSignal{ static_cast( UINT16_MAX + 1 ), SignalType::UINT16 }, + TestSignal{ static_cast( INT16_MIN - 1 ), SignalType::INT16 }, + TestSignal{ static_cast( INT16_MAX + 1 ), SignalType::INT16 } ), + TestSignal::toString ); + +TEST_P( CommandSchemaTestWithOutOfRangeSignals, ingestActuatorCommandRequestWithOutOfRangeSignal ) +{ + TestSignal signal = GetParam(); + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + protoActuatorCommand->set_signal_id( 12345 ); + setSignalValue( protoActuatorCommand, signal.value, signal.type ); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); +} + +TEST_F( CommandSchemaTest, ingestCommandAlreadyTimedOut ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + protoCommandRequest.set_issued_timestamp_ms( ClockHandler::getClock()->systemTimeSinceEpochMs() - 1000 ); + protoCommandRequest.set_timeout_ms( 500 ); + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + protoActuatorCommand->set_signal_id( 12345 ); + protoActuatorCommand->set_uint32_value( 1234 ); + std::string protoSerializedBuffer; + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + mReceiverCommandRequest->onDataReceived( publishEvent ); + std::shared_ptr commandResponse; + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command123" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_TIMEOUT ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_TIMED_OUT_BEFORE_DISPATCH ); + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); +} + +TEST_F( CommandSchemaTest, ingestActuatorCommandRequestWithStringSignalWithBadSignalId ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + protoActuatorCommand->set_signal_id( 12345 ); + protoActuatorCommand->set_string_value( "some string value" ); + std::string protoSerializedBuffer; + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + mReceiverCommandRequest->onDataReceived( publishEvent ); + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 0 ); +} + +TEST_F( CommandSchemaTest, ingestActuatorCommandRequestWithStringSignal ) +{ + mRawBufferManagerSpy->updateConfig( { { 12345, { 12345, "", "" } } } ); + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + // Check setting issued time in future just produces warning + protoCommandRequest.set_issued_timestamp_ms( ClockHandler::getClock()->systemTimeSinceEpochMs() + 1000 ); + auto *protoActuatorCommand = protoCommandRequest.mutable_actuator_command(); + protoActuatorCommand->set_signal_id( 12345 ); + protoActuatorCommand->set_string_value( "some string value" ); + std::string protoSerializedBuffer; + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + mReceiverCommandRequest->onDataReceived( publishEvent ); + ASSERT_EQ( mReceivedActuatorCommandRequests.size(), 1U ); + auto commandRequest = mReceivedActuatorCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.signalID, 12345 ); + ASSERT_EQ( commandRequest.signalValueWrapper.type, SignalType::STRING ); + auto loanedFrame = mRawBufferManagerSpy->borrowFrame( commandRequest.signalValueWrapper.value.rawDataVal.signalId, + commandRequest.signalValueWrapper.value.rawDataVal.handle ); + ASSERT_FALSE( loanedFrame.isNull() ); + std::string receivedStringVal; + receivedStringVal.assign( reinterpret_cast( loanedFrame.getData() ), loanedFrame.getSize() ); + EXPECT_EQ( receivedStringVal, "some string value" ); +} + +TEST_F( CommandSchemaTest, ingestLastKnownStateActivateCommandRequest ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + + auto *protoLastKnownStateCommand = protoCommandRequest.mutable_last_known_state_command(); + auto *protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->mutable_activate_operation(); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 1 ); + auto commandRequest = mReceivedLastKnownStateCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks1" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::ACTIVATE ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); +} + +TEST_F( CommandSchemaTest, ingestLastKnownStateActivateCommandRequestWithAutoDeactivate ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + + auto *protoLastKnownStateCommand = protoCommandRequest.mutable_last_known_state_command(); + auto *protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + auto *protoActivateOperation = protoStateTemplateInfo->mutable_activate_operation(); + protoActivateOperation->set_deactivate_after_seconds( 432 ); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 1 ); + auto commandRequest = mReceivedLastKnownStateCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks1" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::ACTIVATE ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 432 ); +} + +TEST_F( CommandSchemaTest, ingestLastKnownStateDeactivateCommandRequest ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + + auto *protoLastKnownStateCommand = protoCommandRequest.mutable_last_known_state_command(); + auto *protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->mutable_deactivate_operation(); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 1 ); + auto commandRequest = mReceivedLastKnownStateCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks1" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::DEACTIVATE ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); +} + +TEST_F( CommandSchemaTest, ingestLastKnownStateFetchCommandRequest ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + + auto *protoLastKnownStateCommand = protoCommandRequest.mutable_last_known_state_command(); + auto *protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->mutable_fetch_snapshot_operation(); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 1 ); + auto commandRequest = mReceivedLastKnownStateCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks1" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::FETCH_SNAPSHOT ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); +} + +TEST_F( CommandSchemaTest, ingestMultipleLastKnownStateCommandRequest ) +{ + Schemas::Commands::CommandRequest protoCommandRequest; + protoCommandRequest.set_command_id( "command123" ); + + auto *protoLastKnownStateCommand = protoCommandRequest.mutable_last_known_state_command(); + auto *protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->mutable_activate_operation(); + + protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks2" ); + protoStateTemplateInfo->mutable_deactivate_operation(); + + protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks3" ); + protoStateTemplateInfo->mutable_fetch_snapshot_operation(); + + protoStateTemplateInfo = protoLastKnownStateCommand->add_state_template_information(); + protoStateTemplateInfo->set_state_template_sync_id( "lks4" ); + protoStateTemplateInfo->mutable_deactivate_operation(); + + std::string protoSerializedBuffer; + + ASSERT_TRUE( protoCommandRequest.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + + mReceiverCommandRequest->onDataReceived( publishEvent ); + + ASSERT_EQ( mReceivedLastKnownStateCommandRequests.size(), 4 ); + auto commandRequest = mReceivedLastKnownStateCommandRequests[0]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks1" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::ACTIVATE ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); + + commandRequest = mReceivedLastKnownStateCommandRequests[1]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks2" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::DEACTIVATE ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); + + commandRequest = mReceivedLastKnownStateCommandRequests[2]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks3" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::FETCH_SNAPSHOT ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); + + commandRequest = mReceivedLastKnownStateCommandRequests[3]; + ASSERT_EQ( commandRequest.commandID, "command123" ); + ASSERT_EQ( commandRequest.stateTemplateID, "lks4" ); + ASSERT_EQ( commandRequest.operation, LastKnownStateOperation::DEACTIVATE ); + ASSERT_EQ( commandRequest.deactivateAfterSeconds, 0 ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/CustomDataSourceTest.cpp b/test/unit/CustomDataSourceTest.cpp deleted file mode 100644 index 640891ee..00000000 --- a/test/unit/CustomDataSourceTest.cpp +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "CustomDataSource.h" -#include "IDecoderDictionary.h" -#include "MessageTypes.h" -#include "SignalTypes.h" -#include "VehicleDataSourceTypes.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Aws -{ -namespace IoTFleetWise -{ - -using ::testing::_; -using ::testing::AnyNumber; -using ::testing::AtLeast; -using ::testing::Between; -using ::testing::DoAll; -using ::testing::Invoke; -using ::testing::NiceMock; -using ::testing::Return; -using ::testing::ReturnRef; -using ::testing::SaveArg; -using ::testing::SaveArgPointee; - -class CustomDataSourceTestImplementation : public CustomDataSource -{ -public: - MOCK_METHOD( (void), pollData, (), ( override ) ); - - void - setPollInternalIntervalMs( uint32_t pollIntervalMs ) - { - setPollIntervalMs( pollIntervalMs ); - } -}; - -class CustomDataSourceTest : public ::testing::Test -{ -protected: - void - SetUp() override - { - - std::unordered_map frameMap; - CANMessageDecoderMethod decoderMethod; - decoderMethod.collectType = CANMessageCollectType::DECODE; - decoderMethod.format.mMessageID = 12345; - CANSignalFormat sig1; - CANSignalFormat sig2; - sig1.mFirstBitPosition = 0; - sig1.mSignalID = 0x1234; - sig2.mFirstBitPosition = 32; - sig2.mSignalID = 0x5678; - decoderMethod.format.mSignals.push_back( sig1 ); - decoderMethod.format.mSignals.push_back( sig2 ); - frameMap[1] = decoderMethod; - mDictionary = std::make_shared(); - mDictionary->canMessageDecoderMethod[1] = frameMap; - } - - void - TearDown() override - { - } - - std::shared_ptr mDictionary; -}; - -/** @brief pollData should be only called when at least one campaign uses a signal*/ -TEST_F( CustomDataSourceTest, pollOnlyWithValidData ) -{ - NiceMock implementationMock; - EXPECT_CALL( implementationMock, pollData() ).Times( 0 ); - // no setFilter at - implementationMock.start(); - implementationMock.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); - std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) ); - implementationMock.setFilter( 5, 2 ); - std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) ); - implementationMock.setFilter( 1, 2 ); - std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) ); - implementationMock.setFilter( 2, 1 ); - - std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) ); - - // set the filter matching to dictionary 1,1 so now pollData should be called - EXPECT_CALL( implementationMock, pollData() ).Times( AtLeast( 3 ) ); - implementationMock.setFilter( 1, 1 ); - std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) ); - - // change filter again so calls should stop - implementationMock.setFilter( 2, 1 ); - std::this_thread::sleep_for( std::chrono::milliseconds( 150 ) ); - EXPECT_CALL( implementationMock, pollData() ).Times( 0 ); - std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) ); -} - -TEST_F( CustomDataSourceTest, pollIntervalChanges ) -{ - NiceMock implementationMock; - EXPECT_CALL( implementationMock, pollData() ).Times( 0 ); - // no setFilter at - implementationMock.setPollInternalIntervalMs( 50 ); - implementationMock.start(); - implementationMock.setFilter( 1, 1 ); - implementationMock.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); - EXPECT_CALL( implementationMock, pollData() ).Times( Between( 8, 12 ) ); - std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) ); -} - -} // namespace IoTFleetWise -} // namespace Aws diff --git a/test/unit/CustomFunctionMathTest.cpp b/test/unit/CustomFunctionMathTest.cpp new file mode 100644 index 00000000..4665a38d --- /dev/null +++ b/test/unit/CustomFunctionMathTest.cpp @@ -0,0 +1,157 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CustomFunctionMath.h" +#include "CollectionInspectionAPITypes.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +TEST( CustomFunctionMathTest, abs ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::absFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 1 ); + auto result = CustomFunctionMath::absFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = -1; + result = CustomFunctionMath::absFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 1.0 ); +} + +TEST( CustomFunctionMathTest, min ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::minFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 2 ); + auto result = CustomFunctionMath::minFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 1; + args[1] = 2; + result = CustomFunctionMath::minFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 1.0 ); +} + +TEST( CustomFunctionMathTest, max ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::maxFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 2 ); + auto result = CustomFunctionMath::maxFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 1; + args[1] = 2; + result = CustomFunctionMath::maxFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 2.0 ); +} + +TEST( CustomFunctionMathTest, pow ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::powFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 2 ); + auto result = CustomFunctionMath::powFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + // Square-root of -1: + args[0] = -1; + args[1] = 0.5; + result = CustomFunctionMath::powFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 2; + args[1] = 10; + result = CustomFunctionMath::powFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 1024.0 ); +} + +TEST( CustomFunctionMathTest, log ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::logFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 2 ); + auto result = CustomFunctionMath::logFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = -1; + args[1] = 100; + result = CustomFunctionMath::logFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 10; + args[1] = -1; + result = CustomFunctionMath::logFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 10; + args[1] = 100; + result = CustomFunctionMath::logFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 2.0 ); +} + +TEST( CustomFunctionMathTest, ceil ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::ceilFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 1 ); + auto result = CustomFunctionMath::ceilFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 0.001; + result = CustomFunctionMath::ceilFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 1.0 ); +} + +TEST( CustomFunctionMathTest, floor ) +{ + std::vector args; + ASSERT_EQ( CustomFunctionMath::floorFunc( 0, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + + args.resize( 1 ); + auto result = CustomFunctionMath::floorFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isUndefined() ); + + args[0] = 1.999; + result = CustomFunctionMath::floorFunc( 0, args ); + ASSERT_EQ( result.error, ExpressionErrorCode::SUCCESSFUL ); + ASSERT_TRUE( result.value.isBoolOrDouble() ); + ASSERT_EQ( result.value.asDouble(), 1.0 ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/CustomFunctionMultiRisingEdgeTriggerTest.cpp b/test/unit/CustomFunctionMultiRisingEdgeTriggerTest.cpp new file mode 100644 index 00000000..8018ee9e --- /dev/null +++ b/test/unit/CustomFunctionMultiRisingEdgeTriggerTest.cpp @@ -0,0 +1,250 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "CustomFunctionMultiRisingEdgeTrigger.h" +#include "CollectionInspectionAPITypes.h" +#include "IDecoderDictionary.h" +#include "IDecoderManifest.h" +#include "NamedSignalDataSource.h" +#include "QueueTypes.h" +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "VehicleDataSourceTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::NiceMock; + +class CustomFunctionMultiRisingEdgeTriggerTest : public ::testing::Test +{ +public: + CustomFunctionMultiRisingEdgeTriggerTest() + : mDictionary( std::make_shared() ) + , mSignalBuffer( std::make_shared( 100, "Signal Buffer" ) ) + , mSignalBufferDistributor( std::make_shared() ) + , mNamedSignalDataSource( std::make_shared( "NAMED_SIGNAL", mSignalBufferDistributor ) ) + , mRawBufferManagerSpy( std::make_shared>( + RawData::BufferManagerConfig::create().get() ) ) + { + mDictionary->customDecoderMethod["NAMED_SIGNAL"]["Vehicle.MultiRisingEdgeTrigger"] = + CustomSignalDecoderFormat{ "NAMED_SIGNAL", "Vehicle.MultiRisingEdgeTrigger", 1, SignalType::STRING }; + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + mSignalBufferDistributor->registerQueue( mSignalBuffer ); + mRawBufferManagerSpy->updateConfig( { { 1, { 1, "", "" } } } ); + } + void + SetUp() override + { + } + std::shared_ptr mDictionary; + std::shared_ptr mSignalBuffer; + std::shared_ptr mSignalBufferDistributor; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr> mRawBufferManagerSpy; +}; + +TEST_F( CustomFunctionMultiRisingEdgeTriggerTest, wrongArgs ) +{ + CustomFunctionMultiRisingEdgeTrigger customFunc( mNamedSignalDataSource, mRawBufferManagerSpy ); + std::vector args; + std::unordered_set collectedSignalIds = { 1 }; + CollectionInspectionEngineOutput collectedData; + collectedData.triggeredCollectionSchemeData = std::make_shared(); + + // Try no args + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Try bad datatype: + args.resize( 4 ); + args[0] = -1; + args[1] = false; + args[2] = "def"; + args[3] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Successfully add an initial value: + args[0] = "abc"; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Change the datatype: + args[0] = -1; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Change the number of args: + args[0] = "abc"; + args.resize( 2 ); + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::TYPE_MISMATCH ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + customFunc.cleanup( 1 ); +} + +TEST_F( CustomFunctionMultiRisingEdgeTriggerTest, multiTrigger ) +{ + CustomFunctionMultiRisingEdgeTrigger customFunc( mNamedSignalDataSource, mRawBufferManagerSpy ); + std::vector args; + std::unordered_set collectedSignalIds = { 1 }; + CollectionInspectionEngineOutput collectedData; + collectedData.triggeredCollectionSchemeData = std::make_shared(); + + // Undefined initial value: + args.resize( 4 ); + args[0] = "abc"; + args[2] = "def"; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Initial value: + args.resize( 4 ); + args[1] = false; + args[3] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // First rising edge: + args[1] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 1 ); + auto &signal = collectedData.triggeredCollectionSchemeData->signals[0]; + auto loanedFrame = mRawBufferManagerSpy->borrowFrame( signal.signalID, signal.value.value.uint32Val ); + ASSERT_FALSE( loanedFrame.isNull() ); + std::string collectedStringVal; + collectedStringVal.assign( reinterpret_cast( loanedFrame.getData() ), loanedFrame.getSize() ); + EXPECT_EQ( collectedStringVal, "[\"abc\"]" ); + + // Falling edge: + args[3] = false; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + collectedData.triggeredCollectionSchemeData->signals.clear(); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Second rising edge: + args[3] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 1 ); + signal = collectedData.triggeredCollectionSchemeData->signals[0]; + loanedFrame = mRawBufferManagerSpy->borrowFrame( signal.signalID, signal.value.value.uint32Val ); + ASSERT_FALSE( loanedFrame.isNull() ); + collectedStringVal.assign( reinterpret_cast( loanedFrame.getData() ), loanedFrame.getSize() ); + EXPECT_EQ( collectedStringVal, "[\"def\"]" ); + + // Falling edge on both + args[1] = false; + args[3] = false; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + collectedData.triggeredCollectionSchemeData->signals.clear(); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Rising edge on both: + args[1] = true; + args[3] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 1 ); + signal = collectedData.triggeredCollectionSchemeData->signals[0]; + loanedFrame = mRawBufferManagerSpy->borrowFrame( signal.signalID, signal.value.value.uint32Val ); + ASSERT_FALSE( loanedFrame.isNull() ); + collectedStringVal.assign( reinterpret_cast( loanedFrame.getData() ), loanedFrame.getSize() ); + EXPECT_EQ( collectedStringVal, "[\"abc\",\"def\"]" ); + + // Falling edge on both: + args[1] = false; + args[3] = false; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + collectedData.triggeredCollectionSchemeData->signals.clear(); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Signal not collected: + collectedSignalIds.erase( 1 ); + + // Rising edge: + args[1] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + collectedData.triggeredCollectionSchemeData->signals.clear(); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Collect signal again: + collectedSignalIds.emplace( 1 ); + // Remove raw buffer manager config: + mRawBufferManagerSpy->updateConfig( {} ); + + // Rising edge: + args[1] = false; + args[3] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // Add raw buffer config again: + mRawBufferManagerSpy->updateConfig( { { 1, { 1, "", "" } } } ); + // Change of decoder manifest removes custom decoders + mDictionary->customDecoderMethod.clear(); + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + // Rising edge: + args[1] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + customFunc.cleanup( 1 ); +} + +TEST_F( CustomFunctionMultiRisingEdgeTriggerTest, noNamedSignalDataSource ) +{ + CustomFunctionMultiRisingEdgeTrigger customFunc( nullptr, mRawBufferManagerSpy ); + std::vector args; + std::unordered_set collectedSignalIds = { 1 }; + CollectionInspectionEngineOutput collectedData; + collectedData.triggeredCollectionSchemeData = std::make_shared(); + + // Successfully add an initial value: + args.resize( 4 ); + args[0] = "abc"; + args[1] = false; + args[2] = "def"; + args[3] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + // First rising edge: + args[1] = true; + ASSERT_EQ( customFunc.invoke( 1, args ).error, ExpressionErrorCode::SUCCESSFUL ); + customFunc.conditionEnd( collectedSignalIds, 0, collectedData ); + ASSERT_EQ( collectedData.triggeredCollectionSchemeData->signals.size(), 0 ); + + customFunc.cleanup( 1 ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/DataFetchManagerTest.cpp b/test/unit/DataFetchManagerTest.cpp new file mode 100644 index 00000000..423e2f66 --- /dev/null +++ b/test/unit/DataFetchManagerTest.cpp @@ -0,0 +1,191 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "DataFetchManager.h" +#include "CollectionInspectionAPITypes.h" +#include "DataFetchManagerAPITypes.h" +#include "SignalTypes.h" +#include "WaitUntil.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Testing +{ + +class DataFetchManagerTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + mDataFetchManager = std::make_unique(); + } + + void + TearDown() override + { + mDataFetchManager.reset(); + } + + std::unique_ptr mDataFetchManager; +}; + +TEST_F( DataFetchManagerTest, TestNoFetchMatrix ) +{ + mDataFetchManager->start(); + + bool functionCalled = false; + mDataFetchManager->registerCustomFetchFunction( + "testFunction", [&functionCalled]( SignalID, FetchRequestID, const std::vector & ) { + functionCalled = true; + return FetchErrorCode::SUCCESSFUL; + } ); + + mDataFetchManager->onFetchRequest( 1, true ); + ASSERT_FALSE( functionCalled ); + mDataFetchManager->stop(); +} + +TEST_F( DataFetchManagerTest, TestUnknownFetchFunction ) +{ + mDataFetchManager->start(); + + bool functionCalled = false; + mDataFetchManager->registerCustomFetchFunction( + "testFunction", [&functionCalled]( SignalID, FetchRequestID, const std::vector & ) { + functionCalled = true; + return FetchErrorCode::SUCCESSFUL; + } ); + + auto fetchMatrix = std::make_shared(); + std::vector &fetchRequests = + fetchMatrix->fetchRequests.emplace( 1, std::vector() ).first->second; + + fetchRequests.emplace_back(); + auto &fetchRequest = fetchRequests.back(); + fetchRequest.signalID = 1; + fetchRequest.functionName = "unknownFunction"; + fetchRequest.args.emplace_back(); + mDataFetchManager->onChangeFetchMatrix( fetchMatrix ); + + mDataFetchManager->onFetchRequest( 1, true ); + + ASSERT_FALSE( functionCalled ); + mDataFetchManager->stop(); +} + +TEST_F( DataFetchManagerTest, TestNoActionsSet ) +{ + mDataFetchManager->start(); + + bool functionCalled = false; + mDataFetchManager->registerCustomFetchFunction( + "testFunction", [&functionCalled]( SignalID, FetchRequestID, const std::vector & ) { + functionCalled = true; + return FetchErrorCode::SUCCESSFUL; + } ); + + auto fetchMatrix = std::make_shared(); + fetchMatrix->fetchRequests.emplace( 1, std::vector() ).first->second; + + mDataFetchManager->onChangeFetchMatrix( fetchMatrix ); + mDataFetchManager->onFetchRequest( 1, true ); + + ASSERT_FALSE( functionCalled ); + mDataFetchManager->stop(); +} + +TEST_F( DataFetchManagerTest, TestUnknownFetchRequestID ) +{ + mDataFetchManager->start(); + + bool functionCalled = false; + mDataFetchManager->registerCustomFetchFunction( + "testFunction", [&functionCalled]( SignalID, FetchRequestID, const std::vector & ) { + functionCalled = true; + return FetchErrorCode::SUCCESSFUL; + } ); + + auto fetchMatrix = std::make_shared(); + std::vector &fetchRequests = + fetchMatrix->fetchRequests.emplace( 1, std::vector() ).first->second; + fetchRequests.emplace_back(); + auto &fetchRequest = fetchRequests.back(); + fetchRequest.signalID = 1; + fetchRequest.functionName = "testFunction"; + fetchRequest.args.emplace_back(); + mDataFetchManager->onChangeFetchMatrix( fetchMatrix ); + + mDataFetchManager->onFetchRequest( 2, true ); + + ASSERT_FALSE( functionCalled ); + mDataFetchManager->stop(); +} + +TEST_F( DataFetchManagerTest, TestRegisterCustomFetchFunction ) +{ + mDataFetchManager->start(); + + bool functionCalled = false; + mDataFetchManager->registerCustomFetchFunction( + "testFunction", [&functionCalled]( SignalID, FetchRequestID, const std::vector & ) { + functionCalled = true; + return FetchErrorCode::SUCCESSFUL; + } ); + + auto fetchMatrix = std::make_shared(); + std::vector &fetchRequests = + fetchMatrix->fetchRequests.emplace( 1, std::vector() ).first->second; + fetchRequests.emplace_back(); + auto &fetchRequest = fetchRequests.back(); + fetchRequest.signalID = 1; + fetchRequest.functionName = "testFunction"; + fetchRequest.args.emplace_back(); + mDataFetchManager->onChangeFetchMatrix( fetchMatrix ); + + mDataFetchManager->onFetchRequest( 1, true ); + + WAIT_ASSERT_TRUE( functionCalled ); + mDataFetchManager->stop(); +} + +TEST_F( DataFetchManagerTest, TestPeriodicFetchRequest ) +{ + mDataFetchManager->start(); + + int callCount = 0; + mDataFetchManager->registerCustomFetchFunction( + "testFunction", [&callCount]( SignalID, FetchRequestID, const std::vector & ) { + callCount++; + return FetchErrorCode::SUCCESSFUL; + } ); + auto fetchMatrix = std::make_shared(); + std::vector &fetchRequests = + fetchMatrix->fetchRequests.emplace( 1, std::vector() ).first->second; + fetchRequests.emplace_back(); + auto &fetchRequest = fetchRequests.back(); + fetchRequest.signalID = 1; + fetchRequest.functionName = "testFunction"; + fetchRequest.args.emplace_back(); + fetchMatrix->periodicalFetchRequestSetup[1] = { + 100, 3, 1000 }; // execute every 100ms, max 3 executions, reset every 1000ms + mDataFetchManager->onChangeFetchMatrix( fetchMatrix ); + // TODO: max executions and reset interval parameters are not yet supported by the cloud and are ignored on edge + // Expect function calls every 100ms + std::this_thread::sleep_for( std::chrono::milliseconds( 450 ) ); + ASSERT_EQ( callCount, 5 ); + mDataFetchManager->stop(); +} + +} // namespace Testing +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/DataSenderManagerTest.cpp b/test/unit/DataSenderManagerTest.cpp index 422a0bc2..81078f52 100644 --- a/test/unit/DataSenderManagerTest.cpp +++ b/test/unit/DataSenderManagerTest.cpp @@ -46,6 +46,17 @@ #include #include #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS +#include "CommandResponseDataSender.h" +#include "CommandTypes.h" +#include "ICommandDispatcher.h" +#include "command_response.pb.h" +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateDataSender.h" +#include "LastKnownStateTypes.h" +#include "last_known_state_data.pb.h" +#endif namespace Aws { @@ -87,8 +98,8 @@ class DataSenderManagerTest : public ::testing::Test EXPECT_CALL( *mMqttSender, getMaxSendSize() ) .Times( AnyNumber() ) .WillRepeatedly( Return( MAXIMUM_PAYLOAD_SIZE ) ); - ON_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ) - .WillByDefault( InvokeArgument<2>( ConnectivityError::Success ) ); + ON_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ) + .WillByDefault( InvokeArgument<3>( ConnectivityError::Success ) ); auto mProtoWriter = std::make_shared( mCANIDTranslator, mRawDataBufferManager ); auto telemetryDataSender = std::make_shared( @@ -110,6 +121,22 @@ class DataSenderManagerTest : public ::testing::Test std::make_shared( mUploadedS3Objects, mS3Sender, mIonWriter, "" ); dataSenders[SenderDataType::VISION_SYSTEM] = mVisionSystemDataSender; #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + mCommandResponseSender = std::make_shared>(); + EXPECT_CALL( *mCommandResponseSender, isAlive() ).Times( AnyNumber() ).WillRepeatedly( Return( true ) ); + ON_CALL( *mCommandResponseSender, mockedSendBuffer( _, _, _, _ ) ) + .WillByDefault( InvokeArgument<3>( ConnectivityError::Success ) ); + mCommandResponseDataSender = std::make_shared( mCommandResponseSender ); + dataSenders[SenderDataType::COMMAND_RESPONSE] = mCommandResponseDataSender; +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + mLastKnownStateMqttSender = std::make_shared>(); + ON_CALL( *mLastKnownStateMqttSender, mockedSendBuffer( mLastKnownStateDataTopic, _, _, _ ) ) + .WillByDefault( InvokeArgument<3>( ConnectivityError::Success ) ); + mLastKnownStateDataSender = + std::make_shared( mLastKnownStateMqttSender, mMaxMessagesPerPayload ); + dataSenders[SenderDataType::LAST_KNOWN_STATE] = mLastKnownStateDataSender; +#endif mDataSenderManager = std::make_unique( std::move( dataSenders ), mMqttSender, mPayloadManager ); @@ -119,19 +146,31 @@ class DataSenderManagerTest : public ::testing::Test TearDown() override { } + void processCollectedData( std::shared_ptr data ) { mDataSenderManager->processData( std::const_pointer_cast( data ) ); } +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::string + getCommandResponseTopic( const std::string &commandID ) const + { + return "$aws/commands/things/thing-name/executions/" + commandID + "/response/protobuf"; + } +#endif + protected: static constexpr unsigned MAXIMUM_PAYLOAD_SIZE = 400; static constexpr unsigned CAN_DATA_SIZE = 8; PayloadAdaptionConfig mPayloadAdaptionConfigUncompressed{ 80, 70, 90, 10 }; PayloadAdaptionConfig mPayloadAdaptionConfigCompressed{ 80, 70, 90, 10 }; + unsigned mMaxMessagesPerPayload{ 5 }; // Only used by LastKnownStateDataSender + unsigned mCanChannelID{ 0 }; std::shared_ptr mTriggeredCollectionSchemeData; + std::string mTelemetryDataTopic = "$aws/iotfleetwise/vehicles/thing-name/signals"; std::shared_ptr> mMqttSender; std::shared_ptr mPersistency; std::shared_ptr mPayloadManager; @@ -146,13 +185,52 @@ class DataSenderManagerTest : public ::testing::Test std::shared_ptr mActiveCollectionSchemes; std::shared_ptr mUploadedS3Objects; #endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::shared_ptr> mCommandResponseSender; + std::shared_ptr mCommandResponseDataSender; +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + std::string mLastKnownStateDataTopic = "$aws/iotfleetwise/vehicles/thing-name/last_known_states/data"; + std::shared_ptr> mLastKnownStateMqttSender; + std::shared_ptr mLastKnownStateDataSender; +#endif }; +template +std::vector +getSignalValues( const T &signals ) +{ + std::vector signalValues; + for ( const auto &signal : signals ) + { + signalValues.push_back( signal.double_value() ); + } + return signalValues; +} + +template +std::vector +getSignalIds( const T &signals ) +{ + std::vector signalIds; + for ( const auto &signal : signals ) + { + signalIds.push_back( signal.signal_id() ); + } + return signalIds; +} + TEST_F( DataSenderManagerTest, senderDataTypeToString ) { EXPECT_EQ( senderDataTypeToString( SenderDataType::TELEMETRY ), "Telemetry" ); #ifdef FWE_FEATURE_VISION_SYSTEM_DATA EXPECT_EQ( senderDataTypeToString( SenderDataType::VISION_SYSTEM ), "VisionSystem" ); +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + EXPECT_EQ( senderDataTypeToString( SenderDataType::COMMAND_RESPONSE ), "CommandResponse" ); +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + EXPECT_EQ( senderDataTypeToString( SenderDataType::LAST_KNOWN_STATE ), "LastKnownState" ); #endif EXPECT_EQ( senderDataTypeToString( static_cast( -1 ) ), "" ); } @@ -165,13 +243,21 @@ TEST_F( DataSenderManagerTest, stringToSenderDataType ) #ifdef FWE_FEATURE_VISION_SYSTEM_DATA EXPECT_TRUE( stringToSenderDataType( "VisionSystem", output ) ); EXPECT_EQ( output, SenderDataType::VISION_SYSTEM ); +#endif +#ifdef FWE_FEATURE_REMOTE_COMMANDS + EXPECT_TRUE( stringToSenderDataType( "CommandResponse", output ) ); + EXPECT_EQ( output, SenderDataType::COMMAND_RESPONSE ); +#endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + EXPECT_TRUE( stringToSenderDataType( "LastKnownState", output ) ); + EXPECT_EQ( output, SenderDataType::LAST_KNOWN_STATE ); #endif EXPECT_FALSE( stringToSenderDataType( "Invalid", output ) ); } TEST_F( DataSenderManagerTest, ProcessEmptyData ) { - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); // It should just not crash processCollectedData( nullptr ); @@ -182,12 +268,12 @@ TEST_F( DataSenderManagerTest, ProcessSingleSignal ) auto signal1 = CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ); mTriggeredCollectionSchemeData->signals.push_back( signal1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -214,12 +300,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleSignals ) mTriggeredCollectionSchemeData->signals.push_back( signal2 ); mTriggeredCollectionSchemeData->signals.push_back( signal3 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -263,14 +349,14 @@ TEST_F( DataSenderManagerTest, ProcessMultipleSignalsBeyondTransmitThreshold ) CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ) }; mTriggeredCollectionSchemeData->signals = signals; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) .Times( 2 ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::Success ) ); + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::Success ) ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 2 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 2 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -302,12 +388,12 @@ TEST_F( DataSenderManagerTest, ProcessSingleCanFrame ) auto canFrame1 = CollectedCanRawFrame( 0x380, mCanChannelID, 789654, canBuf1, CAN_DATA_SIZE ); mTriggeredCollectionSchemeData->canFrames.push_back( canFrame1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -340,12 +426,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleCanFrames ) mTriggeredCollectionSchemeData->canFrames.push_back( canFrame2 ); mTriggeredCollectionSchemeData->canFrames.push_back( canFrame3 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -394,12 +480,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleCanFramesBeyondTransmitThreshold ) CollectedCanRawFrame( 0x380, mCanChannelID, 789654, canBuf1, CAN_DATA_SIZE ) }; mTriggeredCollectionSchemeData->canFrames = canFrames; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 2 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 2 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 2 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 2 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -433,12 +519,12 @@ TEST_F( DataSenderManagerTest, ProcessSingleDtcCode ) dtcInfo.mDTCCodes.emplace_back( "P0143" ); mTriggeredCollectionSchemeData->mDTCInfo = dtcInfo; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -465,12 +551,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleDtcCodes ) dtcInfo.mDTCCodes.emplace_back( "C0196" ); mTriggeredCollectionSchemeData->mDTCInfo = dtcInfo; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -508,12 +594,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleDtcCodesBeyondTransmitThreshold ) dtcInfo.mDTCCodes = dtcCodes; mTriggeredCollectionSchemeData->mDTCInfo = dtcInfo; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 2 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 2 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 2 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 2 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -547,12 +633,12 @@ TEST_F( DataSenderManagerTest, ProcessSingleUploadedS3Object ) auto uploadedS3Object1 = UploadedS3Object{ "uploaded/object/key1", UploadedS3ObjectDataFormat::Cdr }; mTriggeredCollectionSchemeData->uploadedS3Objects.push_back( uploadedS3Object1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -580,12 +666,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleUploadedS3Objects ) mTriggeredCollectionSchemeData->uploadedS3Objects.push_back( uploadedS3Object2 ); mTriggeredCollectionSchemeData->uploadedS3Objects.push_back( uploadedS3Object3 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -628,12 +714,12 @@ TEST_F( DataSenderManagerTest, ProcessMultipleUploadedS3ObjectsBeyondTransmitThr }; mTriggeredCollectionSchemeData->uploadedS3Objects = uploadedS3Objects; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 2 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 2 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 2 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 2 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -666,7 +752,7 @@ TEST_F( DataSenderManagerTest, ProcessRawDataSignalNoActiveCampaigns ) auto signal1 = CollectedSignal( 1234, 789654, 10000, SignalType::COMPLEX_SIGNAL ); mTriggeredVisionSystemData->signals.push_back( signal1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, setupVehicleData( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, mockedAppend( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, getStreambufBuilder() ).Times( 0 ); @@ -676,7 +762,7 @@ TEST_F( DataSenderManagerTest, ProcessRawDataSignalNoActiveCampaigns ) TEST_F( DataSenderManagerTest, ProcessVisionSystemDataWithoutRawData ) { - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, setupVehicleData( _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, mockedAppend( _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, getStreambufBuilder() ).Times( 0 ); @@ -698,7 +784,7 @@ TEST_F( DataSenderManagerTest, ProcessSingleRawDataSignal ) std::make_shared( "TESTCOLLECTIONSCHEME", "TESTDECODERID", 0, 10, s3UploadMetadata ); mActiveCollectionSchemes->activeCollectionSchemes.push_back( collectionScheme1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, setupVehicleData( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, mockedAppend( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, getStreambufBuilder() ).WillOnce( []() { @@ -760,7 +846,7 @@ TEST_F( DataSenderManagerTest, ProcessMultipleRawDataSignals ) std::make_shared( "TESTCOLLECTIONSCHEME", "TESTDECODERID", 0, 10, s3UploadMetadata ); mActiveCollectionSchemes->activeCollectionSchemes.push_back( collectionScheme1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, setupVehicleData( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, mockedAppend( _ ) ).Times( 2 ); EXPECT_CALL( *mIonWriter, getStreambufBuilder() ).WillOnce( []() { @@ -813,7 +899,7 @@ TEST_F( DataSenderManagerTest, ProcessRawDataSignalFailure ) std::make_shared( "TESTCOLLECTIONSCHEME", "TESTDECODERID", 0, 10, s3UploadMetadata ); mActiveCollectionSchemes->activeCollectionSchemes.push_back( collectionScheme1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, setupVehicleData( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, mockedAppend( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, getStreambufBuilder() ).WillOnce( []() { @@ -853,7 +939,7 @@ TEST_F( DataSenderManagerTest, ProcessRawDataSignalFailureWithPersistency ) mActiveCollectionSchemes->activeCollectionSchemes.push_back( collectionScheme1 ); std::string payload = "fake ion file"; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); EXPECT_CALL( *mIonWriter, setupVehicleData( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, mockedAppend( _ ) ).Times( 1 ); EXPECT_CALL( *mIonWriter, getStreambufBuilder() ).WillOnce( [payload]() { @@ -900,7 +986,7 @@ TEST_F( DataSenderManagerTest, ProcessSingleSignalWithoutRawData ) std::make_shared( "TESTCOLLECTIONSCHEME", "TESTDECODERID", 0, 10, s3UploadMetadata ); mActiveCollectionSchemes->activeCollectionSchemes.push_back( collectionScheme1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); EXPECT_CALL( *mS3Sender, sendStream( _, _, _, _ ) ).Times( 0 ); mVisionSystemDataSender->onChangeCollectionSchemeList( mActiveCollectionSchemes ); @@ -909,7 +995,7 @@ TEST_F( DataSenderManagerTest, ProcessSingleSignalWithoutRawData ) std::shared_ptr senderData; ASSERT_FALSE( mUploadedS3Objects->pop( senderData ) ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); } #endif @@ -919,12 +1005,12 @@ TEST_F( DataSenderManagerTest, ProcessSingleSignalWithCompression ) mTriggeredCollectionSchemeData->signals.push_back( signal1 ); mTriggeredCollectionSchemeData->metadata.compress = true; - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; std::string uncompressedData; @@ -946,7 +1032,7 @@ TEST_F( DataSenderManagerTest, ProcessSingleSignalWithCompression ) TEST_F( DataSenderManagerTest, PersistencyNoFiles ) { - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); mDataSenderManager->checkAndSendRetrievedData(); } @@ -967,7 +1053,7 @@ TEST_F( DataSenderManagerTest, PersistencyUnsupportedFile ) mPayloadManager->storeData( reinterpret_cast( payload.data() ), payload.size(), metadata, filename ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); mDataSenderManager->checkAndSendRetrievedData(); } @@ -986,7 +1072,7 @@ TEST_F( DataSenderManagerTest, PersistencyMissingPayloadFile ) mPersistency->addMetadata( metadata ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, _, _ ) ).Times( 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, _, _ ) ).Times( 0 ); mDataSenderManager->checkAndSendRetrievedData(); } @@ -997,8 +1083,8 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryDisabled ) auto signal1 = CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ); mTriggeredCollectionSchemeData->signals.push_back( signal1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::TransmissionError ) ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::TransmissionError ) ); processCollectedData( mTriggeredCollectionSchemeData ); @@ -1006,7 +1092,7 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryDisabled ) mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); } TEST_F( DataSenderManagerTest, PersistencyForTelemetryLegacyMetadata ) @@ -1022,22 +1108,22 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryLegacyMetadata ) mPayloadManager->storeData( reinterpret_cast( payload.data() ), payload.size(), metadata, filename ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); ASSERT_EQ( sentBufferData[0].data, payload ); // Ensure that there is no more data persisted mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); } TEST_F( DataSenderManagerTest, PersistencyForTelemetrySingleFile ) @@ -1046,20 +1132,20 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetrySingleFile ) auto signal1 = CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ); mTriggeredCollectionSchemeData->signals.push_back( signal1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::TransmissionError ) ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::TransmissionError ) ); processCollectedData( mTriggeredCollectionSchemeData ); mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::Success ) ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -1078,11 +1164,11 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetrySingleFile ) // Ensure that there is no more data persisted mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); } TEST_F( DataSenderManagerTest, PersistencyKeepFilesWhenRestarting ) @@ -1092,8 +1178,8 @@ TEST_F( DataSenderManagerTest, PersistencyKeepFilesWhenRestarting ) auto signal1 = CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ); mTriggeredCollectionSchemeData->signals.push_back( signal1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::TransmissionError ) ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::TransmissionError ) ); processCollectedData( mTriggeredCollectionSchemeData ); @@ -1110,14 +1196,14 @@ TEST_F( DataSenderManagerTest, PersistencyKeepFilesWhenRestarting ) mDataSenderManager = std::make_unique( dataSenders, mMqttSender, mPayloadManager ); mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::Success ) ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -1136,11 +1222,11 @@ TEST_F( DataSenderManagerTest, PersistencyKeepFilesWhenRestarting ) // Ensure that there is no more data persisted mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); } TEST_F( DataSenderManagerTest, PersistencyForTelemetryPersistAgainOnFailure ) @@ -1149,29 +1235,29 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryPersistAgainOnFailure ) auto signal1 = CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ); mTriggeredCollectionSchemeData->signals.push_back( signal1 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::NoConnection ) ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::NoConnection ) ); processCollectedData( mTriggeredCollectionSchemeData ); mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::TransmissionError ) ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::TransmissionError ) ); mDataSenderManager->checkAndSendRetrievedData(); mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); // Now the next attempt succeeds - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillOnce( InvokeArgument<2>( ConnectivityError::Success ) ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 1 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -1190,11 +1276,11 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryPersistAgainOnFailure ) // Ensure that there is no more data persisted mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); } TEST_F( DataSenderManagerTest, PersistencyForTelemetryMultipleFiles ) @@ -1213,22 +1299,22 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryMultipleFiles ) estimatedSize += 20; } - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) .Times( 2 ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::NoConnection ) ); + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::NoConnection ) ); processCollectedData( mTriggeredCollectionSchemeData ); mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) .Times( 2 ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::Success ) ); + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::Success ) ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 2 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 2 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; ASSERT_TRUE( vehicleData.ParseFromString( sentBufferData[0].data ) ); @@ -1268,11 +1354,11 @@ TEST_F( DataSenderManagerTest, PersistencyForTelemetryMultipleFiles ) // Ensure that there is no more data persisted mMqttSender->clearSentBufferData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); mDataSenderManager->checkAndSendRetrievedData(); - ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 0 ); } TEST_F( DataSenderManagerTest, splitAndDecreaseThresholdWhenOverLimit ) @@ -1287,14 +1373,14 @@ TEST_F( DataSenderManagerTest, splitAndDecreaseThresholdWhenOverLimit ) mTriggeredCollectionSchemeData->signals.push_back( CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ) ); } - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) .Times( AtLeast( 12 ) ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::Success ) ); + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::Success ) ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_GE( mMqttSender->getSentBufferData().size(), 12 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_GE( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 12 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; @@ -1349,14 +1435,14 @@ TEST_F( DataSenderManagerTest, increaseThresholdWhenBelowLimit ) mTriggeredCollectionSchemeData->signals.push_back( CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ) ); } - EXPECT_CALL( *mMqttSender, mockedSendBuffer( _, Gt( 0 ), _ ) ) + EXPECT_CALL( *mMqttSender, mockedSendBuffer( mTelemetryDataTopic, _, Gt( 0 ), _ ) ) .Times( AtLeast( 9 ) ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::Success ) ); + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::Success ) ); processCollectedData( mTriggeredCollectionSchemeData ); - ASSERT_GE( mMqttSender->getSentBufferData().size(), 9 ); - auto sentBufferData = mMqttSender->getSentBufferData(); + ASSERT_GE( mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ).size(), 9 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( mTelemetryDataTopic ); Schemas::VehicleDataMsg::VehicleData vehicleData; // Increasing threshold: @@ -1389,5 +1475,351 @@ TEST_F( DataSenderManagerTest, increaseThresholdWhenBelowLimit ) EXPECT_EQ( vehicleData.captured_signals_size(), 29 ); } +#ifdef FWE_FEATURE_REMOTE_COMMANDS +/** + * Helper struct to map our internal status enum to the proto enum and use it in parameterized tests + */ +struct InternalStatusToProto +{ + CommandStatus internalStatus; + Schemas::Commands::Status protoStatus; +}; + +inline std::string +statusToString( const testing::TestParamInfo &info ) +{ + return commandStatusToString( info.param.internalStatus ); +} + +class DataSenderManagerTestWithAllCommandStatuses : public DataSenderManagerTest, + public testing::WithParamInterface +{ +}; + +INSTANTIATE_TEST_SUITE_P( + AllCommandStatuses, + DataSenderManagerTestWithAllCommandStatuses, + ::testing::Values( + InternalStatusToProto{ CommandStatus::SUCCEEDED, Schemas::Commands::COMMAND_STATUS_SUCCEEDED }, + InternalStatusToProto{ CommandStatus::EXECUTION_TIMEOUT, Schemas::Commands::COMMAND_STATUS_EXECUTION_TIMEOUT }, + InternalStatusToProto{ CommandStatus::EXECUTION_FAILED, Schemas::Commands::COMMAND_STATUS_EXECUTION_FAILED }, + InternalStatusToProto{ CommandStatus::IN_PROGRESS, Schemas::Commands::COMMAND_STATUS_IN_PROGRESS } ), + statusToString ); + +TEST_P( DataSenderManagerTestWithAllCommandStatuses, ProcessSingleCommandResponse ) +{ + auto status = GetParam(); + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, Gt( 0 ), _ ) ) + .Times( 1 ); + + mDataSenderManager->processData( + std::make_shared( "command123", status.internalStatus, 0x1234, "status456" ) ); + + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 1 ); + auto sentBufferData = mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ); + + Schemas::Commands::CommandResponse commandResponse; + ASSERT_TRUE( commandResponse.ParseFromString( sentBufferData[0].data ) ); + + ASSERT_EQ( commandResponse.command_id(), "command123" ); + ASSERT_EQ( commandResponse.status(), status.protoStatus ); + ASSERT_EQ( commandResponse.reason_code(), 0x1234 ); + ASSERT_EQ( commandResponse.reason_description(), "status456" ); +} + +TEST_P( DataSenderManagerTestWithAllCommandStatuses, PersistencyForCommandResponse ) +{ + auto status = GetParam(); + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::NoConnection ) ); + + mDataSenderManager->processData( + std::make_shared( "command123", status.internalStatus, 0x1234, "status456" ) ); + mCommandResponseSender->clearSentBufferData(); + + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); + mDataSenderManager->checkAndSendRetrievedData(); + + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 1 ); + auto sentBufferData = mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ); + + Schemas::Commands::CommandResponse commandResponse; + ASSERT_TRUE( commandResponse.ParseFromString( sentBufferData[0].data ) ); + + ASSERT_EQ( commandResponse.command_id(), "command123" ); + ASSERT_EQ( commandResponse.status(), status.protoStatus ); + ASSERT_EQ( commandResponse.reason_code(), 0x1234 ); + ASSERT_EQ( commandResponse.reason_description(), "status456" ); +} + +TEST_F( DataSenderManagerTest, PersistencyForCommandResponsePersistAgainOnFailure ) +{ + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::NoConnection ) ); + + mDataSenderManager->processData( + std::make_shared( "command123", CommandStatus::EXECUTION_FAILED, 0x1234, "status456" ) ); + + mCommandResponseSender->clearSentBufferData(); + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 0 ); + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::TransmissionError ) ); + + mDataSenderManager->checkAndSendRetrievedData(); + + mCommandResponseSender->clearSentBufferData(); + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 0 ); + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); + + mDataSenderManager->checkAndSendRetrievedData(); + + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 1 ); + auto sentBufferData = mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ); + + Schemas::Commands::CommandResponse commandResponse; + ASSERT_TRUE( commandResponse.ParseFromString( sentBufferData[0].data ) ); + + ASSERT_EQ( commandResponse.command_id(), "command123" ); + ASSERT_EQ( commandResponse.status(), Schemas::Commands::COMMAND_STATUS_EXECUTION_FAILED ); + ASSERT_EQ( commandResponse.reason_code(), 0x1234 ); + ASSERT_EQ( commandResponse.reason_description(), "status456" ); +} + +TEST_F( DataSenderManagerTest, InvalidSenderWhenProcessingCommand ) +{ + std::unordered_map> dataSenders; + mDataSenderManager = std::make_unique( std::move( dataSenders ), mMqttSender, mPayloadManager ); + + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, _, _ ) ) + .Times( 0 ); + + mDataSenderManager->processData( + std::make_shared( "command123", CommandStatus::SUCCEEDED, 0x1234, "status456" ) ); + + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 0 ); +} +#endif + +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +class DataSenderManagerTestWithAllSignalTypes : public DataSenderManagerTest, + public testing::WithParamInterface +{ +}; + +INSTANTIATE_TEST_SUITE_P( AllSignalTypes, + DataSenderManagerTestWithAllSignalTypes, + allSignalTypes, + signalTypeParamInfoToString ); + +void +assertLastKnownStateSignalValue( const Schemas::LastKnownState::CapturedSignal &capturedSignal, + double expectedSignalValue, + SignalType expectedSignalType ) +{ + switch ( expectedSignalType ) + { + case SignalType::UINT8: + ASSERT_EQ( capturedSignal.uint8_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::INT8: + ASSERT_EQ( capturedSignal.int8_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::UINT16: + ASSERT_EQ( capturedSignal.uint16_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::INT16: + ASSERT_EQ( capturedSignal.int16_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::UINT32: + ASSERT_EQ( capturedSignal.uint32_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::INT32: + ASSERT_EQ( capturedSignal.int32_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::UINT64: + ASSERT_EQ( capturedSignal.uint64_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::INT64: + ASSERT_EQ( capturedSignal.int64_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::FLOAT: + ASSERT_EQ( capturedSignal.float_value(), static_cast( expectedSignalValue ) ); + break; + case SignalType::DOUBLE: + ASSERT_EQ( capturedSignal.double_value(), expectedSignalValue ); + break; + case SignalType::BOOLEAN: + ASSERT_EQ( capturedSignal.boolean_value(), static_cast( expectedSignalValue ) ); + break; + default: + FAIL() << "Unsupported signal type"; + } +} + +bool +parseLastKnownStateData( const std::string &data, Schemas::LastKnownState::LastKnownStateData *lastKnownStateData ) +{ + std::string uncompressedData; + snappy::Uncompress( data.c_str(), data.size(), &uncompressedData ); + return lastKnownStateData->ParseFromString( uncompressedData ); +} + +TEST_P( DataSenderManagerTestWithAllSignalTypes, ProcessSingleLastKnownStateSignal ) +{ + auto signalType = GetParam(); + EXPECT_CALL( *mLastKnownStateMqttSender, mockedSendBuffer( mLastKnownStateDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); + + auto collectedData = std::make_shared(); + collectedData->triggerTime = 1000; + collectedData->stateTemplateCollectedSignals.emplace_back( + StateTemplateCollectedSignals{ "lks1", { { CollectedSignal( 1234, 789654, 40, signalType ) } } } ); + mDataSenderManager->processData( collectedData ); + + ASSERT_EQ( mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ).size(), 1 ); + auto sentBufferData = mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ); + + Schemas::LastKnownState::LastKnownStateData lastKnownStateProto; + ASSERT_TRUE( parseLastKnownStateData( sentBufferData[0].data, &lastKnownStateProto ) ); + + ASSERT_EQ( lastKnownStateProto.collection_event_time_ms_epoch(), 1000 ); + ASSERT_EQ( lastKnownStateProto.captured_state_template_signals_size(), 1 ); + auto &capturedStateTemplateSignals = lastKnownStateProto.captured_state_template_signals()[0]; + ASSERT_EQ( capturedStateTemplateSignals.state_template_sync_id(), "lks1" ); + ASSERT_EQ( capturedStateTemplateSignals.captured_signals_size(), 1 ); + + ASSERT_NO_FATAL_FAILURE( + assertLastKnownStateSignalValue( capturedStateTemplateSignals.captured_signals()[0], 40, signalType ) ); + ASSERT_EQ( capturedStateTemplateSignals.captured_signals()[0].signal_id(), 1234 ); +} + +TEST_F( DataSenderManagerTest, ProcessMultipleLastKnownStateSignals ) +{ + EXPECT_CALL( *mLastKnownStateMqttSender, mockedSendBuffer( mLastKnownStateDataTopic, _, Gt( 0 ), _ ) ).Times( 1 ); + + auto collectedData = std::make_shared(); + collectedData->triggerTime = 1000; + collectedData->stateTemplateCollectedSignals.emplace_back( StateTemplateCollectedSignals{ + "lks1", + { { CollectedSignal( 1234, 789654, 40.5, SignalType::DOUBLE ) }, + { CollectedSignal( 5678, 789700, 97, SignalType::UINT16 ) } }, + } ); + mDataSenderManager->processData( collectedData ); + + ASSERT_EQ( mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ).size(), 1 ); + auto sentBufferData = mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ); + + Schemas::LastKnownState::LastKnownStateData lastKnownStateProto; + ASSERT_TRUE( parseLastKnownStateData( sentBufferData[0].data, &lastKnownStateProto ) ); + + ASSERT_EQ( lastKnownStateProto.collection_event_time_ms_epoch(), 1000 ); + ASSERT_EQ( lastKnownStateProto.captured_state_template_signals_size(), 1 ); + auto &capturedStateTemplateSignals = lastKnownStateProto.captured_state_template_signals()[0]; + ASSERT_EQ( capturedStateTemplateSignals.state_template_sync_id(), "lks1" ); + + ASSERT_EQ( capturedStateTemplateSignals.captured_signals_size(), 2 ); + + ASSERT_EQ( capturedStateTemplateSignals.captured_signals()[0].double_value(), 40.5 ); + ASSERT_EQ( capturedStateTemplateSignals.captured_signals()[0].signal_id(), 1234 ); + + ASSERT_EQ( capturedStateTemplateSignals.captured_signals()[1].uint16_value(), 97 ); + ASSERT_EQ( capturedStateTemplateSignals.captured_signals()[1].signal_id(), 5678 ); +} + +TEST_F( DataSenderManagerTest, ProcessMultipleLastKnownStateSignalsBeyondTransmitThreshold ) +{ + auto collectedData = std::make_shared(); + collectedData->triggerTime = 1000; + auto stateTemplate1 = StateTemplateCollectedSignals{ + "lks1", + { CollectedSignal( 1234, 788001, 41, SignalType::DOUBLE ), + CollectedSignal( 1234, 788002, 42, SignalType::DOUBLE ), + CollectedSignal( 1234, 788003, 43, SignalType::DOUBLE ), + CollectedSignal( 1234, 788004, 44, SignalType::DOUBLE ), + CollectedSignal( 1234, 788005, 45, SignalType::DOUBLE ), + CollectedSignal( 1234, 788006, 46, SignalType::DOUBLE ) }, + }; + collectedData->stateTemplateCollectedSignals.emplace_back( stateTemplate1 ); + auto stateTemplate2 = StateTemplateCollectedSignals{ + "lks2", + { CollectedSignal( 1234, 788007, 47, SignalType::DOUBLE ), + CollectedSignal( 1234, 788008, 48, SignalType::DOUBLE ), + CollectedSignal( 1234, 788009, 49, SignalType::DOUBLE ), + CollectedSignal( 1234, 788010, 50, SignalType::DOUBLE ), + CollectedSignal( 1234, 788011, 51, SignalType::DOUBLE ) }, + }; + collectedData->stateTemplateCollectedSignals.emplace_back( stateTemplate2 ); + + EXPECT_CALL( *mLastKnownStateMqttSender, mockedSendBuffer( mLastKnownStateDataTopic, _, Gt( 0 ), _ ) ).Times( 3 ); + mDataSenderManager->processData( collectedData ); + + // Since mMaxMessagesPerPayload is 5, the 11 signals should be split in 3 messages + ASSERT_EQ( mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ).size(), 3 ); + auto sentBufferData = mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ); + + Schemas::LastKnownState::LastKnownStateData lastKnownStateProto; + ASSERT_TRUE( parseLastKnownStateData( sentBufferData[0].data, &lastKnownStateProto ) ); + + ASSERT_EQ( lastKnownStateProto.collection_event_time_ms_epoch(), 1000 ); + ASSERT_EQ( lastKnownStateProto.captured_state_template_signals_size(), 1 ); + auto &capturedStateTemplateSignals = lastKnownStateProto.captured_state_template_signals()[0]; + ASSERT_EQ( capturedStateTemplateSignals.state_template_sync_id(), "lks1" ); + + auto &capturedSignals = capturedStateTemplateSignals.captured_signals(); + + ASSERT_EQ( getSignalValues( capturedSignals ), ( std::vector{ 41, 42, 43, 44, 45 } ) ); + + // Now just ensure that when number of signals is multiple of mMaxMessagesPerPayload, we don't + // send an empty message. + mLastKnownStateMqttSender->clearSentBufferData(); + collectedData = std::make_shared(); + collectedData->triggerTime = 1000; + stateTemplate2.signals.pop_back(); + // Sanity check + ASSERT_EQ( stateTemplate1.signals.size() + stateTemplate2.signals.size(), 10 ); + collectedData->stateTemplateCollectedSignals.emplace_back( stateTemplate1 ); + collectedData->stateTemplateCollectedSignals.emplace_back( stateTemplate2 ); + EXPECT_CALL( *mLastKnownStateMqttSender, mockedSendBuffer( mLastKnownStateDataTopic, _, Gt( 0 ), _ ) ).Times( 2 ); + + mDataSenderManager->processData( collectedData ); + + ASSERT_EQ( mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ).size(), 2 ); +} + +TEST_P( DataSenderManagerTestWithAllSignalTypes, PersistencyForLastKnownState ) +{ + auto signalType = GetParam(); + EXPECT_CALL( *mLastKnownStateMqttSender, mockedSendBuffer( mLastKnownStateDataTopic, _, Gt( 0 ), _ ) ) + .WillOnce( InvokeArgument<3>( ConnectivityError::NoConnection ) ); + + auto collectedData = std::make_shared(); + collectedData->triggerTime = 1000; + collectedData->stateTemplateCollectedSignals.emplace_back( + StateTemplateCollectedSignals{ "lks1", { { CollectedSignal( 1234, 789654, 40, signalType ) } } } ); + mDataSenderManager->processData( collectedData ); + + mLastKnownStateMqttSender->clearSentBufferData(); + + // We don't want to persist LKS data, since old data is not very useful. So nothing should be sent. + mDataSenderManager->checkAndSendRetrievedData(); + + ASSERT_EQ( mLastKnownStateMqttSender->getSentBufferDataByTopic( mLastKnownStateDataTopic ).size(), 0 ); +} + +TEST_F( DataSenderManagerTest, InvalidSenderWhenProcessingLastKnownState ) +{ + std::unordered_map> dataSenders; + mDataSenderManager = std::make_unique( std::move( dataSenders ), mMqttSender, mPayloadManager ); + + EXPECT_CALL( *mCommandResponseSender, mockedSendBuffer( getCommandResponseTopic( "command123" ), _, _, _ ) ) + .Times( 0 ); + + mDataSenderManager->processData( std::make_shared() ); + + ASSERT_EQ( mCommandResponseSender->getSentBufferDataByTopic( getCommandResponseTopic( "command123" ) ).size(), 0 ); +} +#endif + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/DataSenderManagerWorkerThreadTest.cpp b/test/unit/DataSenderManagerWorkerThreadTest.cpp index deaa3e76..01119fde 100644 --- a/test/unit/DataSenderManagerWorkerThreadTest.cpp +++ b/test/unit/DataSenderManagerWorkerThreadTest.cpp @@ -19,6 +19,11 @@ #include #include +#ifdef FWE_FEATURE_REMOTE_COMMANDS +#include "CommandTypes.h" +#include "ICommandDispatcher.h" +#endif + namespace Aws { namespace IoTFleetWise @@ -48,6 +53,10 @@ class DataSenderManagerWorkerThreadTest : public ::testing::Test mDataSenderManager = std::make_shared>(); mCollectedDataQueue = std::make_shared( 10000, "Collected Data" ); std::vector> dataToSendQueues; +#ifdef FWE_FEATURE_REMOTE_COMMANDS + mCommandResponses = std::make_shared( 10000, "Command Responses" ); + dataToSendQueues.emplace_back( mCommandResponses ); +#endif dataToSendQueues.emplace_back( mCollectedDataQueue ); mDataSenderManagerWorkerThread = std::make_unique( mConnectivityModule, mDataSenderManager, 100, dataToSendQueues ); @@ -70,6 +79,9 @@ class DataSenderManagerWorkerThreadTest : public ::testing::Test std::shared_ptr> mDataSenderManager; std::shared_ptr mCollectedDataQueue; std::unique_ptr mDataSenderManagerWorkerThread; +#ifdef FWE_FEATURE_REMOTE_COMMANDS + std::shared_ptr mCommandResponses; +#endif }; class DataSenderManagerWorkerThreadTestWithAllSignalTypes : public DataSenderManagerWorkerThreadTest, @@ -152,5 +164,55 @@ TEST_F( DataSenderManagerWorkerThreadTest, ProcessMultipleTriggers ) ASSERT_EQ( processedSignal.value.value.doubleVal, 99.5 ); } +#ifdef FWE_FEATURE_REMOTE_COMMANDS +TEST_F( DataSenderManagerWorkerThreadTest, ProcessSingleCommandResponse ) +{ + EXPECT_CALL( *mDataSenderManager, mockedProcessData( _ ) ).Times( 1 ); + mDataSenderManagerWorkerThread->start(); + + mCommandResponses->push( std::make_shared( + "command123", CommandStatus::EXECUTION_FAILED, REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC, "status456" ) ); + + WAIT_ASSERT_EQ( mDataSenderManager->getProcessedData().size(), 1U ); + ASSERT_TRUE( mDataSenderManagerWorkerThread->stop() ); + + auto processedCommandResponses = mDataSenderManager->getProcessedData(); + ASSERT_EQ( processedCommandResponses[0]->id, "command123" ); + ASSERT_EQ( processedCommandResponses[0]->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( processedCommandResponses[0]->reasonCode, REASON_CODE_DECODER_MANIFEST_OUT_OF_SYNC ); + ASSERT_EQ( processedCommandResponses[0]->reasonDescription, "status456" ); +} + +TEST_F( DataSenderManagerWorkerThreadTest, ProcessMultipleCommandResponses ) +{ + EXPECT_CALL( *mDataSenderManager, mockedProcessData( _ ) ).Times( 3 ); + mDataSenderManagerWorkerThread->start(); + + mCommandResponses->push( + std::make_shared( "command1", CommandStatus::SUCCEEDED, 0x1234, "status1" ) ); + mCommandResponses->push( + std::make_shared( "command2", CommandStatus::EXECUTION_FAILED, 0x5678, "status2" ) ); + mCommandResponses->push( + std::make_shared( "command3", CommandStatus::EXECUTION_TIMEOUT, 0x9ABC, "status3" ) ); + + WAIT_ASSERT_EQ( mDataSenderManager->getProcessedData().size(), 3U ); + ASSERT_TRUE( mDataSenderManagerWorkerThread->stop() ); + + auto processedCommandResponses = mDataSenderManager->getProcessedData(); + ASSERT_EQ( processedCommandResponses[0]->id, "command1" ); + ASSERT_EQ( processedCommandResponses[0]->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( processedCommandResponses[0]->reasonCode, 0x1234 ); + ASSERT_EQ( processedCommandResponses[0]->reasonDescription, "status1" ); + ASSERT_EQ( processedCommandResponses[1]->id, "command2" ); + ASSERT_EQ( processedCommandResponses[1]->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( processedCommandResponses[1]->reasonCode, 0x5678 ); + ASSERT_EQ( processedCommandResponses[1]->reasonDescription, "status2" ); + ASSERT_EQ( processedCommandResponses[2]->id, "command3" ); + ASSERT_EQ( processedCommandResponses[2]->status, CommandStatus::EXECUTION_TIMEOUT ); + ASSERT_EQ( processedCommandResponses[2]->reasonCode, 0x9ABC ); + ASSERT_EQ( processedCommandResponses[2]->reasonDescription, "status3" ); +} +#endif + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/DataSenderProtoReaderTest.cpp b/test/unit/DataSenderProtoReaderTest.cpp new file mode 100644 index 00000000..4abcde24 --- /dev/null +++ b/test/unit/DataSenderProtoReaderTest.cpp @@ -0,0 +1,140 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "DataSenderProtoReader.h" +#include "CANDataTypes.h" +#include "CANInterfaceIDTranslator.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "DataSenderProtoWriter.h" +#include "OBDDataTypes.h" +#include "SignalTypes.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class DataSenderProtoReaderTest : public ::testing::Test +{ + +protected: + DataSenderProtoReaderTest() + { + // required for serializing/deserializing channel ids + mTranslator.add( "can123" ); + + mProtoReader = std::make_shared( mTranslator ); + mProtoWriter = std::make_shared( mTranslator, nullptr ); + setupTestData(); + } + + CANInterfaceIDTranslator mTranslator; + std::shared_ptr mProtoReader; + std::shared_ptr mProtoWriter; + std::shared_ptr mClock = ClockHandler::getClock(); + + // test data + std::string serializedCompleteVehicleData; + std::shared_ptr completeVehicleData; + + void + setupTestData() + { + // complete vehicle data + { + auto data = std::make_shared(); + data->eventID = 1234; + + DTCInfo info{}; + info.receiveTime = mClock->systemTimeSinceEpochMs(); + info.mSID = SID::TESTING; + info.mDTCCodes.emplace_back( "code" ); + data->mDTCInfo = info; + + data->signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT64 } }; + + std::array buf = {}; + CollectedCanRawFrame rawFrame( 0, 0, mClock->systemTimeSinceEpochMs(), buf, 12 ); + data->canFrames = { rawFrame }; + + data->metadata = PassThroughMetadata{ true, true, 1, "decoderId", "campaignId", "campaignArn" }; + + data->triggerTime = mClock->systemTimeSinceEpochMs(); + + mProtoWriter->setupVehicleData( data, data->eventID ); + mProtoWriter->setupDTCInfo( info ); + for ( auto code : info.mDTCCodes ) + { + mProtoWriter->append( code ); + } + for ( auto signal : data->signals ) + { + mProtoWriter->append( signal ); + } + for ( auto frame : data->canFrames ) + { + mProtoWriter->append( frame ); + } + + ASSERT_TRUE( mProtoWriter->serializeVehicleData( &serializedCompleteVehicleData ) ); + completeVehicleData = data; + } + } +}; + +TEST_F( DataSenderProtoReaderTest, TestDeserializeCompleteVehicleData ) +{ + TriggeredCollectionSchemeData data{}; + mProtoReader->setupVehicleData( serializedCompleteVehicleData ); + ASSERT_TRUE( mProtoReader->deserializeVehicleData( data ) ); + + ASSERT_EQ( data.eventID, completeVehicleData->eventID ); + ASSERT_EQ( data.triggerTime, completeVehicleData->triggerTime ); + + // metadata + ASSERT_EQ( data.metadata.decoderID, completeVehicleData->metadata.decoderID ); + ASSERT_EQ( data.metadata.collectionSchemeID, completeVehicleData->metadata.collectionSchemeID ); + // the following metadata isn't preserved during serialization + ASSERT_NE( data.metadata.priority, completeVehicleData->metadata.priority ); + ASSERT_NE( data.metadata.compress, completeVehicleData->metadata.compress ); + ASSERT_NE( data.metadata.persist, completeVehicleData->metadata.persist ); + + // signals + ASSERT_EQ( data.signals.size(), completeVehicleData->signals.size() ); + for ( size_t i = 0; i < data.signals.size(); ++i ) + { + ASSERT_EQ( data.signals[i].receiveTime, completeVehicleData->signals[i].receiveTime ); + ASSERT_EQ( data.signals[i].signalID, completeVehicleData->signals[i].signalID ); + ASSERT_EQ( data.signals[i].value.value.doubleVal, + static_cast( completeVehicleData->signals[i].value.value.uint8Val ) ); + } + + // can frames + ASSERT_EQ( data.canFrames.size(), completeVehicleData->canFrames.size() ); + for ( size_t i = 0; i < data.canFrames.size(); ++i ) + { + ASSERT_EQ( data.canFrames[i].receiveTime, completeVehicleData->canFrames[i].receiveTime ); + ASSERT_EQ( data.canFrames[i].channelId, completeVehicleData->canFrames[i].channelId ); + ASSERT_EQ( data.canFrames[i].frameID, completeVehicleData->canFrames[i].frameID ); + ASSERT_EQ( data.canFrames[i].size, completeVehicleData->canFrames[i].size ); + ASSERT_EQ( data.canFrames[i].data, completeVehicleData->canFrames[i].data ); + } + + // dtc info + // mSID is not preserved during serialization + ASSERT_NE( data.mDTCInfo.mSID, completeVehicleData->mDTCInfo.mSID ); + ASSERT_EQ( data.mDTCInfo.receiveTime, completeVehicleData->mDTCInfo.receiveTime ); + ASSERT_EQ( data.mDTCInfo.mDTCCodes, completeVehicleData->mDTCInfo.mDTCCodes ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/DataSenderProtoWriterTest.cpp b/test/unit/DataSenderProtoWriterTest.cpp index fcc86644..ccf134ce 100644 --- a/test/unit/DataSenderProtoWriterTest.cpp +++ b/test/unit/DataSenderProtoWriterTest.cpp @@ -10,15 +10,19 @@ #include "OBDDataTypes.h" #include "RawDataManager.h" #include "SignalTypes.h" +#include "Testing.h" #include "TimeTypes.h" #include "vehicle_data.pb.h" #include +#include +#include #include #include #include #include #include #include +#include #include namespace Aws @@ -30,6 +34,117 @@ class DataSenderProtoWriterTest : public ::testing::Test { }; +TEST_F( DataSenderProtoWriterTest, CollectStringSignalNoRawBufferManager ) +{ + CANInterfaceIDTranslator canIDTranslator; + CollectedSignal stringSignal; + stringSignal.signalID = 101; + stringSignal.value.type = SignalType::STRING; + stringSignal.receiveTime = 5678; + + std::shared_ptr triggeredCollectionSchemeDataPtr = + std::make_shared(); + auto testClock = ClockHandler::getClock(); + Timestamp testTriggerTime = testClock->systemTimeSinceEpochMs(); + uint32_t collectionEventID = std::rand(); + std::string serializedData; + + triggeredCollectionSchemeDataPtr->metadata.persist = false; + triggeredCollectionSchemeDataPtr->metadata.compress = false; + triggeredCollectionSchemeDataPtr->metadata.priority = 0; + triggeredCollectionSchemeDataPtr->metadata.collectionSchemeID = "123"; + triggeredCollectionSchemeDataPtr->metadata.decoderID = "456"; + triggeredCollectionSchemeDataPtr->triggerTime = testTriggerTime; + + DataSenderProtoWriter mDataSenderProtoWriter( canIDTranslator, nullptr ); + mDataSenderProtoWriter.setupVehicleData( triggeredCollectionSchemeDataPtr, collectionEventID ); + + stringSignal.value.value.uint32Val = static_cast( 123 ); + mDataSenderProtoWriter.append( stringSignal ); + + ASSERT_TRUE( mDataSenderProtoWriter.serializeVehicleData( &serializedData ) ); + + Schemas::VehicleDataMsg::VehicleData vehicleData; + vehicleData.ParseFromString( serializedData ); + ASSERT_EQ( vehicleData.captured_signals_size(), 0 ); +} + +TEST_F( DataSenderProtoWriterTest, CollectStringSignal ) +{ + CANInterfaceIDTranslator canIDTranslator; + CollectedSignal stringSignal; + TimePoint timestamp = { 160000000, 100 }; + std::shared_ptr mRawDataBufferManager; + RawData::SignalUpdateConfig signalUpdateConfig1; + RawData::SignalBufferOverrides signalOverrides1; + std::vector overridesPerSignal; + std::unordered_map updatedSignals; + std::shared_ptr triggeredCollectionSchemeDataPtr = + std::make_shared(); + auto testClock = ClockHandler::getClock(); + Timestamp testTriggerTime = testClock->systemTimeSinceEpochMs(); + uint32_t collectionEventID = std::rand(); + std::string stringData = "1BDD00"; + std::string serializedData; + + stringSignal.signalID = 101; + stringSignal.value.type = SignalType::STRING; + stringSignal.receiveTime = 5678; + + signalUpdateConfig1.typeId = stringSignal.signalID; + signalUpdateConfig1.interfaceId = "interface1"; + signalUpdateConfig1.messageId = "VEHICLE.DTC_INFO"; + signalOverrides1.interfaceId = signalUpdateConfig1.interfaceId; + signalOverrides1.messageId = signalUpdateConfig1.messageId; + signalOverrides1.maxNumOfSamples = 20; + signalOverrides1.maxBytesPerSample = 5_MiB; + signalOverrides1.reservedBytes = 5_MiB; + signalOverrides1.maxBytes = 100_MiB; + + triggeredCollectionSchemeDataPtr->metadata.persist = false; + triggeredCollectionSchemeDataPtr->metadata.compress = false; + triggeredCollectionSchemeDataPtr->metadata.priority = 0; + triggeredCollectionSchemeDataPtr->metadata.collectionSchemeID = "123"; + triggeredCollectionSchemeDataPtr->metadata.decoderID = "456"; + triggeredCollectionSchemeDataPtr->triggerTime = testTriggerTime; + + overridesPerSignal = { signalOverrides1 }; + + boost::optional rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::make_optional( (size_t)20 ), boost::none, boost::none, overridesPerSignal ); + + updatedSignals = { { signalUpdateConfig1.typeId, signalUpdateConfig1 } }; + + std::shared_ptr rawDataBufferManager = + std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + DataSenderProtoWriter mDataSenderProtoWriter( canIDTranslator, rawDataBufferManager ); + mDataSenderProtoWriter.setupVehicleData( triggeredCollectionSchemeDataPtr, collectionEventID ); + + auto handle = rawDataBufferManager->push( + (uint8_t *)stringData.c_str(), stringData.length(), timestamp.systemTimeMs, stringSignal.signalID ); + + rawDataBufferManager->increaseHandleUsageHint( + 101, handle, RawData::BufferHandleUsageStage::COLLECTION_INSPECTION_ENGINE_SELECTED_FOR_UPLOAD ); + + stringSignal.value.value.uint32Val = static_cast( handle ); + mDataSenderProtoWriter.append( stringSignal ); + // Invalid buffer handle + mDataSenderProtoWriter.append( CollectedSignal( 123, // signalId + timestamp.systemTimeMs, // receiveTime, + RawData::INVALID_BUFFER_HANDLE, // value + SignalType::STRING ) ); + + ASSERT_TRUE( mDataSenderProtoWriter.serializeVehicleData( &serializedData ) ); + + Schemas::VehicleDataMsg::VehicleData vehicleData; + vehicleData.ParseFromString( serializedData ); + ASSERT_EQ( vehicleData.captured_signals_size(), 1 ); + EXPECT_EQ( vehicleData.captured_signals( 0 ).signal_id(), stringSignal.signalID ); + EXPECT_EQ( vehicleData.captured_signals( 0 ).string_value(), stringData.c_str() ); +} + // Test the edge to cloud payload fields in the proto TEST_F( DataSenderProtoWriterTest, TestVehicleData ) { diff --git a/test/unit/DecoderDictionaryExtractorTest.cpp b/test/unit/DecoderDictionaryExtractorTest.cpp index 79cf4d2e..872cd597 100644 --- a/test/unit/DecoderDictionaryExtractorTest.cpp +++ b/test/unit/DecoderDictionaryExtractorTest.cpp @@ -17,6 +17,9 @@ #ifdef FWE_FEATURE_VISION_SYSTEM_DATA #include #endif +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateTypes.h" +#endif namespace Aws { @@ -88,6 +91,15 @@ namespace IoTFleetWise * bit right shift: 2 * bit mask length: 2 * + * Custom decoded signal 0 + * SignalID: 0x2000 + * InterfaceID: 30 + * Decoder: custom-decoder-0 + * Custom decoded signal 1 + * SignalID: 0x2001 + * InterfaceID: 31 + * Decoder: custom-decoder-1 + * * CollectionScheme1 is interested in signal 0 ~ 8 and CAN Raw Frame 0x100, 0x200 at Node 10 and OBD Signals * CollectionScheme2 is interested in signal 10 ~ 17 and CAN Raw Frame 0x200 at Node 20 * CollectionScheme3 is interested in signal 25 at Node 20 @@ -243,7 +255,13 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) { 0x1005, PIDSignalDecoderFormat( 10, SID::CURRENT_STATS, 0x70, 1.0, 0.0, 9, 1, 0, 2 ) }, { 0x1006, PIDSignalDecoderFormat( 10, SID::CURRENT_STATS, 0x70, 1.0, 0.0, 9, 1, 2, 2 ) } }; - // Add OBD-II PID signals to CollectionScheme 1 + // Here's input to decoder manifest for Custom Signal decoder information + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat = { + { 0x2000, CustomSignalDecoderFormat{ "30", "custom-decoder-0", 0x2000, SignalType::DOUBLE } }, + { 0x2001, CustomSignalDecoderFormat{ "31", "custom-decoder-1", 0x2001, SignalType::DOUBLE } }, + }; + + // Add OBD-II PID signals to CollectionScheme 2 SignalCollectionInfo obdPidSignal; obdPidSignal.signalID = 0x1000; signalInfo2.emplace_back( obdPidSignal ); @@ -253,6 +271,14 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) signalInfo2.emplace_back( obdPidSignal ); obdPidSignal.signalID = 0x1006; signalInfo2.emplace_back( obdPidSignal ); + + // Add Custom Decoded signals to CollectionScheme 2 + SignalCollectionInfo customSignal; + customSignal.signalID = 0x2000; + signalInfo2.emplace_back( customSignal ); + customSignal.signalID = 0x2001; + signalInfo2.emplace_back( customSignal ); + // Add an invalid network protocol signal. PM shall not add it to decoder dictionary SignalCollectionInfo inValidSignal; inValidSignal.signalID = 0x10000; @@ -269,8 +295,8 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) list1.emplace_back( collectionScheme2 ); list1.emplace_back( collectionScheme3 ); - IDecoderManifestPtr DM1 = - std::make_shared( "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat ); + IDecoderManifestPtr DM1 = std::make_shared( + "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, signalIDToCustomDecoderFormat ); // Set input as DM1, list1 test.setDecoderManifest( DM1 ); @@ -283,6 +309,8 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) test.decoderDictionaryExtractor( decoderDictionaryMap ); ASSERT_TRUE( decoderDictionaryMap.find( VehicleDataSourceProtocol::RAW_SOCKET ) != decoderDictionaryMap.end() ); ASSERT_TRUE( decoderDictionaryMap.find( VehicleDataSourceProtocol::OBD ) != decoderDictionaryMap.end() ); + ASSERT_TRUE( decoderDictionaryMap.find( VehicleDataSourceProtocol::CUSTOM_DECODING ) != + decoderDictionaryMap.end() ); auto decoderDictionary = std::dynamic_pointer_cast( decoderDictionaryMap[VehicleDataSourceProtocol::RAW_SOCKET] ); // Below section exam decoder dictionary @@ -336,9 +364,9 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) // CAN Frame 0x300 at Node 20 shall not exist in dictionary as CollectionScheme3 is not enabled yet ASSERT_EQ( decoderDictionary->canMessageDecoderMethod[secondChannelId].count( 0x300 ), 0 ); // check the signalIDsToCollect from CAN decoder dictionary shall contain all the targeted CAN signals from - // collectionSchemes Note minus 5 because 4 signals are OBD signals which will be included in OBD decoder dictionary - // and one invalid signal - ASSERT_EQ( decoderDictionary->signalIDsToCollect.size(), signalInfo1.size() + signalInfo2.size() - 5 ); + // collectionSchemes Note minus 7 because 4 signals are OBD signals which will be included in OBD decoder dictionary + // 2 custom decoded signals and one invalid signal + ASSERT_EQ( decoderDictionary->signalIDsToCollect.size(), signalInfo1.size() + signalInfo2.size() - 7 ); for ( auto const &signal : signalInfo1 ) { ASSERT_EQ( decoderDictionary->signalIDsToCollect.count( signal.signalID ), 1 ); @@ -382,6 +410,21 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) ASSERT_EQ( formula.mSizeInBits, 2 ); // Decoder Manifest doesn't contain PID 0x20, hence it shall not contain the decoder dictionary ASSERT_TRUE( obdPidDecoderDictionary.at( 0 ).find( 0x20 ) == obdPidDecoderDictionary.at( 0 ).end() ); + + auto customDecoderDictionary = std::dynamic_pointer_cast( + decoderDictionaryMap[VehicleDataSourceProtocol::CUSTOM_DECODING] ); + ASSERT_EQ( customDecoderDictionary->customDecoderMethod.size(), 2 ); // 2 interfaces + auto customDecoder = customDecoderDictionary->customDecoderMethod.find( "30" ); + ASSERT_TRUE( customDecoder != customDecoderDictionary->customDecoderMethod.end() ); + auto customDecoderSignalID = customDecoder->second.find( "custom-decoder-0" ); + ASSERT_TRUE( customDecoderSignalID != customDecoder->second.end() ); + ASSERT_EQ( customDecoderSignalID->second.mSignalID, 0x2000 ); + customDecoder = customDecoderDictionary->customDecoderMethod.find( "31" ); + ASSERT_TRUE( customDecoder != customDecoderDictionary->customDecoderMethod.end() ); + customDecoderSignalID = customDecoder->second.find( "custom-decoder-1" ); + ASSERT_TRUE( customDecoderSignalID != customDecoder->second.end() ); + ASSERT_EQ( customDecoderSignalID->second.mSignalID, 0x2001 ); + // Time travel to the point where Both collectionScheme1 and collectionScheme2 are retired and CollectionScheme 3 // enabled ASSERT_TRUE( test.updateMapsandTimeLine( currTime + SECOND_TO_MILLISECOND( 6 ) ) ); @@ -396,6 +439,11 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) decoderDictionary = std::dynamic_pointer_cast( decoderDictionaryMapNew[VehicleDataSourceProtocol::OBD] ); ASSERT_EQ( decoderDictionary, nullptr ); + ASSERT_TRUE( decoderDictionaryMapNew.find( VehicleDataSourceProtocol::CUSTOM_DECODING ) != + decoderDictionaryMapNew.end() ); + customDecoderDictionary = std::dynamic_pointer_cast( + decoderDictionaryMapNew[VehicleDataSourceProtocol::CUSTOM_DECODING] ); + ASSERT_EQ( customDecoderDictionary, nullptr ); decoderDictionary = std::dynamic_pointer_cast( decoderDictionaryMapNew[VehicleDataSourceProtocol::RAW_SOCKET] ); @@ -423,6 +471,21 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) ASSERT_EQ( decoderMethod.format.mSignals[0].mSignalID, 25 ); } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + auto stateTemplate = std::make_shared(); + stateTemplate->id = "LKS1"; + stateTemplate->decoderManifestID = "DM1"; + stateTemplate->updateStrategy = LastKnownStateUpdateStrategy::PERIODIC; + stateTemplate->periodMs = 10; + stateTemplate->signals.push_back( { 11, SignalType::DOUBLE } ); + test.setStateTemplates( std::make_shared( StateTemplatesDiff{ 123, { stateTemplate }, {} } ) ); + std::map> decoderDictionaryMap3; + test.decoderDictionaryExtractor( decoderDictionaryMap3 ); + decoderDictionary = + std::dynamic_pointer_cast( decoderDictionaryMap3[VehicleDataSourceProtocol::RAW_SOCKET] ); + ASSERT_EQ( decoderDictionary->signalIDsToCollect.count( 11 ), 1 ); +#endif + // The following code validates that when we have first the OBD signals in the decoder manifest // and then the CAN signals, the extraction still functions. The above code starts processing // always the CAN Signals first as the first network type is CAN. @@ -439,8 +502,8 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) list2.emplace_back( collectionSchemeOBD ); // OBD Signals list2.emplace_back( collectionSchemeCAN ); // CAN Signals - IDecoderManifestPtr DM2 = - std::make_shared( "DM2", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat ); + IDecoderManifestPtr DM2 = std::make_shared( + "DM2", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, signalIDToCustomDecoderFormat ); // Set input as DM1, list1 test2.setDecoderManifest( DM2 ); @@ -453,6 +516,8 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorTest ) test2.decoderDictionaryExtractor( decoderDictionaryMap2 ); ASSERT_TRUE( decoderDictionaryMap2.find( VehicleDataSourceProtocol::RAW_SOCKET ) != decoderDictionaryMap2.end() ); ASSERT_TRUE( decoderDictionaryMap2.find( VehicleDataSourceProtocol::OBD ) != decoderDictionaryMap2.end() ); + ASSERT_TRUE( decoderDictionaryMap2.find( VehicleDataSourceProtocol::CUSTOM_DECODING ) != + decoderDictionaryMap2.end() ); } /** @brief @@ -496,9 +561,10 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorNoSignalsTest ) list1.emplace_back( collectionScheme1 ); std::unordered_map signalIDToPIDDecoderFormat = {}; + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat = {}; - IDecoderManifestPtr DM1 = - std::make_shared( "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat ); + IDecoderManifestPtr DM1 = std::make_shared( + "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, signalIDToCustomDecoderFormat ); // Set input as DM1, list1 test.setDecoderManifest( DM1 ); @@ -583,9 +649,10 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryExtractorFirstRawFrameThe list1.emplace_back( collectionScheme2 ); std::unordered_map signalIDToPIDDecoderFormat = {}; + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat = {}; - IDecoderManifestPtr DM1 = - std::make_shared( "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat ); + IDecoderManifestPtr DM1 = std::make_shared( + "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, signalIDToCustomDecoderFormat ); // Set input as DM1, list1 test.setDecoderManifest( DM1 ); @@ -626,6 +693,7 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryComplexDataExtractor ) ICollectionScheme::RawCanFrames_t canFrameInfo1; // empty std::unordered_map> formatMap; // empty std::unordered_map signalIDToPIDDecoderFormat; // empty + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat; // empty ICollectionScheme::PartialSignalIDLookup partialSignalIDLookup; std::unordered_map complexSignalMap; @@ -677,8 +745,13 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryComplexDataExtractor ) std::vector list1; list1.emplace_back( collectionScheme1 ); - IDecoderManifestPtr DM1 = std::make_shared( - "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, complexSignalMap, complexDataTypeMap ); + IDecoderManifestPtr DM1 = std::make_shared( "DM1", + formatMap, + signalToFrameAndNodeID, + signalIDToPIDDecoderFormat, + signalIDToCustomDecoderFormat, + complexSignalMap, + complexDataTypeMap ); // Set input as DM1, list1 test.setDecoderManifest( DM1 ); @@ -688,7 +761,8 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryComplexDataExtractor ) ASSERT_TRUE( test.updateMapsandTimeLine( currTime ) ); auto inspectionMatrixOutput = std::make_shared(); - test.matrixExtractor( inspectionMatrixOutput ); + auto fetchMatrixOutput = std::make_shared(); + test.matrixExtractor( inspectionMatrixOutput, fetchMatrixOutput ); // Invoke Decoder Dictionary Extractor function std::map> decoderDictionaryMap; test.decoderDictionaryExtractor( decoderDictionaryMap, inspectionMatrixOutput ); @@ -769,6 +843,7 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryInvalidPartialSignalIDAnd ICollectionScheme::RawCanFrames_t canFrameInfo1; // empty std::unordered_map> formatMap; // empty std::unordered_map signalIDToPIDDecoderFormat; // empty + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat; // empty ICollectionScheme::PartialSignalIDLookup partialSignalIDLookup; std::unordered_map complexSignalMap; @@ -801,8 +876,13 @@ TEST( DecoderDictionaryExtractorTest, DecoderDictionaryInvalidPartialSignalIDAnd std::vector list1; list1.emplace_back( collectionScheme1 ); - IDecoderManifestPtr DM1 = std::make_shared( - "DM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, complexSignalMap, complexDataTypeMap ); + IDecoderManifestPtr DM1 = std::make_shared( "DM1", + formatMap, + signalToFrameAndNodeID, + signalIDToPIDDecoderFormat, + signalIDToCustomDecoderFormat, + complexSignalMap, + complexDataTypeMap ); test.setDecoderManifest( DM1 ); ICollectionSchemeListPtr PL1 = std::make_shared( list1 ); diff --git a/test/unit/DeviceShadowOverSomeipTest.cpp b/test/unit/DeviceShadowOverSomeipTest.cpp new file mode 100644 index 00000000..f4ff775f --- /dev/null +++ b/test/unit/DeviceShadowOverSomeipTest.cpp @@ -0,0 +1,349 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "DeviceShadowOverSomeip.h" +#include "IConnectionTypes.h" +#include "IReceiver.h" +#include "ISender.h" +#include "SenderMock.h" +#include "v1/commonapi/DeviceShadowOverSomeipInterface.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::Eq; +using ::testing::Invoke; +using ::testing::InvokeArgument; +using ::testing::MockFunction; +using ::testing::StrictMock; + +class DeviceShadowOverSomeipTest : public ::testing::Test +{ +protected: + DeviceShadowOverSomeipTest() + : mSenderMock( std::make_shared>() ) + , mDeviceShadowOverSomeip( std::make_shared( mSenderMock ) ) + { + } + void + SetUp() override + { + } + + void + TearDown() override + { + } + + std::shared_ptr> mSenderMock; + std::shared_ptr mDeviceShadowOverSomeip; +}; + +TEST_F( DeviceShadowOverSomeipTest, updateInvalidSentJson ) +{ + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::INVALID_REQUEST ), + Eq( "JSON parse error" ), + _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "invalid", callback.AsStdFunction() ); +} + +TEST_F( DeviceShadowOverSomeipTest, updateConnectivityErrorInvalidRequest ) +{ + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( InvokeArgument<3>( ConnectivityError::NotConfigured ) ); + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::INVALID_REQUEST ), + Eq( "NotConfigured" ), + _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "{}", callback.AsStdFunction() ); +} + +TEST_F( DeviceShadowOverSomeipTest, updateConnectivityErrorUnreachable ) +{ + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( InvokeArgument<3>( ConnectivityError::TransmissionError ) ); + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::SHADOW_SERVICE_UNREACHABLE ), + Eq( "TransmissionError" ), + _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "{}", callback.AsStdFunction() ); +} + +TEST_F( DeviceShadowOverSomeipTest, updateInvalidReceivedJson ) +{ + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); + MockFunction + callback; + EXPECT_CALL( callback, Call( _, _, _ ) ).Times( 0 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "{}", callback.AsStdFunction() ); + { + std::string payload = "invalid"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/update/accepted" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } +} + +TEST_F( DeviceShadowOverSomeipTest, updateOtherClient ) +{ + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( InvokeArgument<3>( ConnectivityError::Success ) ); + MockFunction + callback; + EXPECT_CALL( callback, Call( _, _, _ ) ).Times( 0 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "{}", callback.AsStdFunction() ); + { + std::string payload = "{}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/update/accepted" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } +} + +TEST_F( DeviceShadowOverSomeipTest, updateAcceptedClassic ) +{ + std::string clientToken; + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( + Invoke( [&clientToken]( + const std::string &topic, const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) { + static_cast( topic ); + std::string sendMessage( buf, buf + size ); + Json::Reader jsonReader; + Json::Value sendJson; + ASSERT_TRUE( jsonReader.parse( sendMessage, sendJson ) ); + clientToken = sendJson["clientToken"].asString(); + callback( ConnectivityError::Success ); + } ) ); + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::NO_ERROR ), Eq( "" ), _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "{}", callback.AsStdFunction() ); + { + std::string payload = "{\"clientToken\":\"" + clientToken + "\"}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/update/accepted" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } +} + +TEST_F( DeviceShadowOverSomeipTest, updateRejectedNamed ) +{ + std::string clientToken; + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/name/test/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( + Invoke( [&clientToken]( + const std::string &topic, const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) { + static_cast( topic ); + std::string sendMessage( buf, buf + size ); + Json::Reader jsonReader; + Json::Value sendJson; + ASSERT_TRUE( jsonReader.parse( sendMessage, sendJson ) ); + clientToken = sendJson["clientToken"].asString(); + callback( ConnectivityError::Success ); + } ) ); + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::REJECTED ), Eq( "abc" ), _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "test", "{}", callback.AsStdFunction() ); + { + // Check receiving own request is ignored + std::string payload = "{\"clientToken\":\"" + clientToken + "\"}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/update" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } + { + std::string payload = "{\"clientToken\":\"" + clientToken + "\",\"message\":\"abc\"}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/name/test/update/rejected" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } +} + +TEST_F( DeviceShadowOverSomeipTest, updateUnknown ) +{ + std::string clientToken; + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/update", _, _, _ ) ) + .Times( 1 ) + .WillOnce( + Invoke( [&clientToken]( + const std::string &topic, const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) { + static_cast( topic ); + std::string sendMessage( buf, buf + size ); + Json::Reader jsonReader; + Json::Value sendJson; + ASSERT_TRUE( jsonReader.parse( sendMessage, sendJson ) ); + clientToken = sendJson["clientToken"].asString(); + callback( ConnectivityError::Success ); + } ) ); + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::UNKNOWN ), Eq( "" ), _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->updateShadow( nullptr, "", "{}", callback.AsStdFunction() ); + std::string payload = "{\"clientToken\":\"" + clientToken + "\"}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/update/blah" ); + mDeviceShadowOverSomeip->onDataReceived( message ); +} + +TEST_F( DeviceShadowOverSomeipTest, documentsUpdateClassic ) +{ + std::string payload = "{}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/update/documents" ); + mDeviceShadowOverSomeip->onDataReceived( message ); +} + +TEST_F( DeviceShadowOverSomeipTest, documentsUpdateNamed ) +{ + std::string payload = "{}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/name/test/update/documents" ); + mDeviceShadowOverSomeip->onDataReceived( message ); +} + +TEST_F( DeviceShadowOverSomeipTest, documentsUpdateWrongPrefix ) +{ + std::string payload = "{}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "wrong_$aws/things/thing-name/shadow/update/documents" ); + mDeviceShadowOverSomeip->onDataReceived( message ); +} + +TEST_F( DeviceShadowOverSomeipTest, getAccepted ) +{ + std::string clientToken; + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/get", _, _, _ ) ) + .Times( 1 ) + .WillOnce( + Invoke( [&clientToken]( + const std::string &topic, const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) { + static_cast( topic ); + std::string sendMessage( buf, buf + size ); + Json::Reader jsonReader; + Json::Value sendJson; + ASSERT_TRUE( jsonReader.parse( sendMessage, sendJson ) ); + clientToken = sendJson["clientToken"].asString(); + callback( ConnectivityError::Success ); + } ) ); + MockFunction + callback; + EXPECT_CALL( callback, + Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::NO_ERROR ), Eq( "" ), _ ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->getShadow( nullptr, "", callback.AsStdFunction() ); + { + std::string payload = "{\"clientToken\":\"" + clientToken + "\"}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/get/accepted" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } +} + +TEST_F( DeviceShadowOverSomeipTest, deleteAccepted ) +{ + std::string clientToken; + EXPECT_CALL( *mSenderMock, mockedSendBuffer( "$aws/things/thing-name/shadow/delete", _, _, _ ) ) + .Times( 1 ) + .WillOnce( + Invoke( [&clientToken]( + const std::string &topic, const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) { + static_cast( topic ); + std::string sendMessage( buf, buf + size ); + Json::Reader jsonReader; + Json::Value sendJson; + ASSERT_TRUE( jsonReader.parse( sendMessage, sendJson ) ); + clientToken = sendJson["clientToken"].asString(); + callback( ConnectivityError::Success ); + } ) ); + MockFunction + callback; + EXPECT_CALL( callback, Call( Eq( v1::commonapi::DeviceShadowOverSomeipInterface::ErrorCode::NO_ERROR ), Eq( "" ) ) ) + .Times( 1 ); + mDeviceShadowOverSomeip->deleteShadow( nullptr, "", callback.AsStdFunction() ); + { + std::string payload = "{\"clientToken\":\"" + clientToken + "\"}"; + ReceivedConnectivityMessage message( reinterpret_cast( payload.data() ), + payload.size(), + 0, + "$aws/things/thing-name/shadow/delete/accepted" ); + mDeviceShadowOverSomeip->onDataReceived( message ); + } +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/ExampleUDSInterfaceTest.cpp b/test/unit/ExampleUDSInterfaceTest.cpp new file mode 100644 index 00000000..141c3c10 --- /dev/null +++ b/test/unit/ExampleUDSInterfaceTest.cpp @@ -0,0 +1,637 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "ExampleUDSInterface.h" +#include "CollectionInspectionAPITypes.h" +#include "DataFetchManagerAPITypes.h" +#include "IDecoderDictionary.h" +#include "IDecoderManifest.h" +#include "IRemoteDiagnostics.h" +#include "ISOTPOverCANOptions.h" +#include "ISOTPOverCANSender.h" +#include "ISOTPOverCANSenderReceiver.h" +#include "LoggingModule.h" +#include "NamedSignalDataSource.h" +#include "QueueTypes.h" +#include "RawDataManager.h" +#include "RemoteDiagnosticDataSource.h" +#include "SignalTypes.h" +#include "Testing.h" +#include "VehicleDataSourceTypes.h" +#include "WaitUntil.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class UDSTemplateInterfaceTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + EcuConfig ecuConfig; + ecuConfig.targetAddress = 1; + ecuConfig.canBus = "vcan0"; + ecuConfig.ecuName = "ECM"; + ecuConfig.functionalAddress = 0x7DF; + ecuConfig.physicalRequestID = 0x123; + ecuConfig.physicalResponseID = 0x456; + mEcuConfigs.emplace_back( ecuConfig ); + mSignalBuffer = std::make_shared( 256, "Signal Buffer" ); + mSignalBufferDistributor = std::make_shared(); + mSignalBufferDistributor->registerQueue( mSignalBuffer ); + mNamedSignalDataSource = std::make_shared( "interface1", mSignalBufferDistributor ); + mUDSInterface = std::make_shared(); + mDictionary = std::make_shared(); + mDictionary->customDecoderMethod["interface1"]["Vehicle.ECU1.DTC_INFO"] = + CustomSignalDecoderFormat{ "interface1", "Vehicle.ECU1.DTC_INFO", 0x1234, SignalType::STRING }; + } + void + TearDown() override + { + } + + static void + sendPDU( ISOTPOverCANSender &sender, const std::vector &pdu ) + { + if ( !sender.sendPDU( pdu ) ) + { + FWE_LOG_ERROR( "Error sending ISO-TP message" ); + } + } + + static void + functionalSenderReceiverSendPDU( ISOTPOverCANSenderReceiver &senderReceiver, std::vector &pdu ) + { + if ( !senderReceiver.receivePDU( pdu ) ) + { + FWE_LOG_ERROR( "Error receiving ISO-TP message" ); + return; + } + if ( pdu.at( 1 ) == 0x02 ) + { + auto txPDUData = std::vector( { 0x59, 0x02, 0xAA, 0x11, 0x22, 0x33 } ); + ISOTPOverCANSender sender; + ISOTPOverCANSenderOptions senderOptions; + senderOptions.mSocketCanIFName = "vcan0"; + senderOptions.mSourceCANId = 0x456; + senderOptions.mDestinationCANId = 0x123; + ASSERT_TRUE( sender.init( senderOptions ) ); + ASSERT_TRUE( sender.connect() ); + sender.sendPDU( txPDUData ); + sender.disconnect(); + } + else if ( pdu.at( 1 ) == 0x04 ) + { + auto txPDUData = std::vector( + { 0x59, 0x04, 0x11, 0x22, 0x33, 0x24, 0x02, 0x01, 0x47, 0x11, 0xA6, 0x66, 0x07, 0x50, 0x20 } ); + ISOTPOverCANSender sender; + ISOTPOverCANSenderOptions senderOptions; + senderOptions.mSocketCanIFName = "vcan0"; + senderOptions.mSourceCANId = 0x456; + senderOptions.mDestinationCANId = 0x123; + ASSERT_TRUE( sender.init( senderOptions ) ); + ASSERT_TRUE( sender.connect() ); + sender.sendPDU( txPDUData ); + sender.disconnect(); + } + else if ( pdu.at( 1 ) == 0x06 ) + { + auto txPDUData = std::vector( + { 0x59, 0x06, 0x11, 0x22, 0x33, 0x24, 0x02, 0x01, 0x47, 0x11, 0xA6, 0x66, 0x07, 0x50, 0x20 } ); + ISOTPOverCANSender sender; + ISOTPOverCANSenderOptions senderOptions; + senderOptions.mSocketCanIFName = "vcan0"; + senderOptions.mSourceCANId = 0x456; + senderOptions.mDestinationCANId = 0x123; + ASSERT_TRUE( sender.init( senderOptions ) ); + ASSERT_TRUE( sender.connect() ); + sender.sendPDU( txPDUData ); + sender.disconnect(); + } + } + + static void + phySenderReceiverSendPDU( ISOTPOverCANSenderReceiver &senderReceiver, std::vector &pdu ) + { + if ( !senderReceiver.receivePDU( pdu ) ) + { + FWE_LOG_ERROR( "Error receiving ISO-TP message" ); + return; + } + if ( pdu.at( 1 ) == 0x02 ) + { + auto txPDUData = std::vector( { 0x59, 0x02, 0xFF, 0xAA, 0x11, 0x22, 0x33 } ); + senderReceiver.sendPDU( txPDUData ); + } + if ( pdu.at( 1 ) == 0x03 ) + { + auto txPDUData = std::vector( { 0x59, 0x03, 0xAA, 0x11, 0x22, 0x01 } ); + senderReceiver.sendPDU( txPDUData ); + } + else if ( pdu.at( 1 ) == 0x04 ) + { + auto txPDUData = std::vector( + { 0x59, 0x04, 0x11, 0x22, 0x33, 0x24, 0x02, 0x01, 0x47, 0x11, 0xA6, 0x66, 0x07, 0x50, 0x20 } ); + senderReceiver.sendPDU( txPDUData ); + } + else if ( pdu.at( 1 ) == 0x06 ) + { + auto txPDUData = std::vector( + { 0x59, 0x06, 0xAA, 0x11, 0x22, 0x33, 0x24, 0x02, 0x01, 0x47, 0x11, 0xA6, 0x66, 0x07, 0x50, 0x20 } ); + senderReceiver.sendPDU( txPDUData ); + } + } + + std::thread mThread; + std::shared_ptr mUdsModule; + std::shared_ptr mUDSInterface; + SignalBufferPtr mSignalBuffer; + SignalBufferDistributorPtr mSignalBufferDistributor; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr mDictionary; + std::vector mEcuConfigs; +}; + +TEST_F( UDSTemplateInterfaceTest, InitFailureEmptyConfig ) +{ + std::vector emptyConfig; + ASSERT_FALSE( mUDSInterface->init( emptyConfig ) ); +} + +TEST_F( UDSTemplateInterfaceTest, NoNamedSignalDataSource ) +{ + mUdsModule = std::make_shared( nullptr, nullptr, mUDSInterface ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue; + targetAddress = static_cast( -1 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_BY_STATUS_MASK ); + udsStatusMaskValue = static_cast( -1 ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::SIGNAL_NOT_FOUND ); +} + +TEST_F( UDSTemplateInterfaceTest, UnknownSignal ) +{ + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + mUdsModule = std::make_shared( mNamedSignalDataSource, nullptr, mUDSInterface ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue; + targetAddress = static_cast( -1 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_BY_STATUS_MASK ); + udsStatusMaskValue = static_cast( -1 ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + + // Unknown SignalID + ASSERT_EQ( mUdsModule->DTC_QUERY( 756, 0x03, params ), FetchErrorCode::SIGNAL_NOT_FOUND ); +} + +TEST_F( UDSTemplateInterfaceTest, UnsupportedDTCQueryParameters ) +{ + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + mUdsModule = std::make_shared( mNamedSignalDataSource, nullptr, mUDSInterface ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue; + params.push_back( std::move( targetAddress ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + targetAddress = static_cast( -1 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_BY_STATUS_MASK ); + udsStatusMaskValue = static_cast( -1 ); + + params.push_back( "wrong target address" ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( "wrong sub function" ); + params.push_back( std::move( udsStatusMaskValue ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( -1 ); + params.push_back( std::move( udsStatusMaskValue ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( 0 ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( "wrong status mask" ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + params.push_back( 0 ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + params.push_back( "wrong dtc number" ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); + + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + params.push_back( "0x112233" ); + params.push_back( "wrong record id" ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); + params.clear(); +} + +TEST_F( UDSTemplateInterfaceTest, TestDTCQuery ) +{ + std::shared_ptr rawDataBufferManager; + boost::optional rawDataBufferManagerConfig; + RawData::SignalUpdateConfig signalUpdateConfig1; + std::vector rawDataBufferOverridesPerSignal; + signalUpdateConfig1.typeId = 0x1234; + RawData::SignalBufferOverrides signalOverrides; + signalOverrides.interfaceId = "interface1"; + signalUpdateConfig1.interfaceId = signalOverrides.interfaceId; + signalOverrides.messageId = "Vehicle.ECU1.DTC_INFO"; + signalUpdateConfig1.messageId = signalOverrides.messageId; + rawDataBufferOverridesPerSignal = { signalOverrides }; + std::unordered_map updatedSignals; + updatedSignals = { { 0x1234, signalUpdateConfig1 } }; + rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::none, boost::none, boost::none, rawDataBufferOverridesPerSignal ); + + rawDataBufferManager = std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUDSInterface->start() ); + mUdsModule = + std::make_shared( mNamedSignalDataSource, rawDataBufferManager, mUDSInterface ); + + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue; + targetAddress = static_cast( -1 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_BY_STATUS_MASK ); + udsStatusMaskValue = static_cast( -1 ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + + ISOTPOverCANSenderReceiver senderReceiver; + ISOTPOverCANSenderReceiverOptions senderReceiverOptions; + senderReceiverOptions.mSocketCanIFName = "vcan0"; + senderReceiverOptions.mSourceCANId = 0; + senderReceiverOptions.mDestinationCANId = 0x7DF; + ASSERT_TRUE( senderReceiver.init( senderReceiverOptions ) ); + ASSERT_TRUE( senderReceiver.connect() ); + std::vector rxPDUData; + std::thread receiverThread( &functionalSenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::SUCCESSFUL ); + receiverThread.join(); + senderReceiver.disconnect(); + + CollectedDataFrame collectedDataFrame; + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 1 ); + auto signal = collectedDataFrame.mCollectedSignals[0]; + ASSERT_EQ( signal.value.type, SignalType::STRING ); + ASSERT_EQ( signal.fetchRequestID, 0x03 ); + // Check string content + std::string dtcInfo = + "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"AA\",\"dtcCodes\":[{\"DTC\":" + "\"112233\",\"DTCExtendedData\":\"\",\"DTCSnapshotRecord\":\"\"}]},\"ECUID\":\"01\"}]}"; + auto loanedRawDataFrame = rawDataBufferManager->borrowFrame( + 0x1234, static_cast( signal.getValue().value.uint32Val ) ); + if ( !loanedRawDataFrame.isNull() ) + { + auto data = loanedRawDataFrame.getData(); + auto size = loanedRawDataFrame.getSize(); + std::string dataString = std::string( reinterpret_cast( data ), size ); + ASSERT_EQ( dataString, dtcInfo ); + } +} + +TEST_F( UDSTemplateInterfaceTest, TestDTCSnapshotQueryForSpecificCodeForSpecificRecord ) +{ + std::shared_ptr rawDataBufferManager; + boost::optional rawDataBufferManagerConfig; + RawData::SignalUpdateConfig signalUpdateConfig1; + std::vector rawDataBufferOverridesPerSignal; + signalUpdateConfig1.typeId = 0x1234; + RawData::SignalBufferOverrides signalOverrides; + signalOverrides.interfaceId = "interface1"; + signalUpdateConfig1.interfaceId = signalOverrides.interfaceId; + signalOverrides.messageId = "Vehicle.ECU1.DTC_INFO"; + signalUpdateConfig1.messageId = signalOverrides.messageId; + rawDataBufferOverridesPerSignal = { signalOverrides }; + std::unordered_map updatedSignals; + updatedSignals = { { 0x1234, signalUpdateConfig1 } }; + rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::none, boost::none, boost::none, rawDataBufferOverridesPerSignal ); + + rawDataBufferManager = std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUDSInterface->start() ); + mUdsModule = + std::make_shared( mNamedSignalDataSource, rawDataBufferManager, mUDSInterface ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue, dtcString, recordNumber; + targetAddress = static_cast( 0x01 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_SNAPSHOT_RECORD_BY_DTC_NUMBER ); + udsStatusMaskValue = static_cast( -1 ); + dtcString = static_cast( "0x112233" ); + recordNumber = static_cast( 1 ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + params.push_back( std::move( dtcString ) ); + params.push_back( std::move( recordNumber ) ); + + ISOTPOverCANSenderReceiver senderReceiver; + ISOTPOverCANSenderReceiverOptions senderReceiverOptions; + senderReceiverOptions.mSocketCanIFName = "vcan0"; + senderReceiverOptions.mSourceCANId = 0x456; + senderReceiverOptions.mDestinationCANId = 0x123; + ASSERT_TRUE( senderReceiver.init( senderReceiverOptions ) ); + ASSERT_TRUE( senderReceiver.connect() ); + std::vector rxPDUData; + std::thread receiverThread( &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::SUCCESSFUL ); + CollectedDataFrame collectedDataFrame; + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + receiverThread.join(); + senderReceiver.disconnect(); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 1 ); + auto signal = collectedDataFrame.mCollectedSignals[0]; + ASSERT_EQ( signal.value.type, SignalType::STRING ); + ASSERT_EQ( signal.fetchRequestID, 0x03 ); + // Check string content + std::string dtcInfo = "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"00\",\"dtcCodes\":[{" + "\"DTC\":\"112233\",\"DTCExtendedData\":\"\",\"DTCSnapshotRecord\":" + "\"1122332402014711A666075020\"}]},\"ECUID\":\"01\"}]}"; + auto loanedRawDataFrame = rawDataBufferManager->borrowFrame( + 0x1234, static_cast( signal.getValue().value.uint32Val ) ); + if ( !loanedRawDataFrame.isNull() ) + { + auto data = loanedRawDataFrame.getData(); + auto size = loanedRawDataFrame.getSize(); + std::string dataString = std::string( reinterpret_cast( data ), size ); + ASSERT_EQ( dataString, dtcInfo ); + } +} + +TEST_F( UDSTemplateInterfaceTest, TestDTCExtendedQuery ) +{ + std::shared_ptr rawDataBufferManager; + boost::optional rawDataBufferManagerConfig; + RawData::SignalUpdateConfig signalUpdateConfig1; + std::vector rawDataBufferOverridesPerSignal; + signalUpdateConfig1.typeId = 0x1234; + RawData::SignalBufferOverrides signalOverrides; + signalOverrides.interfaceId = "interface1"; + signalUpdateConfig1.interfaceId = signalOverrides.interfaceId; + signalOverrides.messageId = "Vehicle.ECU1.DTC_INFO"; + signalUpdateConfig1.messageId = signalOverrides.messageId; + rawDataBufferOverridesPerSignal = { signalOverrides }; + std::unordered_map updatedSignals; + updatedSignals = { { 0x1234, signalUpdateConfig1 } }; + rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::none, boost::none, boost::none, rawDataBufferOverridesPerSignal ); + + rawDataBufferManager = std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUDSInterface->start() ); + mUdsModule = + std::make_shared( mNamedSignalDataSource, rawDataBufferManager, mUDSInterface ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue, dtcString, recordNumber; + targetAddress = static_cast( 0x01 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ); + udsStatusMaskValue = static_cast( -1 ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + + ISOTPOverCANSenderReceiver senderReceiver; + ISOTPOverCANSenderReceiverOptions senderReceiverOptions; + senderReceiverOptions.mSocketCanIFName = "vcan0"; + senderReceiverOptions.mSourceCANId = 0x456; + senderReceiverOptions.mDestinationCANId = 0x123; + ASSERT_TRUE( senderReceiver.init( senderReceiverOptions ) ); + ASSERT_TRUE( senderReceiver.connect() ); + std::vector rxPDUData; + std::thread receiverThread( &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::SUCCESSFUL ); + receiverThread.join(); + + std::thread recordIDReceiverThread( &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + recordIDReceiverThread.join(); + + std::thread extendedDataReceiverThread( + &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + extendedDataReceiverThread.join(); + + CollectedDataFrame collectedDataFrame; + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + senderReceiver.disconnect(); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 1 ); + auto signal = collectedDataFrame.mCollectedSignals[0]; + ASSERT_EQ( signal.value.type, SignalType::STRING ); + ASSERT_EQ( signal.fetchRequestID, 0x03 ); + // Check string content + std::string dtcInfo = + "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"FF\",\"dtcCodes\":[{" + "\"DTC\":\"AA1122\",\"DTCExtendedData\":\"AA1122332402014711A666075020\",\"DTCSnapshotRecord\":" + "\"\"}]},\"ECUID\":\"01\"}]}"; + auto loanedRawDataFrame = rawDataBufferManager->borrowFrame( + 0x1234, static_cast( signal.getValue().value.uint32Val ) ); + if ( !loanedRawDataFrame.isNull() ) + { + auto data = loanedRawDataFrame.getData(); + auto size = loanedRawDataFrame.getSize(); + std::string dataString = std::string( reinterpret_cast( data ), size ); + ASSERT_EQ( dataString, dtcInfo ); + } +} + +TEST_F( UDSTemplateInterfaceTest, TestDTCExtendedQueryForSpecificCode ) +{ + std::shared_ptr rawDataBufferManager; + boost::optional rawDataBufferManagerConfig; + RawData::SignalUpdateConfig signalUpdateConfig1; + std::vector rawDataBufferOverridesPerSignal; + signalUpdateConfig1.typeId = 0x1234; + RawData::SignalBufferOverrides signalOverrides; + signalOverrides.interfaceId = "interface1"; + signalUpdateConfig1.interfaceId = signalOverrides.interfaceId; + signalOverrides.messageId = "Vehicle.ECU1.DTC_INFO"; + signalUpdateConfig1.messageId = signalOverrides.messageId; + rawDataBufferOverridesPerSignal = { signalOverrides }; + std::unordered_map updatedSignals; + updatedSignals = { { 0x1234, signalUpdateConfig1 } }; + rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::none, boost::none, boost::none, rawDataBufferOverridesPerSignal ); + + rawDataBufferManager = std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUDSInterface->start() ); + mUdsModule = + std::make_shared( mNamedSignalDataSource, rawDataBufferManager, mUDSInterface ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue, dtcString, recordNumber; + targetAddress = static_cast( 0x01 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ); + udsStatusMaskValue = static_cast( -1 ); + dtcString = static_cast( "0xAA1122" ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + params.push_back( std::move( dtcString ) ); + + ISOTPOverCANSenderReceiver senderReceiver; + ISOTPOverCANSenderReceiverOptions senderReceiverOptions; + senderReceiverOptions.mSocketCanIFName = "vcan0"; + senderReceiverOptions.mSourceCANId = 0x456; + senderReceiverOptions.mDestinationCANId = 0x123; + ASSERT_TRUE( senderReceiver.init( senderReceiverOptions ) ); + ASSERT_TRUE( senderReceiver.connect() ); + std::vector rxPDUData; + std::thread receiverThread( &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::SUCCESSFUL ); + receiverThread.join(); + + std::thread recordIDReceiverThread( &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + recordIDReceiverThread.join(); + + std::thread extendedDataReceiverThread( + &phySenderReceiverSendPDU, std::ref( senderReceiver ), std::ref( rxPDUData ) ); + extendedDataReceiverThread.join(); + + CollectedDataFrame collectedDataFrame; + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + senderReceiver.disconnect(); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 1 ); + auto signal = collectedDataFrame.mCollectedSignals[0]; + ASSERT_EQ( signal.value.type, SignalType::STRING ); + ASSERT_EQ( signal.fetchRequestID, 0x03 ); + // Check string content + std::string dtcInfo = + "{\"DetectedDTCs\":[{\"DTCAndSnapshot\":{\"DTCStatusAvailabilityMask\":\"00\",\"dtcCodes\":[{" + "\"DTC\":\"AA1122\",\"DTCExtendedData\":\"AA1122332402014711A666075020\",\"DTCSnapshotRecord\":" + "\"\"}]},\"ECUID\":\"01\"}]}"; + auto loanedRawDataFrame = rawDataBufferManager->borrowFrame( + 0x1234, static_cast( signal.getValue().value.uint32Val ) ); + if ( !loanedRawDataFrame.isNull() ) + { + auto data = loanedRawDataFrame.getData(); + auto size = loanedRawDataFrame.getSize(); + std::string dataString = std::string( reinterpret_cast( data ), size ); + ASSERT_EQ( dataString, dtcInfo ); + } +} + +TEST_F( UDSTemplateInterfaceTest, InvalidDTCQueryParametersForSnapshotQuery ) +{ + std::shared_ptr rawDataBufferManager; + boost::optional rawDataBufferManagerConfig; + RawData::SignalUpdateConfig signalUpdateConfig1; + std::vector rawDataBufferOverridesPerSignal; + signalUpdateConfig1.typeId = 0x1234; + RawData::SignalBufferOverrides signalOverrides; + signalOverrides.interfaceId = "interface1"; + signalUpdateConfig1.interfaceId = signalOverrides.interfaceId; + signalOverrides.messageId = "Vehicle.ECU1.DTC_INFO"; + signalUpdateConfig1.messageId = signalOverrides.messageId; + rawDataBufferOverridesPerSignal = { signalOverrides }; + std::unordered_map updatedSignals; + updatedSignals = { { 0x1234, signalUpdateConfig1 } }; + rawDataBufferManagerConfig = RawData::BufferManagerConfig::create( + 1_GiB, boost::none, boost::none, boost::none, boost::none, rawDataBufferOverridesPerSignal ); + + rawDataBufferManager = std::make_shared( rawDataBufferManagerConfig.get() ); + rawDataBufferManager->updateConfig( updatedSignals ); + + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + + ASSERT_TRUE( mUDSInterface->init( mEcuConfigs ) ); + ASSERT_TRUE( mUDSInterface->start() ); + mUdsModule = + std::make_shared( mNamedSignalDataSource, rawDataBufferManager, mUDSInterface ); + ASSERT_TRUE( mUdsModule->start() ); + std::vector params; + InspectionValue targetAddress, udsSubFunctionValue, udsStatusMaskValue, dtcString, recordNumber; + targetAddress = static_cast( -1 ); + udsSubFunctionValue = static_cast( UDSSubFunction::DTC_EXT_DATA_RECORD_BY_DTC_NUMBER ); + udsStatusMaskValue = static_cast( -1 ); + dtcString = static_cast( "0xAA1122" ); + recordNumber = static_cast( 1 ); + params.push_back( std::move( targetAddress ) ); + params.push_back( std::move( udsSubFunctionValue ) ); + params.push_back( std::move( udsStatusMaskValue ) ); + params.push_back( std::move( dtcString ) ); + params.push_back( std::move( recordNumber ) ); + + ASSERT_EQ( mUdsModule->DTC_QUERY( 0x1234, 0x03, params ), FetchErrorCode::UNSUPPORTED_PARAMETERS ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/ExternalGpsSourceTest.cpp b/test/unit/ExternalGpsSourceTest.cpp index 53376676..212cbd28 100644 --- a/test/unit/ExternalGpsSourceTest.cpp +++ b/test/unit/ExternalGpsSourceTest.cpp @@ -4,7 +4,8 @@ #include "ExternalGpsSource.h" #include "CollectionInspectionAPITypes.h" #include "IDecoderDictionary.h" -#include "MessageTypes.h" +#include "IDecoderManifest.h" +#include "NamedSignalDataSource.h" #include "QueueTypes.h" #include "SignalTypes.h" #include "VehicleDataSourceTypes.h" @@ -12,7 +13,6 @@ #include #include #include -#include namespace Aws { @@ -22,24 +22,24 @@ namespace IoTFleetWise class ExternalGpsSourceTest : public ::testing::Test { protected: + ExternalGpsSourceTest() + : mSignalBuffer( std::make_shared( 2, "Signal Buffer" ) ) + , mSignalBufferDistributor( std::make_shared() ) + , mNamedSignalDataSource( std::make_shared( "5", mSignalBufferDistributor ) ) + , mExternalGpsSource( std::make_shared( + mNamedSignalDataSource, "Vehicle.CurrentLocation.Latitude", "Vehicle.CurrentLocation.Longitude" ) ) + , mDictionary( std::make_shared() ) + { + mSignalBufferDistributor->registerQueue( mSignalBuffer ); + } + void SetUp() override { - std::unordered_map frameMap; - CANMessageDecoderMethod decoderMethod; - decoderMethod.collectType = CANMessageCollectType::DECODE; - decoderMethod.format.mMessageID = 12345; - CANSignalFormat sig1; - CANSignalFormat sig2; - sig1.mFirstBitPosition = 0; - sig1.mSignalID = 0x1234; - sig2.mFirstBitPosition = 32; - sig2.mSignalID = 0x5678; - decoderMethod.format.mSignals.push_back( sig1 ); - decoderMethod.format.mSignals.push_back( sig2 ); - frameMap[1] = decoderMethod; - mDictionary = std::make_shared(); - mDictionary->canMessageDecoderMethod[1] = frameMap; + mDictionary->customDecoderMethod["5"]["Vehicle.CurrentLocation.Latitude"] = + CustomSignalDecoderFormat{ "5", "Vehicle.CurrentLocation.Latitude", 0x1234, SignalType::DOUBLE }; + mDictionary->customDecoderMethod["5"]["Vehicle.CurrentLocation.Longitude"] = + CustomSignalDecoderFormat{ "5", "Vehicle.CurrentLocation.Longitude", 0x5678, SignalType::DOUBLE }; } void @@ -47,28 +47,25 @@ class ExternalGpsSourceTest : public ::testing::Test { } - std::shared_ptr mDictionary; + std::shared_ptr mSignalBuffer; + SignalBufferDistributorPtr mSignalBufferDistributor; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr mExternalGpsSource; + std::shared_ptr mDictionary; }; // Test if valid gps data TEST_F( ExternalGpsSourceTest, testDecoding ) // NOLINT { - auto signalBuffer = std::make_shared( 100, "Signal Buffer" ); - auto signalBufferDistributor = std::make_shared(); - signalBufferDistributor->registerQueue( signalBuffer ); - ExternalGpsSource gpsSource( signalBufferDistributor ); - ASSERT_FALSE( gpsSource.init( INVALID_CAN_SOURCE_NUMERIC_ID, 1, 0, 32 ) ); - ASSERT_TRUE( gpsSource.init( 1, 1, 0, 32 ) ); - gpsSource.start(); - gpsSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); CollectedDataFrame collectedDataFrame; - DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); + DELAY_ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); - gpsSource.setLocation( 360, 360 ); // Invalid - gpsSource.setLocation( 52.5761, 12.5761 ); + mExternalGpsSource->setLocation( 360, 360 ); // Invalid + mExternalGpsSource->setLocation( 52.5761, 12.5761 ); - WAIT_ASSERT_TRUE( signalBuffer->pop( collectedDataFrame ) ); + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); auto firstSignal = collectedDataFrame.mCollectedSignals[0]; auto secondSignal = collectedDataFrame.mCollectedSignals[1]; @@ -77,38 +74,24 @@ TEST_F( ExternalGpsSourceTest, testDecoding ) // NOLINT ASSERT_EQ( secondSignal.signalID, 0x5678 ); ASSERT_NEAR( firstSignal.value.value.doubleVal, 52.5761, 0.0001 ); ASSERT_NEAR( secondSignal.value.value.doubleVal, 12.5761, 0.0001 ); - - ASSERT_TRUE( gpsSource.init( 1, 1, 123, 456 ) ); // Invalid start bits - gpsSource.setLocation( 52.5761, 12.5761 ); - DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); - - ASSERT_TRUE( gpsSource.stop() ); } // Test longitude west TEST_F( ExternalGpsSourceTest, testWestNegativeLongitude ) // NOLINT { - SignalBufferPtr signalBuffer = std::make_shared( 100, "Signal Buffer" ); - auto signalBufferDistributor = std::make_shared(); - signalBufferDistributor->registerQueue( signalBuffer ); - ExternalGpsSource gpsSource( signalBufferDistributor ); - gpsSource.init( 1, 1, 0, 32 ); - gpsSource.start(); CollectedDataFrame collectedDataFrame; - DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); - gpsSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); - DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); - gpsSource.setLocation( 52.5761, -12.5761 ); + DELAY_ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + DELAY_ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + mExternalGpsSource->setLocation( 52.5761, -12.5761 ); - WAIT_ASSERT_TRUE( signalBuffer->pop( collectedDataFrame ) ); + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); auto firstSignal = collectedDataFrame.mCollectedSignals[0]; auto secondSignal = collectedDataFrame.mCollectedSignals[1]; ASSERT_EQ( firstSignal.signalID, 0x1234 ); ASSERT_EQ( secondSignal.signalID, 0x5678 ); ASSERT_NEAR( firstSignal.value.value.doubleVal, 52.5761, 0.0001 ); ASSERT_NEAR( secondSignal.value.value.doubleVal, -12.5761, 0.0001 ); // negative number - - ASSERT_TRUE( gpsSource.stop() ); } } // namespace IoTFleetWise diff --git a/test/unit/IWaveGpsSourceTest.cpp b/test/unit/IWaveGpsSourceTest.cpp index 6d56fb88..13ea6442 100644 --- a/test/unit/IWaveGpsSourceTest.cpp +++ b/test/unit/IWaveGpsSourceTest.cpp @@ -4,7 +4,8 @@ #include "IWaveGpsSource.h" #include "CollectionInspectionAPITypes.h" #include "IDecoderDictionary.h" -#include "MessageTypes.h" +#include "IDecoderManifest.h" +#include "NamedSignalDataSource.h" #include "QueueTypes.h" #include "SignalTypes.h" #include "VehicleDataSourceTypes.h" @@ -17,7 +18,6 @@ #include #include #include -#include namespace Aws { @@ -27,32 +27,35 @@ namespace IoTFleetWise class IWaveGpsSourceTest : public ::testing::Test { protected: + IWaveGpsSourceTest() + : mSignalBuffer( std::make_shared( 2, "Signal Buffer" ) ) + , mSignalBufferDistributor( std::make_shared() ) + , mNamedSignalDataSource( std::make_shared( "5", mSignalBufferDistributor ) ) + , mDictionary( std::make_shared() ) + { + mSignalBufferDistributor->registerQueue( mSignalBuffer ); + } + void SetUp() override { + mDictionary->customDecoderMethod["5"]["Vehicle.CurrentLocation.Latitude"] = + CustomSignalDecoderFormat{ "5", "Vehicle.CurrentLocation.Latitude", 0x1234, SignalType::DOUBLE }; + mDictionary->customDecoderMethod["5"]["Vehicle.CurrentLocation.Longitude"] = + CustomSignalDecoderFormat{ "5", "Vehicle.CurrentLocation.Longitude", 0x5678, SignalType::DOUBLE }; - std::unordered_map frameMap; - CANMessageDecoderMethod decoderMethod; - decoderMethod.collectType = CANMessageCollectType::DECODE; - decoderMethod.format.mMessageID = 12345; - CANSignalFormat sig1; - CANSignalFormat sig2; - sig1.mFirstBitPosition = 0; - sig1.mSignalID = 0x1234; - sig2.mFirstBitPosition = 32; - sig2.mSignalID = 0x5678; - decoderMethod.format.mSignals.push_back( sig1 ); - decoderMethod.format.mSignals.push_back( sig2 ); - frameMap[1] = decoderMethod; - mDictionary = std::make_shared(); - mDictionary->canMessageDecoderMethod[1] = frameMap; char bufferFilePath[PATH_MAX]; if ( getcwd( bufferFilePath, sizeof( bufferFilePath ) ) != nullptr ) { - filePath = bufferFilePath; - filePath += "/testGpsNMEA.txt"; - std::cout << " File being saved here: " << filePath << std::endl; - nmeaFile = std::make_unique( filePath ); + mFilePath = bufferFilePath; + mFilePath += "/testGpsNMEA.txt"; + std::cout << "File being saved here: " << mFilePath << std::endl; + mNmeaFile = std::make_unique( mFilePath ); + mIWaveGpsSource = std::make_shared( mNamedSignalDataSource, + mFilePath, + "Vehicle.CurrentLocation.Latitude", + "Vehicle.CurrentLocation.Longitude", + 1000 ); } } @@ -61,35 +64,32 @@ class IWaveGpsSourceTest : public ::testing::Test { } - std::shared_ptr mDictionary; - std::unique_ptr nmeaFile; - std::string filePath; + std::string mFilePath; + std::unique_ptr mNmeaFile; + std::shared_ptr mSignalBuffer; + SignalBufferDistributorPtr mSignalBufferDistributor; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr mIWaveGpsSource; + std::shared_ptr mDictionary; }; // Test if valid gps data TEST_F( IWaveGpsSourceTest, testDecoding ) { - auto signalBuffer = std::make_shared( 100, "Signal Buffer" ); - auto signalBufferDistributor = std::make_shared(); - signalBufferDistributor->registerQueue( signalBuffer ); - // Random data so checksum etc. will not be valid - *nmeaFile << "$GPGSV,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25*26\n" - "GPGSV,27,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25*26\\n\n" - "GPGSV,28,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25*26\\n\n" - "$GPGGA,133120.00,5234.56789,N,01234.56789,E,1,08,0.6,123.4,M,56.7,M,,*89\n\n" - "$GPVTG,29.30,T,31.32,M,33.34,N,35.36,K,A*37C\n\n\n"; - nmeaFile->close(); - IWaveGpsSource gpsSource( signalBufferDistributor ); - ASSERT_FALSE( gpsSource.init( filePath, INVALID_CAN_SOURCE_NUMERIC_ID, 1, 0, 32 ) ); - ASSERT_TRUE( gpsSource.init( filePath, 1, 1, 0, 32 ) ); - gpsSource.connect(); - gpsSource.start(); + ASSERT_TRUE( mIWaveGpsSource->connect() ); CollectedDataFrame collectedDataFrame; - DELAY_ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); - gpsSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + DELAY_ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + // Random data so checksum etc. will not be valid + *mNmeaFile << "$GPGSV,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25*26\n" + "GPGSV,27,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25*26\\n\n" + "GPGSV,28,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25*26\\n\n" + "$GPGGA,133120.00,5234.56789,N,01234.56789,E,1,08,0.6,123.4,M,56.7,M,,*89\n\n" + "$GPVTG,29.30,T,31.32,M,33.34,N,35.36,K,A*37C\n\n\n"; + mNmeaFile->close(); - WAIT_ASSERT_TRUE( signalBuffer->pop( collectedDataFrame ) ); + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); auto firstSignal = collectedDataFrame.mCollectedSignals[0]; auto secondSignal = collectedDataFrame.mCollectedSignals[1]; @@ -99,29 +99,19 @@ TEST_F( IWaveGpsSourceTest, testDecoding ) // raw value from NMEA 5234.56789 01234.56789 converted to DD ASSERT_NEAR( firstSignal.value.value.doubleVal, 52.5761, 0.0001 ); ASSERT_NEAR( secondSignal.value.value.doubleVal, 12.5761, 0.0001 ); - - ASSERT_TRUE( gpsSource.stop() ); - ASSERT_TRUE( gpsSource.disconnect() ); } // Test longitude west TEST_F( IWaveGpsSourceTest, testWestNegativeLongitude ) { - SignalBufferPtr signalBuffer = std::make_shared( 100, "Signal Buffer" ); - auto signalBufferDistributor = std::make_shared(); - signalBufferDistributor->registerQueue( signalBuffer ); - + ASSERT_TRUE( mIWaveGpsSource->connect() ); + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); // instead of E for east now, W for West - *nmeaFile << "$GPGGA,133120.00,5234.56789,N,01234.56789,W,1,08,0.6,123.4,M,56.7,M,,*89\n\n"; - nmeaFile->close(); - IWaveGpsSource gpsSource( signalBufferDistributor ); - gpsSource.init( filePath, 1, 1, 0, 32 ); - gpsSource.connect(); - gpsSource.start(); - gpsSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + *mNmeaFile << "$GPGGA,133120.00,5234.56789,N,01234.56789,W,1,08,0.6,123.4,M,56.7,M,,*89\n\n"; + mNmeaFile->close(); CollectedDataFrame collectedDataFrame; - WAIT_ASSERT_TRUE( signalBuffer->pop( collectedDataFrame ) ); + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); auto firstSignal = collectedDataFrame.mCollectedSignals[0]; auto secondSignal = collectedDataFrame.mCollectedSignals[1]; @@ -131,9 +121,6 @@ TEST_F( IWaveGpsSourceTest, testWestNegativeLongitude ) // raw value from NMEA 5234.56789 01234.56789 converted to DD ASSERT_NEAR( firstSignal.value.value.doubleVal, 52.5761, 0.0001 ); ASSERT_NEAR( secondSignal.value.value.doubleVal, -12.5761, 0.0001 ); // negative number - - ASSERT_TRUE( gpsSource.stop() ); - ASSERT_TRUE( gpsSource.disconnect() ); } } // namespace IoTFleetWise diff --git a/test/unit/InspectionMatrixExtractorTest.cpp b/test/unit/InspectionMatrixExtractorTest.cpp index 6044da36..9db2d0eb 100644 --- a/test/unit/InspectionMatrixExtractorTest.cpp +++ b/test/unit/InspectionMatrixExtractorTest.cpp @@ -5,18 +5,15 @@ #include "CheckinSender.h" #include "CollectionSchemeManagerMock.h" #include "CollectionSchemeManagerTest.h" // IWYU pragma: associated +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include #include #include #include #include #include -#ifdef FWE_FEATURE_VISION_SYSTEM_DATA -#include "RawDataBufferManagerSpy.h" -#include "RawDataManager.h" -#include -#endif - namespace Aws { namespace IoTFleetWise @@ -26,9 +23,9 @@ using ::testing::_; using ::testing::NiceMock; /** @brief - * This test aims to test PM's functionality to invoke Inspection Engine on Inspection Matrix update + * This test aims to test PM's functionality to invoke Inspection Engine on Inspection Matrix and Fetch Matrix update * Step1: Register two Inspection Engines are listeners - * Step2: Invoke Matrix Updater and check if two Inspection Engines receive Inspection Matrix update + * Step2: Invoke Matrix Updater and check if two Inspection Engines receive Inspection Matrix and Fetch Matrix update */ TEST( InspectionMatrixExtractorTest, MatrixUpdaterTest ) { @@ -63,6 +60,40 @@ TEST( InspectionMatrixExtractorTest, MatrixUpdaterTest ) // Check if both two consumers set Inspection Matrix update flag ASSERT_TRUE( InspectionEnginePtr1->getInspectionMatrixUpdateFlag() ); ASSERT_TRUE( InspectionEnginePtr2->getInspectionMatrixUpdateFlag() ); + + // Register Inspection Engine #1 as listeners to Fetch Matrix update + testPtr->subscribeToFetchMatrixChange( std::bind( &CollectionInspectionWorkerThreadMock::onChangeFetchMatrix, + InspectionEnginePtr1.get(), + std::placeholders::_1 ) ); + + // Clear Fetch Matrix update flag (this flag only exist in mock class for testing purpose) + InspectionEnginePtr1->setFetchMatrixUpdateFlag( false ); + InspectionEnginePtr2->setFetchMatrixUpdateFlag( false ); + + // Invoke Fetch Matrix updater + testPtr->fetchMatrixUpdater( std::make_shared() ); + + // Check if both two consumers set Fetch Matrix update flag appropriately + // Only Inspection Engine #1 should receive Fetch Matrix update + ASSERT_TRUE( InspectionEnginePtr1->getFetchMatrixUpdateFlag() ); + ASSERT_FALSE( InspectionEnginePtr2->getFetchMatrixUpdateFlag() ); + + // Register Inspection Engine #2 as listeners to Fetch Matrix update, too + testPtr->subscribeToFetchMatrixChange( std::bind( &CollectionInspectionWorkerThreadMock::onChangeFetchMatrix, + InspectionEnginePtr2.get(), + std::placeholders::_1 ) ); + + // Clear Fetch Matrix update flag (this flag only exist in mock class for testing purpose) + InspectionEnginePtr1->setFetchMatrixUpdateFlag( false ); + InspectionEnginePtr2->setFetchMatrixUpdateFlag( false ); + + // Invoke Fetch Matrix updater + testPtr->fetchMatrixUpdater( std::make_shared() ); + + // Check if both two consumers set Fetch Matrix update flag appropriately + // Both Inspection Engines should receive Fetch Matrix update + ASSERT_TRUE( InspectionEnginePtr1->getFetchMatrixUpdateFlag() ); + ASSERT_TRUE( InspectionEnginePtr2->getFetchMatrixUpdateFlag() ); } ExpressionNode * @@ -182,7 +213,8 @@ TEST( CollectionSchemeManager, InspectionMatrixExtractorTreeTest ) // All three polices are expected to be enabled ASSERT_TRUE( test.updateMapsandTimeLine( { 0, 0 } ) ); std::shared_ptr output = std::make_shared(); - test.matrixExtractor( output ); + std::shared_ptr fetchMatrixOutput = std::make_shared(); + test.matrixExtractor( output, fetchMatrixOutput ); /* exam output */ for ( uint32_t i = 0; i < output->expressionNodeStorage.size(); i++ ) @@ -232,7 +264,8 @@ TEST( CollectionSchemeManager, InspectionMatrixExtractorConditionDataTest ) ASSERT_TRUE( test.updateMapsandTimeLine( { 0, 0 } ) ); std::shared_ptr output = std::make_shared(); - test.matrixExtractor( output ); + std::shared_ptr fetchMatrixOutput = std::make_shared(); + test.matrixExtractor( output, fetchMatrixOutput ); for ( auto conditionData : output->conditions ) { // Signals @@ -254,6 +287,417 @@ TEST( CollectionSchemeManager, InspectionMatrixExtractorConditionDataTest ) } } +static void +addSignalCollectionInfo( std::vector &collectSignals, + SignalID signalID, + uint32_t sampleBufferSize, + uint32_t minimumSamplePeriodMs, + uint32_t fixedWindowPeriodMs, + bool conditionOnlySignal ) +{ + collectSignals.emplace_back(); + + SignalCollectionInfo &collectSignal = collectSignals.back(); + + collectSignal.signalID = signalID; + collectSignal.sampleBufferSize = sampleBufferSize; + collectSignal.minimumSampleIntervalMs = minimumSamplePeriodMs; + collectSignal.fixedWindowPeriod = fixedWindowPeriodMs; + collectSignal.isConditionOnlySignal = conditionOnlySignal; +} + +static void +addCanFrameCollectionInfo( std::vector &collectRawCanFrames, + CANRawFrameID canMessageID, + InterfaceID canInterfaceID, + uint32_t sampleBufferSize, + uint32_t minimumSamplePeriodMs ) +{ + collectRawCanFrames.emplace_back(); + + CanFrameCollectionInfo &collectRawCanFrame = collectRawCanFrames.back(); + + collectRawCanFrame.frameID = canMessageID; + collectRawCanFrame.interfaceID = canInterfaceID; + collectRawCanFrame.sampleBufferSize = sampleBufferSize; + collectRawCanFrame.minimumSampleIntervalMs = minimumSamplePeriodMs; +} + +static void +addFetchInformation( std::vector &fetchInformations, + const std::shared_ptr> &nodesForFetchCondition, + const std::shared_ptr> &nodesForFetchAction, + SignalID signalID, + bool isTimeBased, + uint64_t maxExecutionCount, + uint64_t executionFrequencyMs, + uint64_t resetMaxExecutionCountIntervalMs, + bool isActionValid, + bool isActionParamValid, + ExpressionNodeType nodeType ) +{ + fetchInformations.emplace_back(); + + FetchInformation &fetchInformation = fetchInformations.back(); + + fetchInformation.signalID = signalID; + + if ( isTimeBased ) + { + fetchInformation.maxExecutionPerInterval = maxExecutionCount; + fetchInformation.executionPeriodMs = executionFrequencyMs; + fetchInformation.executionIntervalMs = resetMaxExecutionCountIntervalMs; + } + else + { + nodesForFetchCondition->emplace_back(); + + ExpressionNode &fetchInformationCondition = nodesForFetchCondition->back(); + + fetchInformation.condition = &fetchInformationCondition; + fetchInformation.triggerOnlyOnRisingEdge = true; + + fetchInformationCondition.nodeType = ExpressionNodeType::SIGNAL; + fetchInformationCondition.signalID = signalID; + } + + nodesForFetchAction->emplace_back(); + + ExpressionNode &fetchInformationAction = nodesForFetchAction->back(); + + fetchInformation.actions.push_back( &fetchInformationAction ); + + if ( isActionValid ) + { + // action valid per inspection matrix extractor only (not guarantee to be valid through whole system) + fetchInformationAction.nodeType = ExpressionNodeType::CUSTOM_FUNCTION; + fetchInformationAction.function.customFunctionName = "Custom_Function_Name"; + + nodesForFetchAction->emplace_back(); + + ExpressionNode &customFunctionParam = nodesForFetchAction->back(); + + fetchInformationAction.function.customFunctionParams.push_back( &customFunctionParam ); + + if ( isActionParamValid ) + { + switch ( nodeType ) + { + case ExpressionNodeType::BOOLEAN: + customFunctionParam.nodeType = ExpressionNodeType::BOOLEAN; + customFunctionParam.booleanValue = true; + break; + case ExpressionNodeType::FLOAT: + customFunctionParam.nodeType = ExpressionNodeType::FLOAT; + customFunctionParam.floatingValue = 1.0; + break; + case ExpressionNodeType::STRING: + customFunctionParam.nodeType = ExpressionNodeType::STRING; + customFunctionParam.stringValue = "test_string"; + break; + default: + customFunctionParam.nodeType = ExpressionNodeType::NONE; + break; + } + } + else + { + customFunctionParam.nodeType = ExpressionNodeType::NONE; + } + } + else + { + fetchInformationAction.nodeType = ExpressionNodeType::NONE; + } +} + +TEST( CollectionSchemeManager, MatrixExtractorTest ) +{ + std::vector collectSignals; + std::vector collectRawCanFrames; + std::vector fetchInformations; + ExpressionNode *tree = new ExpressionNode(); + tree->nodeType = ExpressionNodeType::IS_NULL_FUNCTION; + tree->left = new ExpressionNode(); + tree->left->nodeType = ExpressionNodeType::SIGNAL; + tree->left->signalID = 1; + + std::shared_ptr> nodesForFetchCondition = + std::make_shared>(); + std::shared_ptr> nodesForFetchAction = std::make_shared>(); + + const SignalID signalID1 = 10; + const SignalID signalID2 = 20; + const SignalID signalID3 = 30; + + addSignalCollectionInfo( collectSignals, signalID1, 11, 12, 13, false ); + addSignalCollectionInfo( collectSignals, signalID2, 21, 22, 23, false ); + addSignalCollectionInfo( collectSignals, signalID3, 31, 32, 33, true ); + addCanFrameCollectionInfo( collectRawCanFrames, 100, "110", 120, 130 ); + + // refer to addFetchInformation implementation + // each FetchInformations need 0-1 ExpressionNode for condition and 1-2 ExpressionNodes for actions + // we're going to add 7 FetchInformations + // so that we will need 0-7 ExpressionNodes for conditions and 7-14 ExpressionNodes for actions + nodesForFetchCondition->reserve( 7 ); + nodesForFetchAction->reserve( 14 ); + + // time-based fetch for signalID1 => accepted (FetchRequestID 0) + // action is a custom function with name "Custom_Function_Name" and double parameter + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID1, + true, + 1000, + 1100, + 1200, + true, + true, + ExpressionNodeType::FLOAT ); + // condition-based fetch for signalID1 => accepted (FetchRequestID 1) + // action is a custom function with name "Custom_Function_Name" and string parameter + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID1, + false, + 2000, + 2100, + 2200, + true, + true, + ExpressionNodeType::STRING ); + // time-based fetch for signalID1 => not accepted + // action is not a custom function + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID1, + true, + 3000, + 3100, + 3200, + false, + true, + ExpressionNodeType::FLOAT ); + // condition-based fetch for signalID1 => not accepted + // action is a custom function but custom function parameter is neither bool nor double nor string value + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID1, + false, + 4000, + 4100, + 4200, + true, + false, + ExpressionNodeType::NONE ); + // time-based fetch for signalID2 => accepted (FetchRequestID 2) + // action is a custom function with name "Custom_Function_Name" and bool parameter + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID2, + true, + 5000, + 5100, + 5200, + true, + true, + ExpressionNodeType::BOOLEAN ); + // condition-based fetch for signalID2 => accepted (FetchRequestID 3) + // action is a custom function with name "Custom_Function_Name" and bool parameter + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID2, + false, + 6000, + 6100, + 6200, + true, + true, + ExpressionNodeType::BOOLEAN ); + // time-based fetch for signalID1 => not accepted + // Fetch frequency can't be 0 + addFetchInformation( fetchInformations, + nodesForFetchCondition, + nodesForFetchAction, + signalID1, + true, + 1000, + 0, + 0, + true, + true, + ExpressionNodeType::FLOAT ); + + ICollectionSchemePtr collectionScheme = std::make_shared( "CollectionScheme 1", + "DecoderManifest 1", + 10000, + 11000, + 12000, + 13000, + true, + false, + 14000, + false, + true, + collectSignals, + collectRawCanFrames, + tree, + fetchInformations ); + std::vector collectionSchemes; + + collectionSchemes.emplace_back( collectionScheme ); + + CANInterfaceIDTranslator canIDTranslator; + + canIDTranslator.add( "110" ); + + CollectionSchemeManagerWrapper test( + nullptr, canIDTranslator, std::make_shared( nullptr ), "DecoderManifest 1" ); + IDecoderManifestPtr decoderManifest = std::make_shared( "DecoderManifest 1" ); + ICollectionSchemeListPtr collectionSchemeList = std::make_shared( collectionSchemes ); + + test.setDecoderManifest( decoderManifest ); + test.setCollectionSchemeList( collectionSchemeList ); + + ASSERT_TRUE( test.updateMapsandTimeLine( { 10000, 10000 } ) ); + + std::shared_ptr inspectionMatrix = std::make_shared(); + std::shared_ptr fetchMatrix = std::make_shared(); + + test.matrixExtractor( inspectionMatrix, fetchMatrix ); + + // total 4 FetchRequestIDs + // FetchRequestID 0 is time-based and for signalID1 + // FetchRequestID 1 is condition-based and for signalID1 + // FetchRequestID 2 is time-based and for signalID2 + // FetchRequestID 3 is condition-based and for signalID2 + ASSERT_EQ( fetchMatrix->fetchRequests.size(), 4 ); + + ASSERT_EQ( fetchMatrix->fetchRequests.count( 0 ), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[0].size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[0].at( 0 ).signalID, signalID1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[0].at( 0 ).functionName, "Custom_Function_Name" ); + ASSERT_EQ( fetchMatrix->fetchRequests[0].at( 0 ).args.size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[0].at( 0 ).args.at( 0 ).type, InspectionValue::DataType::DOUBLE ); + ASSERT_EQ( fetchMatrix->fetchRequests[0].at( 0 ).args.at( 0 ).doubleVal, 1.0 ); + + ASSERT_EQ( fetchMatrix->fetchRequests.count( 1 ), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[1].size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[1].at( 0 ).signalID, signalID1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[1].at( 0 ).functionName, "Custom_Function_Name" ); + ASSERT_EQ( fetchMatrix->fetchRequests[1].at( 0 ).args.size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[1].at( 0 ).args.at( 0 ).type, InspectionValue::DataType::STRING ); + ASSERT_EQ( *( fetchMatrix->fetchRequests[1].at( 0 ).args.at( 0 ).stringVal ), "test_string" ); + + ASSERT_EQ( fetchMatrix->fetchRequests.count( 2 ), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[2].size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[2].at( 0 ).signalID, signalID2 ); + ASSERT_EQ( fetchMatrix->fetchRequests[2].at( 0 ).functionName, "Custom_Function_Name" ); + ASSERT_EQ( fetchMatrix->fetchRequests[2].at( 0 ).args.size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[2].at( 0 ).args.at( 0 ).type, InspectionValue::DataType::BOOL ); + ASSERT_EQ( fetchMatrix->fetchRequests[2].at( 0 ).args.at( 0 ).boolVal, true ); + + ASSERT_EQ( fetchMatrix->fetchRequests.count( 3 ), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[3].size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[3].at( 0 ).signalID, signalID2 ); + ASSERT_EQ( fetchMatrix->fetchRequests[3].at( 0 ).functionName, "Custom_Function_Name" ); + ASSERT_EQ( fetchMatrix->fetchRequests[3].at( 0 ).args.size(), 1 ); + ASSERT_EQ( fetchMatrix->fetchRequests[3].at( 0 ).args.at( 0 ).type, InspectionValue::DataType::BOOL ); + ASSERT_EQ( fetchMatrix->fetchRequests[3].at( 0 ).args.at( 0 ).boolVal, true ); + + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup.size(), 2 ); + + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup.count( 0 ), 1 ); + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup[0].maxExecutionCount, 1000 ); + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup[0].fetchFrequencyMs, 1100 ); + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup[0].maxExecutionCountResetPeriodMs, 1200 ); + + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup.count( 2 ), 1 ); + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup[2].maxExecutionCount, 5000 ); + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup[2].fetchFrequencyMs, 5100 ); + ASSERT_EQ( fetchMatrix->periodicalFetchRequestSetup[2].maxExecutionCountResetPeriodMs, 5200 ); + + // main condition is nullptr => contribute 0 ExpressionNode + // FetchRequestID 1 is condition-based => contribute 1 ExpressionNode (because ExpressionNodeType::SIGNAL is used) + // FetchRequestID 3 is condition-based => contribute 1 ExpressionNode (because ExpressionNodeType::SIGNAL is used) + // Two more nodes for condition expression + ASSERT_EQ( inspectionMatrix->expressionNodeStorage.size(), 4 ); + + for ( auto conditionData : inspectionMatrix->conditions ) + { + ASSERT_NE( conditionData.condition, nullptr ); + ASSERT_EQ( conditionData.minimumPublishIntervalMs, 12000 ); + ASSERT_EQ( conditionData.afterDuration, 13000 ); + ASSERT_EQ( conditionData.includeActiveDtcs, true ); + ASSERT_EQ( conditionData.triggerOnlyOnRisingEdge, false ); + ASSERT_EQ( conditionData.alwaysEvaluateCondition, true ); + + // metadata + ASSERT_EQ( conditionData.metadata.collectionSchemeID, "CollectionScheme 1" ); + ASSERT_EQ( conditionData.metadata.decoderID, "DecoderManifest 1" ); + ASSERT_EQ( conditionData.metadata.priority, 14000 ); + ASSERT_EQ( conditionData.metadata.persist, false ); + ASSERT_EQ( conditionData.metadata.compress, true ); + + // signals + ASSERT_EQ( conditionData.signals.size(), 3 ); + + ASSERT_EQ( conditionData.signals[0].signalID, signalID1 ); + ASSERT_EQ( conditionData.signals[0].sampleBufferSize, 11 ); + ASSERT_EQ( conditionData.signals[0].minimumSampleIntervalMs, 12 ); + ASSERT_EQ( conditionData.signals[0].fixedWindowPeriod, 13 ); + ASSERT_EQ( conditionData.signals[0].isConditionOnlySignal, false ); + ASSERT_EQ( conditionData.signals[0].fetchRequestIDs.size(), 2 ); + ASSERT_EQ( conditionData.signals[0].fetchRequestIDs[0], 0 ); + ASSERT_EQ( conditionData.signals[0].fetchRequestIDs[1], 1 ); + + ASSERT_EQ( conditionData.signals[1].signalID, signalID2 ); + ASSERT_EQ( conditionData.signals[1].sampleBufferSize, 21 ); + ASSERT_EQ( conditionData.signals[1].minimumSampleIntervalMs, 22 ); + ASSERT_EQ( conditionData.signals[1].fixedWindowPeriod, 23 ); + ASSERT_EQ( conditionData.signals[1].isConditionOnlySignal, false ); + ASSERT_EQ( conditionData.signals[1].fetchRequestIDs.size(), 2 ); + ASSERT_EQ( conditionData.signals[1].fetchRequestIDs[0], 2 ); + ASSERT_EQ( conditionData.signals[1].fetchRequestIDs[1], 3 ); + + ASSERT_EQ( conditionData.signals[2].signalID, signalID3 ); + ASSERT_EQ( conditionData.signals[2].sampleBufferSize, 31 ); + ASSERT_EQ( conditionData.signals[2].minimumSampleIntervalMs, 32 ); + ASSERT_EQ( conditionData.signals[2].fixedWindowPeriod, 33 ); + ASSERT_EQ( conditionData.signals[2].isConditionOnlySignal, true ); + ASSERT_EQ( conditionData.signals[2].fetchRequestIDs.size(), 0 ); + + // canFrames + ASSERT_EQ( conditionData.canFrames.size(), 1 ); + ASSERT_EQ( conditionData.canFrames[0].frameID, 100 ); + ASSERT_EQ( canIDTranslator.getInterfaceID( conditionData.canFrames[0].channelID ), "110" ); + ASSERT_EQ( conditionData.canFrames[0].sampleBufferSize, 120 ); + ASSERT_EQ( conditionData.canFrames[0].minimumSampleIntervalMs, 130 ); + + // fetchConditions + ASSERT_EQ( conditionData.fetchConditions.size(), 2 ); + + ASSERT_EQ( conditionData.fetchConditions[0].condition, &inspectionMatrix->expressionNodeStorage[2] ); + ASSERT_EQ( conditionData.fetchConditions[0].condition->nodeType, ExpressionNodeType::SIGNAL ); + ASSERT_EQ( conditionData.fetchConditions[0].condition->signalID, signalID1 ); + ASSERT_EQ( conditionData.fetchConditions[0].triggerOnlyOnRisingEdge, true ); + ASSERT_EQ( conditionData.fetchConditions[0].fetchRequestID, 1 ); + + ASSERT_EQ( conditionData.fetchConditions[1].condition, &inspectionMatrix->expressionNodeStorage[3] ); + ASSERT_EQ( conditionData.fetchConditions[1].condition->nodeType, ExpressionNodeType::SIGNAL ); + ASSERT_EQ( conditionData.fetchConditions[1].condition->signalID, signalID2 ); + ASSERT_EQ( conditionData.fetchConditions[1].triggerOnlyOnRisingEdge, true ); + ASSERT_EQ( conditionData.fetchConditions[1].fetchRequestID, 3 ); + } + deleteTree( tree ); +} + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA /** @brief * This test aims to test PM's functionality to create and update the RawBuffer Config on Inspection Matrix Update @@ -377,7 +821,15 @@ TEST( InspectionMatrixExtractorTest, InspectionMatrixRawBufferConfigUpdaterWithC signal3.sampleBufferSize = 3; signal3.minimumSampleIntervalMs = 3; signal3.fixedWindowPeriod = 4; - std::vector testSignals = { signal1, signal2, signal3 }; + + struct SignalCollectionInfo signal4; + // Range 0x2000-0x3000 is used for custom decoding in the unit test mock + signal4.signalID = 0x2001; + signal4.sampleBufferSize = 2; + signal4.minimumSampleIntervalMs = 3; + signal4.fixedWindowPeriod = 4; + + std::vector testSignals = { signal1, signal2, signal3, signal4 }; std::vector testCANFrames = {}; ICollectionSchemePtr collectionScheme = std::make_shared( "COLLECTIONSCHEME1", "DMBM1", 0, 10, testSignals, testCANFrames ); @@ -395,12 +847,19 @@ TEST( InspectionMatrixExtractorTest, InspectionMatrixRawBufferConfigUpdaterWithC std::unordered_map> formatMap; std::unordered_map> signalToFrameAndNodeID; std::unordered_map signalIDToPIDDecoderFormat; + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat = { + { 0x2001, CustomSignalDecoderFormat{ "30", "custom-decoder-0", 0x2001, SignalType::STRING } } }; std::unordered_map complexDataTypeMap; std::unordered_map complexSignalMap = { { signal1.signalID, { "interfaceId1", "ImageTopic:sensor_msgs/msg/Image", 100 } }, { signal2.signalID, { "interfaceId2", "PointFieldTopic:sensor_msgs/msg/PointField", 200 } } }; - IDecoderManifestPtr DMBM1 = std::make_shared( - "DMBM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, complexSignalMap, complexDataTypeMap ); + IDecoderManifestPtr DMBM1 = std::make_shared( "DMBM1", + formatMap, + signalToFrameAndNodeID, + signalIDToPIDDecoderFormat, + signalIDToCustomDecoderFormat, + complexSignalMap, + complexDataTypeMap ); ICollectionSchemeListPtr PL1 = std::make_shared( list1 ); test.setDecoderManifest( DMBM1 ); test.setCollectionSchemeList( PL1 ); @@ -433,8 +892,9 @@ TEST( InspectionMatrixExtractorTest, InspectionMatrixRawBufferConfigUpdaterWithC } ); // Verify that the list of updatedSignals isn't overwritten test.updateRawDataBufferConfigComplexSignals( complexDataDictionary, updatedSignals ); + test.updateRawDataBufferConfigStringSignals( updatedSignals ); - ASSERT_EQ( updatedSignals.size(), 3 ); + ASSERT_EQ( updatedSignals.size(), 4 ); ASSERT_NE( updatedSignals.find( signal1.signalID ), updatedSignals.end() ); ASSERT_EQ( updatedSignals[signal1.signalID].typeId, signal1.signalID ); @@ -453,12 +913,97 @@ TEST( InspectionMatrixExtractorTest, InspectionMatrixRawBufferConfigUpdaterWithC ASSERT_EQ( updatedSignals[signal3.signalID].interfaceId, "" ); ASSERT_EQ( updatedSignals[signal3.signalID].messageId, "" ); + ASSERT_NE( updatedSignals.find( signal4.signalID ), updatedSignals.end() ); + ASSERT_EQ( updatedSignals[signal4.signalID].typeId, signal4.signalID ); + ASSERT_EQ( updatedSignals[signal4.signalID].interfaceId, "30" ); + ASSERT_EQ( updatedSignals[signal4.signalID].messageId, "custom-decoder-0" ); + rawDataBufferManager->updateConfig( updatedSignals ); - // The Config should be updated and 3 Raw Data Buffer should be Allocated - ASSERT_EQ( rawDataBufferManager->getActiveBuffers(), 3 ); + // The Config should be updated and 4 Raw Data Buffer should be Allocated + ASSERT_EQ( rawDataBufferManager->getActiveBuffers(), 4 ); } #endif +TEST( InspectionMatrixExtractorTest, InspectionMatrixRawBufferConfigUpdaterWithCustomDecodingDictionary ) +{ + struct SignalCollectionInfo signal1; + // Range 0x2000-0x3000 is used for custom decoding in the unit test mock + signal1.signalID = 0x2001; + signal1.sampleBufferSize = 2; + signal1.minimumSampleIntervalMs = 3; + signal1.fixedWindowPeriod = 4; + struct SignalCollectionInfo signal2; + signal2.signalID = 0x2002; + signal2.sampleBufferSize = 2; + signal2.minimumSampleIntervalMs = 3; + signal2.fixedWindowPeriod = 4; + std::vector testSignals = { signal1, signal2 }; + std::vector testCANFrames = {}; + ICollectionSchemePtr collectionScheme = + std::make_shared( "COLLECTIONSCHEME1", "DMBM1", 0, 10, testSignals, testCANFrames ); + std::vector list1; + list1.emplace_back( collectionScheme ); + + // Create a Raw Data Buffer Manager + auto rawDataBufferManager = + std::make_shared>( RawData::BufferManagerConfig::create().get() ); + + CANInterfaceIDTranslator canIDTranslator; + CollectionSchemeManagerWrapper test( nullptr, + canIDTranslator, + std::make_shared( nullptr ), + "DMBM1", + rawDataBufferManager +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , + []() { + return std::unordered_map>{ + { "30", { "custom-decoder-0", "custom-decoder-1" } } }; + } +#endif + ); + + std::unordered_map> formatMap; + std::unordered_map> signalToFrameAndNodeID; + std::unordered_map signalIDToPIDDecoderFormat; + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat = { + { 0x2001, CustomSignalDecoderFormat{ "30", "custom-decoder-0", 0x2001, SignalType::STRING } }, + { 0x2002, CustomSignalDecoderFormat{ "30", "custom-decoder-1", 0x2002, SignalType::DOUBLE } } }; + + IDecoderManifestPtr DMBM1 = std::make_shared( + "DMBM1", formatMap, signalToFrameAndNodeID, signalIDToPIDDecoderFormat, signalIDToCustomDecoderFormat ); + ICollectionSchemeListPtr PL1 = std::make_shared( list1 ); + test.setDecoderManifest( DMBM1 ); + test.setCollectionSchemeList( PL1 ); + + // Config not set so no Buffer should be allocated + ASSERT_EQ( rawDataBufferManager->getActiveBuffers(), 0 ); + + ASSERT_TRUE( test.updateMapsandTimeLine( { 0, 0 } ) ); + std::shared_ptr output = std::make_shared(); + + std::unordered_map updatedSignals; + + EXPECT_CALL( *rawDataBufferManager, mockedUpdateConfig( _ ) ).WillOnce( [&updatedSignals]( auto arg ) { + updatedSignals = arg; + return RawData::BufferErrorCode::SUCCESSFUL; + } ); + test.updateRawDataBufferConfigStringSignals( updatedSignals ); + + // Only string signal should be used for update + ASSERT_EQ( updatedSignals.size(), 1 ); + + ASSERT_NE( updatedSignals.find( signal1.signalID ), updatedSignals.end() ); + ASSERT_EQ( updatedSignals[signal1.signalID].typeId, signal1.signalID ); + ASSERT_EQ( updatedSignals[signal1.signalID].interfaceId, "30" ); + ASSERT_EQ( updatedSignals[signal1.signalID].messageId, "custom-decoder-0" ); + + rawDataBufferManager->updateConfig( updatedSignals ); + + // The Config should be updated and 1 Raw Data Buffer should be Allocated + ASSERT_EQ( rawDataBufferManager->getActiveBuffers(), 1 ); +} + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/IoTJobsDataRequestHandlerTest.cpp b/test/unit/IoTJobsDataRequestHandlerTest.cpp new file mode 100644 index 00000000..cd1f8c84 --- /dev/null +++ b/test/unit/IoTJobsDataRequestHandlerTest.cpp @@ -0,0 +1,1059 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "IoTJobsDataRequestHandler.h" +#include "AwsBootstrap.h" +#include "AwsIotConnectivityModule.h" +#include "CANInterfaceIDTranslator.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "DataSenderProtoWriter.h" +#include "IReceiver.h" +#include "MqttClientWrapper.h" +#include "MqttClientWrapperMock.h" +#include "SenderMock.h" +#include "StreamForwarder.h" +#include "StreamForwarderMock.h" +#include "StreamManagerMock.h" +#include "TelemetryDataSender.h" +#include "TopicConfig.h" +#include "WaitUntil.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::A; +using ::testing::AnyNumber; +using ::testing::AtLeast; +using ::testing::DoAll; +using ::testing::Invoke; +using ::testing::MockFunction; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::ReturnRef; +using ::testing::SaveArg; +using ::testing::Sequence; +using ::testing::StrictMock; + +class IoTJobsDataRequestHandlerTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + // Need to initialize the SDK to get proper error strings + AwsBootstrap::getInstance().getClientBootStrap(); + + mMqttClientWrapperMock = std::make_shared>(); + // We need to pass the client shared_ptr to AwsIotSender and AwsIotReceiver as a reference, so we can't pass the + // pointer to the subclass (i.e. MqttClientWrapperMock). + mMqttClientWrapper = mMqttClientWrapperMock; + EXPECT_CALL( *mMqttClientWrapperMock, MockedOperatorBool() ) + .Times( AnyNumber() ) + .WillRepeatedly( Return( true ) ); + ON_CALL( *mMqttClientWrapperMock, Start() ).WillByDefault( Invoke( [this]() noexcept -> bool { + Aws::Crt::Mqtt5::OnConnectionSuccessEventData eventData; + mMqttClientBuilderWrapperMock->mOnConnectionSuccessCallback( eventData ); + return true; + } ) ); + ON_CALL( *mMqttClientWrapperMock, Stop( _ ) ) + .WillByDefault( Invoke( + [this]( std::shared_ptr disconnectOptions ) noexcept -> bool { + static_cast( disconnectOptions ); + Aws::Crt::Mqtt5::OnStoppedEventData eventData; + mMqttClientBuilderWrapperMock->mOnStoppedCallback( eventData ); + return true; + } ) ); + + mMqttClientBuilderWrapperMock = std::make_shared>(); + ON_CALL( *mMqttClientBuilderWrapperMock, Build() ).WillByDefault( Return( mMqttClientWrapperMock ) ); + ON_CALL( *mMqttClientBuilderWrapperMock, WithClientExtendedValidationAndFlowControl( _ ) ) + .WillByDefault( ReturnRef( *mMqttClientBuilderWrapperMock ) ); + ON_CALL( *mMqttClientBuilderWrapperMock, WithConnectOptions( _ ) ) + .WillByDefault( DoAll( SaveArg<0>( &mConnectPacket ), ReturnRef( *mMqttClientBuilderWrapperMock ) ) ); + ON_CALL( *mMqttClientBuilderWrapperMock, WithOfflineQueueBehavior( _ ) ) + .WillByDefault( ReturnRef( *mMqttClientBuilderWrapperMock ) ); + ON_CALL( *mMqttClientBuilderWrapperMock, WithSessionBehavior( _ ) ) + .WillByDefault( ReturnRef( *mMqttClientBuilderWrapperMock ) ); + ON_CALL( *mMqttClientBuilderWrapperMock, WithPingTimeoutMs( _ ) ) + .WillByDefault( ReturnRef( *mMqttClientBuilderWrapperMock ) ); + ON_CALL( *mMqttClientBuilderWrapperMock, WithCertificateAuthority( _ ) ) + .WillByDefault( ReturnRef( *mMqttClientBuilderWrapperMock ) ); + + TopicConfigArgs topicConfigArgs; + mTopicConfig = std::make_unique( "clientIdTest", topicConfigArgs ); + mConnectivityModule = std::make_shared( + "", "clientIdTest", mMqttClientBuilderWrapperMock, *mTopicConfig ); + + auto protoWriter = std::make_shared( mCANIDTranslator, nullptr ); + mStreamManager = std::make_shared>( protoWriter ); + + mMqttSender = std::make_shared>( *mTopicConfig ); + EXPECT_CALL( *mMqttSender, getMaxSendSize() ) + .Times( AnyNumber() ) + .WillRepeatedly( Return( MAXIMUM_PAYLOAD_SIZE ) ); + mTelemetryDataSender = std::make_shared( + mMqttSender, protoWriter, mPayloadAdaptionConfigUncompressed, mPayloadAdaptionConfigCompressed ); + mStreamForwarder = + std::make_shared>( mStreamManager, mTelemetryDataSender ); + } + + std::shared_ptr> mMqttClientWrapperMock; + std::shared_ptr mMqttClientWrapper; + std::shared_ptr> mMqttClientBuilderWrapperMock; + std::unique_ptr mTopicConfig; + std::shared_ptr mConnectivityModule; + std::shared_ptr mConnectPacket; + + CANInterfaceIDTranslator mCANIDTranslator; + std::shared_ptr> mStreamManager; + std::shared_ptr mTelemetryDataSender; + std::shared_ptr> mStreamForwarder; + std::shared_ptr> mMqttSender; + std::shared_ptr mReceiverIotJob; + std::shared_ptr mReceiverJobDocumentAccepted; + std::shared_ptr mReceiverJobDocumentRejected; + std::shared_ptr mReceiverPendingJobsAccepted; + std::shared_ptr mReceiverPendingJobsRejected; + std::shared_ptr mReceiverUpdateIotJobStatusAccepted; + std::shared_ptr mReceiverUpdateIotJobStatusRejected; + std::shared_ptr mReceiverCanceledIoTJobs; + std::string thingName = "clientIdTest"; + static constexpr unsigned MAXIMUM_PAYLOAD_SIZE = 400; + PayloadAdaptionConfig mPayloadAdaptionConfigUncompressed{ 80, 70, 90, 10 }; + PayloadAdaptionConfig mPayloadAdaptionConfigCompressed{ 80, 70, 90, 10 }; +}; + +TEST_F( IoTJobsDataRequestHandlerTest, IoTJobsDataRequestHandler ) +{ + EXPECT_CALL( *mStreamForwarder, registerJobCompletionCallback( _ ) ) + .Times( 1 ) + .WillRepeatedly( Invoke( [this]( Store::StreamForwarder::JobCompletionCallback jobCompletionCallback ) -> void { + jobCompletionCallback( "test" ); + } ) ); + + mReceiverIotJob = mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/notify" ); + mReceiverJobDocumentAccepted = + mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/+/get/accepted" ); + mReceiverJobDocumentRejected = + mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/+/get/rejected" ); + mReceiverPendingJobsAccepted = + mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/get/accepted" ); + mReceiverPendingJobsRejected = + mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/get/rejected" ); + mReceiverUpdateIotJobStatusAccepted = + mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/+/update/accepted" ); + mReceiverUpdateIotJobStatusRejected = + mConnectivityModule->createReceiver( "$aws/things/" + thingName + "/jobs/+/update/rejected" ); + mReceiverCanceledIoTJobs = mConnectivityModule->createReceiver( "$aws/events/job/+/cancellation_in_progress" ); + IoTJobsDataRequestHandler mIoTJobsDataRequestHandler( mMqttSender, + mReceiverIotJob, + mReceiverJobDocumentAccepted, + mReceiverJobDocumentRejected, + mReceiverPendingJobsAccepted, + mReceiverPendingJobsRejected, + mReceiverUpdateIotJobStatusAccepted, + mReceiverUpdateIotJobStatusRejected, + mReceiverCanceledIoTJobs, + mStreamManager, + mStreamForwarder, + thingName ); + + EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithClientExtendedValidationAndFlowControl( _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithConnectOptions( _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithOfflineQueueBehavior( _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttClientBuilderWrapperMock, WithPingTimeoutMs( _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttClientBuilderWrapperMock, Build() ).Times( 1 ); + EXPECT_CALL( *mMqttClientWrapperMock, Start() ).Times( 1 ); + EXPECT_CALL( *mMqttClientWrapperMock, Subscribe( _, _ ) ) + .Times( 8 ) + .WillRepeatedly( + Invoke( [this]( std::shared_ptr, + Aws::Crt::Mqtt5::OnSubscribeCompletionHandler onSubscribeCompletionCallback ) -> bool { + onSubscribeCompletionCallback( AWS_ERROR_SUCCESS, nullptr ); + return true; + } ) ); + + ASSERT_TRUE( mConnectivityModule->connect() ); + + WAIT_ASSERT_TRUE( mConnectivityModule->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverIotJob.get() )->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverJobDocumentAccepted.get() )->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverJobDocumentRejected.get() )->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverPendingJobsAccepted.get() )->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverPendingJobsRejected.get() )->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverUpdateIotJobStatusRejected.get() )->isAlive() ); + WAIT_ASSERT_TRUE( static_cast( mReceiverCanceledIoTJobs.get() )->isAlive() ); + + EXPECT_CALL( *mStreamManager, hasCampaign( _ ) ).Times( AnyNumber() ).WillRepeatedly( Return( true ) ); + + auto publishTime = ClockHandler::getClock()->monotonicTimeSinceEpochMs(); + + // Test GetPendingJobExecutions Accepted + + Json::StreamWriterBuilder builder; + Json::Value getPendingJobsAccepted1; + getPendingJobsAccepted1["jobId"] = "1"; + getPendingJobsAccepted1["queuedAt"] = publishTime; + getPendingJobsAccepted1["lastUpdatedAt"] = publishTime; + getPendingJobsAccepted1["versionNumber"] = 1; + + Json::Value getPendingJobsAccepted2; + getPendingJobsAccepted2["jobId"] = "2"; + getPendingJobsAccepted2["queuedAt"] = publishTime; + getPendingJobsAccepted2["lastUpdatedAt"] = publishTime; + getPendingJobsAccepted2["versionNumber"] = 1; + + Json::Value noJobIdPendingJobsAccepted; + noJobIdPendingJobsAccepted["queuedAt"] = publishTime; + noJobIdPendingJobsAccepted["lastUpdatedAt"] = publishTime; + noJobIdPendingJobsAccepted["versionNumber"] = 1; + + Json::Value invalidJobIdPendingJobsAccepted; + invalidJobIdPendingJobsAccepted["jobId"] = 1; + invalidJobIdPendingJobsAccepted["queuedAt"] = publishTime; + invalidJobIdPendingJobsAccepted["lastUpdatedAt"] = publishTime; + invalidJobIdPendingJobsAccepted["versionNumber"] = 1; + + Json::Value nullJobIdPendingJobsAccepted; + nullJobIdPendingJobsAccepted["jobId"] = Json::Value::null; + nullJobIdPendingJobsAccepted["queuedAt"] = publishTime; + nullJobIdPendingJobsAccepted["lastUpdatedAt"] = publishTime; + nullJobIdPendingJobsAccepted["versionNumber"] = 1; + + Json::Value pendingJobsMocked; + pendingJobsMocked["timestamp"] = publishTime; + pendingJobsMocked["queuedJobs"] = Json::Value( Json::arrayValue ); + pendingJobsMocked["inProgressJobs"] = Json::Value( Json::arrayValue ); + pendingJobsMocked["inProgressJobs"].append( getPendingJobsAccepted1 ); + pendingJobsMocked["inProgressJobs"].append( noJobIdPendingJobsAccepted ); + pendingJobsMocked["inProgressJobs"].append( invalidJobIdPendingJobsAccepted ); + pendingJobsMocked["inProgressJobs"].append( nullJobIdPendingJobsAccepted ); + pendingJobsMocked["queuedJobs"].append( getPendingJobsAccepted2 ); + pendingJobsMocked["queuedJobs"].append( noJobIdPendingJobsAccepted ); + pendingJobsMocked["queuedJobs"].append( invalidJobIdPendingJobsAccepted ); + pendingJobsMocked["queuedJobs"].append( nullJobIdPendingJobsAccepted ); + + const std::string pendingJobData = Json::writeString( builder, pendingJobsMocked ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/get/accepted", + Aws::Crt::ByteCursorFromCString( pendingJobData.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/1/get", _, _, _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/2/get", _, _, _ ) ).Times( 1 ); + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Two valid pending jobs so there should be two job document requests + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/get" ).size(), 1 ); + auto sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/get" ); + + Json::Value expectedJobDocRequest1; + expectedJobDocRequest1["jobId"] = "1"; + expectedJobDocRequest1["thingName"] = thingName; + expectedJobDocRequest1["includeJobDocument"] = true; + + Json::Reader reader; + Json::Value actualJobDocRequest1; + + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobDocRequest1 ) ); + ASSERT_EQ( actualJobDocRequest1, expectedJobDocRequest1 ); + + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/2/get" ).size(), 1 ); + sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/2/get" ); + + Json::Value expectedJobDocRequest2; + expectedJobDocRequest2["jobId"] = "2"; + expectedJobDocRequest2["thingName"] = thingName; + expectedJobDocRequest2["includeJobDocument"] = true; + + Json::Value actualJobDocRequest2; + + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobDocRequest2 ) ); + ASSERT_EQ( actualJobDocRequest2, expectedJobDocRequest2 ); + + // Clear sent buffer data mock so that we can test updating the job status + mMqttSender->clearSentBufferData(); + + Json::Value pendingJobsMockedBad1; + pendingJobsMockedBad1["timestamp"] = publishTime; + pendingJobsMockedBad1["queuedJobs"] = ""; + + Json::Value pendingJobsMockedBad2; + pendingJobsMockedBad2["timestamp"] = publishTime; + pendingJobsMockedBad2["inProgressJobs"] = ""; + + const std::string pendingJobBad1 = Json::writeString( builder, pendingJobsMockedBad1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/get/accepted", + Aws::Crt::ByteCursorFromCString( pendingJobBad1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string pendingJobBad2 = Json::writeString( builder, pendingJobsMockedBad2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/get/accepted", + Aws::Crt::ByteCursorFromCString( pendingJobBad2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Two invalid pending jobs so there should be zero job document requests + ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + + // Test GetPendingJobExecutions Rejected + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/get/rejected", + Aws::Crt::ByteCursorFromCString( pendingJobData.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/get", _, _, _ ) ).Times( 1 ); + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // One pending job so there should be one job document request after retrying GetPendingJobExecutions + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/get" ).size(), 1 ); + + // Clear sent buffer data mock so that we can test updating the job status + mMqttSender->clearSentBufferData(); + + // Test JobExecutionChanged (notify topic) + + Json::Value job1; + job1["jobId"] = "1"; + job1["queuedAt"] = publishTime; + job1["lastUpdatedAt"] = publishTime; + job1["versionNumber"] = 1; + + Json::Value job2; + job2["jobId"] = "2"; + job2["queuedAt"] = publishTime; + job2["lastUpdatedAt"] = publishTime; + job2["versionNumber"] = 1; + + Json::Value mockJob; + mockJob["timestamp"] = publishTime; + mockJob["jobs"]["QUEUED"] = Json::Value( Json::arrayValue ); + mockJob["jobs"]["QUEUED"].append( job1 ); + mockJob["jobs"]["QUEUED"].append( job2 ); + + const std::string data = Json::writeString( builder, mockJob ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( data.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/1/get", _, _, _ ) ).Times( 1 ); + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/2/get", _, _, _ ) ).Times( 1 ); + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Two valid jobs so there should be two job document requests + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/get" ).size(), 1 ); + sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/get" ); + + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobDocRequest1 ) ); + ASSERT_EQ( actualJobDocRequest1, expectedJobDocRequest1 ); + + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/2/get" ).size(), 1 ); + sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/2/get" ); + + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobDocRequest2 ) ); + ASSERT_EQ( actualJobDocRequest2, expectedJobDocRequest2 ); + + // Clear sent buffer data mock so that we can test updating the job status + mMqttSender->clearSentBufferData(); + + Json::Value badJob1; + badJob1["timestamp"] = publishTime; + + Json::Value badJob2; + badJob2["timestamp"] = publishTime; + badJob2["jobs"]["QUEUED"] = Json::Value( Json::arrayValue ); + + Json::Value badJob3; + badJob3["timestamp"] = publishTime; + badJob3["jobs"]["QUEUED"] = "QUEUED should be a list"; + + const std::string bad1 = Json::writeString( builder, badJob1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( bad1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string bad2 = Json::writeString( builder, badJob2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( bad2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string bad3 = Json::writeString( builder, badJob3 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( bad3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Received 3 malformed jobs so there should be 0 JobDocumentRequests + ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + + Json::Value invalidJson; + // No jobId key + invalidJson["badJobIdKey"] = "3"; + invalidJson["queuedAt"] = publishTime; + invalidJson["lastUpdatedAt"] = publishTime; + invalidJson["versionNumber"] = 1; + + Json::Value unexpectedFormat; + // jobId should be a string, not an int + unexpectedFormat["jobId"] = 4; + unexpectedFormat["queuedAt"] = publishTime; + unexpectedFormat["lastUpdatedAt"] = publishTime; + unexpectedFormat["versionNumber"] = 1; + + Json::Value nullJobId; + // jobId is null + nullJobId["jobId"] = Json::Value::null; + nullJobId["queuedAt"] = publishTime; + nullJobId["lastUpdatedAt"] = publishTime; + nullJobId["versionNumber"] = 1; + + Json::Value emptyJobId; + // jobId is "" + emptyJobId["jobId"] = ""; + emptyJobId["queuedAt"] = publishTime; + emptyJobId["lastUpdatedAt"] = publishTime; + emptyJobId["versionNumber"] = 1; + + Json::Value invalidJob; + invalidJob["timestamp"] = publishTime; + invalidJob["jobs"]["QUEUED"] = Json::Value( Json::arrayValue ); + invalidJob["jobs"]["QUEUED"].append( invalidJson ); + invalidJob["jobs"]["QUEUED"].append( unexpectedFormat ); + invalidJob["jobs"]["QUEUED"].append( nullJobId ); + invalidJob["jobs"]["QUEUED"].append( emptyJobId ); + + const std::string invalidJsonData = Json::writeString( builder, invalidJob ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( invalidJsonData.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Four invalid jobs so there should be zero document requests + ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + + Json::Value emptyQueue; + emptyQueue["timestamp"] = publishTime; + emptyQueue["jobs"]["QUEUED"] = Json::Value( Json::arrayValue ); + + Json::Value queueNotAList; + queueNotAList["timestamp"] = publishTime; + queueNotAList["jobs"]["QUEUED"] = "QUEUED should be a list"; + + const std::string emptyQueueJsonData = Json::writeString( builder, emptyQueue ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( emptyQueueJsonData.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Invalid job so there should be zero document requests + ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + + const std::string invalidQueueJsonData = Json::writeString( builder, queueNotAList ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/notify", + Aws::Crt::ByteCursorFromCString( invalidQueueJsonData.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Invalid job so there should be zero document requests + ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + + // Test DescribeJobExecution Accepted and Rejected + + Json::Value invalidJobDoc1; + // no execution field + invalidJobDoc1["clientToken"] = thingName; + invalidJobDoc1["timestamp"] = publishTime; + + Json::Value invalidJobDoc2; + // null execution field + invalidJobDoc2["clientToken"] = thingName; + invalidJobDoc2["timestamp"] = publishTime; + invalidJobDoc2["execution"] = Json::Value::null; + + Json::Value invalidJobDoc3; + // no jobId field + invalidJobDoc3["clientToken"] = thingName; + invalidJobDoc3["timestamp"] = publishTime; + invalidJobDoc3["execution"]["status"] = "QUEUED"; + + Json::Value invalidJobDoc4; + // invalid jobId field + invalidJobDoc4["clientToken"] = thingName; + invalidJobDoc4["timestamp"] = publishTime; + invalidJobDoc4["execution"]["status"] = "QUEUED"; + invalidJobDoc4["execution"]["jobId"] = 1; + + Json::Value invalidJobDoc5; + // invalid jobId field + invalidJobDoc5["clientToken"] = thingName; + invalidJobDoc5["timestamp"] = publishTime; + invalidJobDoc5["execution"]["status"] = "QUEUED"; + invalidJobDoc5["execution"]["jobId"] = ""; + + const std::string badDataReq1 = Json::writeString( builder, invalidJobDoc1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( badDataReq1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/rejected", + Aws::Crt::ByteCursorFromCString( badDataReq1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string badDataReq2 = Json::writeString( builder, invalidJobDoc2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( badDataReq2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/rejected", + Aws::Crt::ByteCursorFromCString( badDataReq2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string badDataReq3 = Json::writeString( builder, invalidJobDoc3 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( badDataReq3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/rejected", + Aws::Crt::ByteCursorFromCString( badDataReq3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string badDataReq4 = Json::writeString( builder, invalidJobDoc4 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( badDataReq4.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/rejected", + Aws::Crt::ByteCursorFromCString( badDataReq4.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string badDataReq5 = Json::writeString( builder, invalidJobDoc5 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( badDataReq5.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/rejected", + Aws::Crt::ByteCursorFromCString( badDataReq5.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // There should be no update job status requests since all the job docs were invalid + ASSERT_EQ( mMqttSender->getSentBufferData().size(), 0 ); + + Json::Value docRequest1; + docRequest1["clientToken"] = thingName; + docRequest1["timestamp"] = publishTime; + docRequest1["execution"]["approximateSecondsBeforeTimedOut"] = 10; + docRequest1["execution"]["jobId"] = "1"; + docRequest1["execution"]["status"] = "QUEUED"; + docRequest1["execution"]["queuedAt"] = publishTime; + docRequest1["execution"]["lastUpdatedAt"] = publishTime; + docRequest1["execution"]["versionNumber"] = 1; + docRequest1["execution"]["jobDocument"]["versionNumber"] = 1; + docRequest1["execution"]["jobDocument"]["parameters"]["campaignArn"] = "garbage_data"; + docRequest1["execution"]["jobDocument"]["parameters"]["endTime"] = publishTime; + + Json::Value docRequest2; + docRequest2["clientToken"] = thingName; + docRequest2["timestamp"] = publishTime; + docRequest2["execution"]["approximateSecondsBeforeTimedOut"] = 10; + docRequest2["execution"]["jobId"] = "2"; + docRequest2["execution"]["status"] = "IN_PROGRESS"; + docRequest2["execution"]["queuedAt"] = publishTime; + docRequest2["execution"]["lastUpdatedAt"] = publishTime; + docRequest2["execution"]["versionNumber"] = 1; + docRequest2["execution"]["jobDocument"]["versionNumber"] = 1; + docRequest2["execution"]["jobDocument"]["parameters"]["campaignArn"] = "garbage_data"; + docRequest2["execution"]["jobDocument"]["parameters"]["endTime"] = publishTime; + + Json::Value docRequest3; + // no campaignArn + docRequest3["clientToken"] = thingName; + docRequest3["timestamp"] = publishTime; + docRequest3["execution"]["approximateSecondsBeforeTimedOut"] = 10; + docRequest3["execution"]["jobId"] = "3"; + docRequest3["execution"]["status"] = "QUEUED"; + docRequest3["execution"]["queuedAt"] = publishTime; + docRequest3["execution"]["lastUpdatedAt"] = publishTime; + docRequest3["execution"]["versionNumber"] = 1; + docRequest3["execution"]["jobDocument"]["versionNumber"] = 1; + docRequest3["execution"]["jobDocument"]["parameters"]["endTime"] = publishTime; + + Json::Value docRequest4; + // no parameters + docRequest4["clientToken"] = thingName; + docRequest4["timestamp"] = publishTime; + docRequest4["execution"]["approximateSecondsBeforeTimedOut"] = 10; + docRequest4["execution"]["jobId"] = "3"; + docRequest4["execution"]["status"] = "QUEUED"; + docRequest4["execution"]["queuedAt"] = publishTime; + docRequest4["execution"]["lastUpdatedAt"] = publishTime; + docRequest4["execution"]["versionNumber"] = 1; + docRequest4["execution"]["jobDocument"]["versionNumber"] = 1; + + Json::Value docRequest5; + // no parameters + docRequest5["clientToken"] = thingName; + docRequest5["timestamp"] = publishTime; + docRequest5["execution"]["approximateSecondsBeforeTimedOut"] = 10; + docRequest5["execution"]["jobId"] = "3"; + docRequest5["execution"]["status"] = "IN_PROGRESS"; + docRequest5["execution"]["queuedAt"] = publishTime; + docRequest5["execution"]["lastUpdatedAt"] = publishTime; + docRequest5["execution"]["versionNumber"] = 1; + docRequest5["execution"]["jobDocument"]["versionNumber"] = 1; + docRequest5["execution"]["jobDocument"]["parameters"]["campaignArn"] = "garbage_data"; + docRequest5["execution"]["jobDocument"]["parameters"]["endTime"] = publishTime; + + const std::string dataReq1 = Json::writeString( builder, docRequest1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( dataReq1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/1/update", _, _, _ ) ).Times( 1 ); + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string dataReq2 = Json::writeString( builder, docRequest2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/2/get/accepted", + Aws::Crt::ByteCursorFromCString( dataReq2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Two jobs, but only 1 of them is QUEUED so there should be 1 update event + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/update" ).size(), 1 ); + sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/update" ); + + Json::Value expectedJobUpdateStatus1; + expectedJobUpdateStatus1["status"] = "IN_PROGRESS"; + expectedJobUpdateStatus1["clientToken"] = "1"; + + Json::Value actualJobUpdateStatus1; + + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobUpdateStatus1 ) ); + ASSERT_EQ( actualJobUpdateStatus1, expectedJobUpdateStatus1 ); + + mMqttSender->clearSentBufferData(); + + const std::string dataReq3 = Json::writeString( builder, docRequest3 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/3/get/accepted", + Aws::Crt::ByteCursorFromCString( dataReq3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string dataReq4 = Json::writeString( builder, docRequest4 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/3/get/accepted", + Aws::Crt::ByteCursorFromCString( dataReq4.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + EXPECT_CALL( *mStreamManager, hasCampaign( _ ) ).Times( AnyNumber() ).WillRepeatedly( Return( false ) ); + + const std::string dataReq5 = Json::writeString( builder, docRequest5 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/3/get/accepted", + Aws::Crt::ByteCursorFromCString( dataReq5.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/3/update", _, _, _ ) ).Times( 1 ); + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/accepted", + Aws::Crt::ByteCursorFromCString( dataReq1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + EXPECT_CALL( *mMqttSender, mockedSendBuffer( "$aws/things/clientIdTest/jobs/1/update", _, _, _ ) ).Times( 1 ); + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Two valid job docs so there should be two update events + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/update" ).size(), 1 ); + sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/update" ); + + Json::Value expectedJobUpdateStatus6; + expectedJobUpdateStatus6["status"] = "REJECTED"; + expectedJobUpdateStatus6["clientToken"] = "1"; + + Json::Value actualJobUpdateStatus6; + + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobUpdateStatus6 ) ); + ASSERT_EQ( actualJobUpdateStatus6, expectedJobUpdateStatus6 ); + + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/3/update" ).size(), 1 ); + sentBufferData = mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/3/update" ); + + Json::Value expectedJobUpdateStatus5; + expectedJobUpdateStatus5["status"] = "REJECTED"; + expectedJobUpdateStatus5["clientToken"] = "3"; + + Json::Value actualJobUpdateStatus5; + ASSERT_TRUE( reader.parse( sentBufferData[0].data, actualJobUpdateStatus5 ) ); + ASSERT_EQ( actualJobUpdateStatus5, expectedJobUpdateStatus5 ); + + mMqttSender->clearSentBufferData(); + + // Test DescribeJobExecution rejected + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/get/rejected", + Aws::Crt::ByteCursorFromCString( dataReq1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Test UpdateJobExecution retry + const std::string updateRejected = Json::writeString( builder, expectedJobUpdateStatus1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = + std::make_shared( "$aws/things/clientIdTest/jobs/1/update/rejected", + Aws::Crt::ByteCursorFromCString( updateRejected.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + Json::Value badUpdateStatus1; + badUpdateStatus1["status"] = "IN_PROGRESS"; + + const std::string updateRejected1 = Json::writeString( builder, badUpdateStatus1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/1/update/rejected", + Aws::Crt::ByteCursorFromCString( updateRejected1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + Json::Value badUpdateStatus2; + badUpdateStatus2["status"] = "IN_PROGRESS"; + badUpdateStatus2["clientToken"] = 1; + + const std::string updateRejected2 = Json::writeString( builder, badUpdateStatus2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/1/update/rejected", + Aws::Crt::ByteCursorFromCString( updateRejected2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + Json::Value badUpdateStatus3; + badUpdateStatus3["status"] = "IN_PROGRESS"; + badUpdateStatus3["clientToken"] = ""; + + const std::string updateRejected3 = Json::writeString( builder, badUpdateStatus3 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/1/update/rejected", + Aws::Crt::ByteCursorFromCString( updateRejected3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // We are not retrying rejected update requests so the update status sender should have sent 0 messages + ASSERT_EQ( mMqttSender->getSentBufferDataByTopic( "$aws/things/clientIdTest/jobs/1/update" ).size(), 0 ); + + // Test UpdateJobExecution accepted + const std::string updateAccepted1 = Json::writeString( builder, badUpdateStatus1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/1/update/accepted", + Aws::Crt::ByteCursorFromCString( updateAccepted1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string updateAccepted2 = Json::writeString( builder, badUpdateStatus2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/1/update/accepted", + Aws::Crt::ByteCursorFromCString( updateAccepted2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string updateAccepted3 = Json::writeString( builder, badUpdateStatus3 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/things/clientIdTest/jobs/1/update/accepted", + Aws::Crt::ByteCursorFromCString( updateAccepted3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + // Test onCanceledJobReceived + // TODO: Finish canceledJob testing once the cancel job handler is completed + Json::Value canceledJob1; + canceledJob1["jobId"] = "1"; + + Json::Value canceledJob2; + canceledJob2["jobId"] = 2; + + Json::Value canceledJob3; + canceledJob3["jobId"] = ""; + + const std::string canceledJobData1 = Json::writeString( builder, canceledJob1 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/events/job/1/cancellation_in_progress", + Aws::Crt::ByteCursorFromCString( canceledJobData1.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string canceledJobData2 = Json::writeString( builder, canceledJob2 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/events/job/1/cancellation_in_progress", + Aws::Crt::ByteCursorFromCString( canceledJobData2.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + const std::string canceledJobData3 = Json::writeString( builder, canceledJob3 ); + { + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + + auto publishPacket = std::make_shared( + "$aws/events/job/1/cancellation_in_progress", + Aws::Crt::ByteCursorFromCString( canceledJobData3.c_str() ), + Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ); + eventData.publishPacket = publishPacket; + mMqttClientBuilderWrapperMock->mOnPublishReceivedHandlerCallback( eventData ); + } + + uint64_t endTime = IoTJobsDataRequestHandler::convertEndTimeToMS( "2024-03-05T23:00:00Z" ); + uint64_t expectedEndTime = 1709679600000; + + uint64_t malformedEndTime = IoTJobsDataRequestHandler::convertEndTimeToMS( "2024-03-0523:00:00Z" ); + uint64_t expectedMalformedEndTime = 0; + + ASSERT_EQ( endTime, expectedEndTime ); + ASSERT_EQ( malformedEndTime, expectedMalformedEndTime ); + + // Should be called on destruction + EXPECT_CALL( *mMqttClientWrapperMock, Unsubscribe( _, _ ) ) + .Times( 8 ) + .WillRepeatedly( Invoke( + [this]( std::shared_ptr, + Aws::Crt::Mqtt5::OnUnsubscribeCompletionHandler onUnsubscribeCompletionCallback ) noexcept -> bool { + onUnsubscribeCompletionCallback( AWS_ERROR_SUCCESS, nullptr ); + return true; + } ) ); + + EXPECT_CALL( *mMqttClientWrapperMock, Stop( _ ) ).Times( 1 ); + + ASSERT_TRUE( mConnectivityModule->disconnect() ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/LastKnownStateInspectorTest.cpp b/test/unit/LastKnownStateInspectorTest.cpp new file mode 100644 index 00000000..daf3c6a2 --- /dev/null +++ b/test/unit/LastKnownStateInspectorTest.cpp @@ -0,0 +1,1042 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateInspector.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "ICommandDispatcher.h" +#include "LastKnownStateTypes.h" +#include "QueueTypes.h" +#include "SignalTypes.h" +#include "Testing.h" +#include "TimeTypes.h" +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using signalTypes = + ::testing::Types; + +template +class LastKnownStateInspectorTest : public ::testing::Test +{ +protected: + LastKnownStateInspectorTest() + : mCommandResponses( std::make_shared( 100, "Command Responses" ) ) + { + } + + void + SetUp() override + { + mStateTemplatePeriodic1000ms = std::make_shared( + StateTemplateInformation{ "stateTemplate1", + "decoder", + { LastKnownStateSignalInformation{ SIG_D, SignalType::DOUBLE } }, + LastKnownStateUpdateStrategy::PERIODIC, + 1000 } ); + mStateTemplatePeriodic400ms = std::make_shared( + StateTemplateInformation{ "stateTemplate2", + "decoder", + { LastKnownStateSignalInformation{ SIG_E, SignalType::DOUBLE } }, + LastKnownStateUpdateStrategy::PERIODIC, + 400 } ); + mStateTemplateOnChange = std::make_shared( + StateTemplateInformation{ "stateTemplate3", + "decoder", + { LastKnownStateSignalInformation{ SIG_A, SignalType::DOUBLE }, + LastKnownStateSignalInformation{ SIG_B, SignalType::INT64 } }, + LastKnownStateUpdateStrategy::ON_CHANGE } ); + } + + void + TearDown() override + { + } + + bool + popCommandResponse( std::shared_ptr &commandResponse ) + { + std::shared_ptr senderData; + auto succeeded = mCommandResponses->pop( senderData ); + commandResponse = std::dynamic_pointer_cast( senderData ); + return succeeded; + } + + /** + * Collects the next data sorting all elements and all signals in a predictable way + */ + std::shared_ptr + collectNextDataToSendSorted( LastKnownStateInspector &inspector, const TimePoint ¤tTime ) + { + auto dataToSend = inspector.collectNextDataToSend( currentTime ); + if ( dataToSend == nullptr ) + { + return nullptr; + } + + // Make a copy of everything to get rid of the const qualifier + std::shared_ptr dataToSendSorted = + std::make_shared( *dataToSend ); + + std::sort( dataToSendSorted->stateTemplateCollectedSignals.begin(), + dataToSendSorted->stateTemplateCollectedSignals.end(), + []( StateTemplateCollectedSignals &a, StateTemplateCollectedSignals &b ) { + return a.stateTemplateSyncId < b.stateTemplateSyncId; + } ); + + for ( auto &collectedData : dataToSendSorted->stateTemplateCollectedSignals ) + { + std::sort( collectedData.signals.begin(), + collectedData.signals.end(), + []( CollectedSignal &a, CollectedSignal &b ) { + return a.signalID < b.signalID; + } ); + } + + return dataToSendSorted; + } + + const SignalID SIG_A = 1; + const SignalID SIG_B = 2; + const SignalID SIG_D = 4; + const SignalID SIG_E = 5; + const SignalID SIG_F = 6; + const SignalID SIG_G = 7; + std::shared_ptr mStateTemplatePeriodic1000ms; + std::shared_ptr mStateTemplatePeriodic400ms; + std::shared_ptr mStateTemplateOnChange; + std::shared_ptr mCommandResponses; + std::map mOutputSignalMap; +}; + +TYPED_TEST_SUITE( LastKnownStateInspectorTest, signalTypes ); + +TYPED_TEST( LastKnownStateInspectorTest, inspectTwoSameSignalValues ) +{ + const SignalID SIG_C = 3; + std::vector signalsToInspect; + LastKnownStateInspector inspector( this->mCommandResponses, nullptr ); + signalsToInspect.emplace_back( LastKnownStateSignalInformation{ SIG_C, getSignalType() } ); + auto stateTemplate = std::make_shared( StateTemplateInformation{ + "stateTemplate1", "decoder", signalsToInspect, LastKnownStateUpdateStrategy::ON_CHANGE } ); + + inspector.onStateTemplatesChanged( std::make_shared( StateTemplateList{ stateTemplate } ) ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", stateTemplate->id, LastKnownStateOperation::ACTIVATE } ); + + inspector.inspectNewSignal( SIG_C, TimePoint{ 100, 1000 }, 1.0 ); + auto dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint{ 110, 1100 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->triggerTime, 110 ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_C ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 100 ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), getSignalType() ); + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate1" ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 1, getSignalType() ) ); + inspector.inspectNewSignal( SIG_C, TimePoint(), 1.0 ); + ASSERT_EQ( this->collectNextDataToSendSorted( inspector, TimePoint() ), nullptr ); +} + +TYPED_TEST( LastKnownStateInspectorTest, inspectTwoDifferentSignalValues ) +{ + const SignalID SIG_C = 3; + std::vector signalsToInspect; + LastKnownStateInspector inspector( this->mCommandResponses, nullptr ); + signalsToInspect.emplace_back( LastKnownStateSignalInformation{ SIG_C, getSignalType() } ); + auto stateTemplate = std::make_shared( StateTemplateInformation{ + "stateTemplate1", "decoder", signalsToInspect, LastKnownStateUpdateStrategy::ON_CHANGE } ); + + inspector.onStateTemplatesChanged( std::make_shared( StateTemplateList{ stateTemplate } ) ); + + inspector.inspectNewSignal( SIG_C, TimePoint(), 2.0 ); + auto dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint() ); + // Before the state template is activate, nothing should be collected + ASSERT_EQ( dataToSend, nullptr ); + + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", stateTemplate->id, LastKnownStateOperation::ACTIVATE } ); + + inspector.inspectNewSignal( SIG_C, TimePoint(), 1.0 ); + dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_C ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), getSignalType() ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 1, getSignalType() ) ); + + inspector.inspectNewSignal( SIG_C, TimePoint(), 0.0 ); + dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_C ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), getSignalType() ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 0, getSignalType() ) ); +} + +class LastKnownStateInspectorDoubleTest : public LastKnownStateInspectorTest +{ +protected: + LastKnownStateInspectorDoubleTest() + : inspector( mCommandResponses, nullptr ) + { + } + + LastKnownStateInspector inspector; +}; + +TEST_F( LastKnownStateInspectorDoubleTest, inspectFirstTimeReceivedSignal ) +{ + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplateOnChange } ) ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", mStateTemplateOnChange->id, LastKnownStateOperation::ACTIVATE } ); + inspector.inspectNewSignal( SIG_A, TimePoint(), 1.0 ); + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 1.0 ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, inspectTwoSignalsWithSameValue ) +{ + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplateOnChange } ) ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", mStateTemplateOnChange->id, LastKnownStateOperation::ACTIVATE } ); + inspector.inspectNewSignal( SIG_A, TimePoint(), 1.0 ); + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 1.0 ); + + inspector.inspectNewSignal( SIG_A, TimePoint(), 1.0 ); + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint() ), nullptr ); + + // Although the value is different than previous value but it's still within the comparison threshold + inspector.inspectNewSignal( SIG_A, TimePoint(), 1.0009 ); + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint() ), nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, inspectSignalWithDifferentValue ) +{ + auto stateTemplate4 = std::make_shared( + StateTemplateInformation{ "stateTemplate4", + "decoder", + { LastKnownStateSignalInformation{ SIG_F, SignalType::DOUBLE } }, + LastKnownStateUpdateStrategy::ON_CHANGE } ); + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplateOnChange, stateTemplate4 } ) ); + + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", mStateTemplateOnChange->id, LastKnownStateOperation::ACTIVATE } ); + inspector.inspectNewSignal( SIG_A, TimePoint(), 1.0 ); + inspector.inspectNewSignal( SIG_F, TimePoint(), 10.0 ); + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 1.0 ); + + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command2", stateTemplate4->id, LastKnownStateOperation::ACTIVATE } ); + + inspector.inspectNewSignal( SIG_A, TimePoint(), 1.002 ); + inspector.inspectNewSignal( SIG_F, TimePoint(), 11.0 ); + dataToSend = collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 2 ); + + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 1.002 ); + + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 1 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 11.0 ); + + // Now deactivate only one template and ensure its signal is not collected + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command3", mStateTemplateOnChange->id, LastKnownStateOperation::DEACTIVATE } ); + + inspector.inspectNewSignal( SIG_A, TimePoint(), 2.0 ); + inspector.inspectNewSignal( SIG_F, TimePoint(), 12.0 ); + dataToSend = collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 12.0 ); + + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command4", stateTemplate4->id, LastKnownStateOperation::DEACTIVATE } ); + + inspector.inspectNewSignal( SIG_A, TimePoint(), 3.0 ); + inspector.inspectNewSignal( SIG_F, TimePoint(), 13.0 ); + dataToSend = collectNextDataToSendSorted( inspector, TimePoint() ); + ASSERT_EQ( dataToSend, nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, withNoLksInspectionMatrix ) +{ + inspector.inspectNewSignal( SIG_B, TimePoint(), 0xAA55AA55 ); + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint() ), nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, activateStateTemplateMultipleTimesWithAutoDeactivate ) +{ + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command1", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 5, // deactivateAfterSeconds + TimePoint{ 0, 0 }, // receivedTime + } ); + + std::shared_ptr commandResponse; + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_UNSPECIFIED ); + ASSERT_EQ( commandResponse->reasonDescription, "" ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 500, 500 }, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 1000, 1000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.inspectNewSignal( SIG_D, TimePoint{ 1100, 1100 }, 2.0 ); + // State template should have been deactivated already + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 5001, 5001 } ), nullptr ); + + // Re-activate the state template + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command2", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 5, // deactivateAfterSeconds + TimePoint{ 5000, 5000 }, // receivedTime + } ); + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command2" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_UNSPECIFIED ); + ASSERT_EQ( commandResponse->reasonDescription, "" ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 8000, 8000 }, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 10000, 10000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + // Send another Activate command for a state template that is already activated. This should + // reset the deactivateAfterSeconds config. + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command3", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 3, // deactivateAfterSeconds + TimePoint{ 9000, 9000 }, // state template activation should be extended until 12000 + } ); + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command3" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_STATE_TEMPLATE_ALREADY_ACTIVATED ); + ASSERT_EQ( commandResponse->reasonDescription, REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_ACTIVATED ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 9000, 9000 }, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 10001, 10001 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.inspectNewSignal( SIG_D, TimePoint{ 11000, 11000 }, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 12000, 12000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.inspectNewSignal( SIG_D, TimePoint{ 11500, 11500 }, 1.0 ); + // State template should have been deactivated already + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 12001, 12001 } ), nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, activateStateTemplateMultipleTimesWithoutAutoDeactivate ) +{ + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command1", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 5, // deactivateAfterSeconds + TimePoint{ 0, 0 }, // receivedTime + } ); + + std::shared_ptr commandResponse; + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_UNSPECIFIED ); + ASSERT_EQ( commandResponse->reasonDescription, "" ); + + // Send another command, but without deactivateAfterSeconds. + // This should clear the deactivateAfterSeconds set by previous command. + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command2", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 0, // deactivateAfterSeconds + TimePoint{ 4000, 4000 }, // receivedTime + } ); + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command2" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_STATE_TEMPLATE_ALREADY_ACTIVATED ); + ASSERT_EQ( commandResponse->reasonDescription, REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_ACTIVATED ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 4500, 4500 }, 1.0 ); + + { + // Auto-deactivate shouldn't have kicked in. + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 5001, 5001 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + // Now set deactivateAfterSeconds again, which should replace the missing deactivateAfterSeconds + // from previous command. + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command3", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 3, // deactivateAfterSeconds + TimePoint{ 6000, 6000 }, // state template activation should be extended until 9000 + } ); + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command3" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_STATE_TEMPLATE_ALREADY_ACTIVATED ); + ASSERT_EQ( commandResponse->reasonDescription, REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_ACTIVATED ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 7000, 7000 }, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 9000, 9000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.inspectNewSignal( SIG_D, TimePoint{ 8500, 8500 }, 2.0 ); + // State template should have been deactivated already + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 9001, 9001 } ), nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, keepActivationStatusOnStateTemplatesUpdate ) +{ + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command1", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 5, // deactivateAfterSeconds + TimePoint{ 0, 0 }, // receivedTime + } ); + + std::shared_ptr commandResponse; + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_UNSPECIFIED ); + ASSERT_EQ( commandResponse->reasonDescription, "" ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 500, 500 }, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 800, 800 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.onStateTemplatesChanged( std::make_shared( + StateTemplateList{ mStateTemplatePeriodic1000ms, mStateTemplatePeriodic400ms } ) ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 4500, 4500 }, 1.0 ); + + { + // The initial template should be still activated. The inspector shouldn't clear all + // existing state when the list of state templates is updated. + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 5000, 5000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.inspectNewSignal( SIG_D, TimePoint{ 4900, 4900 }, 2.0 ); + // State template should have been already deactivated due to deactivateAfterSeconds config + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 5001, 5001 } ), nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, deactivateAlreadyDeactivatedStateTemplate ) +{ + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command1", mStateTemplatePeriodic1000ms->id, LastKnownStateOperation::DEACTIVATE } ); + + std::shared_ptr commandResponse; + + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_STATE_TEMPLATE_ALREADY_DEACTIVATED ); + ASSERT_EQ( commandResponse->reasonDescription, REASON_DESCRIPTION_STATE_TEMPLATE_ALREADY_DEACTIVATED ); + + inspector.inspectNewSignal( SIG_D, TimePoint{ 1000, 1000 }, 1.0 ); + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 2000, 2000 } ), nullptr ); +} + +TEST_F( LastKnownStateInspectorDoubleTest, persistActivatedStateTemplate ) +{ + auto clock = ClockHandler::getClock(); + auto initialTime = clock->timeSinceEpoch(); + auto persistency = createCacheAndPersist(); + { + LastKnownStateInspector inspector( mCommandResponses, persistency ); + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command1", + mStateTemplatePeriodic1000ms->id, + LastKnownStateOperation::ACTIVATE, + 5, // deactivateAfterSeconds + initialTime, // receivedTime + } ); + + std::shared_ptr commandResponse; + ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + + inspector.inspectNewSignal( SIG_D, initialTime + 500, 1.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, initialTime + 1000 ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + } + + { + // Create a new inspector, which should load the persisted metadata from the previous instance + // and thus set the state template as activated. + LastKnownStateInspector inspector( mCommandResponses, persistency ); + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.inspectNewSignal( SIG_D, initialTime + 1100, 2.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, initialTime + 1200 ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + } + + inspector.inspectNewSignal( SIG_D, initialTime + 2000, 3.0 ); + // State template should have been deactivated already + ASSERT_EQ( collectNextDataToSendSorted( inspector, initialTime + 6000 ), nullptr ); + } + + { + // Create a new inspector, but since the last time the state template was deactivate, this + // new instance should not activate the state template. + LastKnownStateInspector inspector( mCommandResponses, persistency ); + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplatePeriodic1000ms } ) ); + + inspector.inspectNewSignal( SIG_D, initialTime + 6500, 4.0 ); + + ASSERT_EQ( collectNextDataToSendSorted( inspector, initialTime + 7000 ), nullptr ); + } +} + +/** + * Here's the test scenarios: + * signal D (period: 1s) | | + * signal E (period: 0.4s) | | | + * Inspection | | | | + */ +TEST_F( LastKnownStateInspectorDoubleTest, inspectSignalPeriodically ) +{ + auto stateTemplate4 = std::make_shared( + StateTemplateInformation{ "stateTemplate4", + "decoder", + { LastKnownStateSignalInformation{ SIG_F, SignalType::DOUBLE }, + LastKnownStateSignalInformation{ SIG_G, SignalType::DOUBLE } }, + LastKnownStateUpdateStrategy::PERIODIC, + 500 } ); + inspector.onStateTemplatesChanged( std::make_shared( + StateTemplateList{ mStateTemplatePeriodic1000ms, mStateTemplatePeriodic400ms, stateTemplate4 } ) ); + + // Activate only two templates + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command1", mStateTemplatePeriodic1000ms->id, LastKnownStateOperation::ACTIVATE } ); + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command2", mStateTemplatePeriodic400ms->id, LastKnownStateOperation::ACTIVATE } ); + inspector.inspectNewSignal( SIG_D, TimePoint{ 500, 500 }, 1.0 ); + inspector.inspectNewSignal( SIG_E, TimePoint{ 800, 800 }, 11.0 ); + inspector.inspectNewSignal( SIG_E, TimePoint{ 900, 900 }, 12.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 1000, 1000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 2 ); + + { + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate1" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_D ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 1.0 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 500 ); + } + + { + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 1 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate2" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_E ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 12.0 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 900 ); + } + } + + inspector.inspectNewSignal( SIG_D, TimePoint{ 1100, 1100 }, 2.0 ); + inspector.inspectNewSignal( SIG_E, TimePoint{ 1100, 1100 }, 13.0 ); + // Last trigger time for SIG_E is 1000, update period is set as 400, hence we should not trigger update this time + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 1300, 1300 } ), nullptr ); + + // Activate the other template + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command3", stateTemplate4->id, LastKnownStateOperation::ACTIVATE } ); + inspector.inspectNewSignal( SIG_F, TimePoint{ 1000, 1000 }, 100.0 ); + inspector.inspectNewSignal( SIG_G, TimePoint{ 1100, 1100 }, 110.0 ); + + { + // Here we shall expect the update for signal E + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 1400, 1400 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 2 ); + + { + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate2" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_E ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1100 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 13.0 ); + } + + { + // For the second template, we should expect a snapshot with both signals + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 1 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate4" ); + ASSERT_EQ( inspectionOutput.signals.size(), 2 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1000 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 100.0 ); + ASSERT_EQ( inspectionOutput.signals[1].signalID, SIG_G ); + ASSERT_EQ( inspectionOutput.signals[1].receiveTime, 1100 ); + ASSERT_EQ( inspectionOutput.signals[1].getValue().value.doubleVal, 110.0 ); + } + } + + inspector.inspectNewSignal( SIG_F, TimePoint{ 1420, 1420 }, 101.0 ); + inspector.inspectNewSignal( SIG_G, TimePoint{ 1450, 1450 }, 111.0 ); + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 1900, 1900 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate4" ); + ASSERT_EQ( inspectionOutput.signals.size(), 2 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1420 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 101.0 ); + ASSERT_EQ( inspectionOutput.signals[1].signalID, SIG_G ); + ASSERT_EQ( inspectionOutput.signals[1].receiveTime, 1450 ); + ASSERT_EQ( inspectionOutput.signals[1].getValue().value.doubleVal, 111.0 ); + } + + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 2000, 2000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate1" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_D ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1100 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 2.0 ); + } + + // Now ensure that after deactivating a template, no signal from that template is collected + inspector.onNewCommandReceived( LastKnownStateCommandRequest{ + "command3", mStateTemplatePeriodic1000ms->id, LastKnownStateOperation::DEACTIVATE } ); + inspector.inspectNewSignal( SIG_D, TimePoint{ 2600, 2600 }, 3.0 ); + inspector.inspectNewSignal( SIG_F, TimePoint{ 2650, 2650 }, 102.0 ); + + { + // Only the signal from the second template should be collected now + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 4000, 4000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate4" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 2650 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 102.0 ); + } + + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command3", stateTemplate4->id, LastKnownStateOperation::DEACTIVATE } ); + inspector.inspectNewSignal( SIG_D, TimePoint{ 4100, 4100 }, 3.0 ); + inspector.inspectNewSignal( SIG_F, TimePoint{ 4100, 4100 }, 102.0 ); + + // Nothing else should be collected now + { + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 6000, 6000 } ); + ASSERT_EQ( dataToSend, nullptr ); + } +} + +TEST_F( LastKnownStateInspectorDoubleTest, inspectSignalWithUpdatedInspectionLogic ) +{ + // We first set SIG_A as on change policy + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ mStateTemplateOnChange } ) ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", mStateTemplateOnChange->id, LastKnownStateOperation::ACTIVATE } ); + inspector.inspectNewSignal( SIG_A, TimePoint{ 500, 500 }, 0.1 ); + auto dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 550, 550 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 500 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.1 ); + + inspector.inspectNewSignal( SIG_A, TimePoint{ 600, 600 }, 0.2 ); + dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 650, 650 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 600 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.2 ); + + inspector.inspectNewSignal( SIG_A, TimePoint{ 700, 700 }, 0.3 ); + auto stateTemplate4 = std::make_shared( + StateTemplateInformation{ "stateTemplate4", + "decoder", + { LastKnownStateSignalInformation{ SIG_A, SignalType::DOUBLE } }, + LastKnownStateUpdateStrategy::PERIODIC, + 1000 } ); + // Here we change update policy to one-second period + inspector.onStateTemplatesChanged( std::make_shared( StateTemplateList{ stateTemplate4 } ) ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command2", stateTemplate4->id, LastKnownStateOperation::ACTIVATE } ); + // The first collection should be a snapshot with all signals for the newly activate state template. + dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 750, 750 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.3 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 700 ); + + // This sample shall be collected as it's the first sample + inspector.inspectNewSignal( SIG_A, TimePoint{ 800, 800 }, 0.3 ); + + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 1700, 1700 } ), nullptr ); + // Collect at 1000 after the time it tried to send the snapshot + dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 1750, 1750 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + + inspector.inspectNewSignal( SIG_A, TimePoint{ 1100, 1100 }, 0.3 ); + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 2700, 2700 } ), nullptr ); + + dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 2750, 2750 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.3 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1100 ); + + // Here we change period to two-second + auto stateTemplate5 = std::make_shared( + StateTemplateInformation{ "stateTemplate5", + "decoder", + { LastKnownStateSignalInformation{ SIG_A, SignalType::DOUBLE } }, + LastKnownStateUpdateStrategy::PERIODIC, + 2000 } ); + // Here we change update policy to one-second period + inspector.onStateTemplatesChanged( std::make_shared( StateTemplateList{ stateTemplate5 } ) ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command3", stateTemplate5->id, LastKnownStateOperation::ACTIVATE } ); + + inspector.inspectNewSignal( SIG_A, TimePoint{ 2850, 2850 }, 0.3 ); + // The first collected data should be a snapshot. Then, the period should start counting from + // the time the snapshot was generated. + dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 2950, 2950 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.3 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 2850 ); + + inspector.inspectNewSignal( SIG_A, TimePoint{ 3050, 3050 }, 0.3 ); + // Because we changed period to 2-second + ASSERT_EQ( collectNextDataToSendSorted( inspector, TimePoint{ 3950, 3950 } ), nullptr ); + + dataToSend = collectNextDataToSendSorted( inspector, TimePoint{ 4950, 4950 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals[0].signalID, SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.3 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 3050 ); +} + +/** + * Here's the test scenarios: + * signal A (on change) | | + * signal B (on change) | + * signal F (period: 1s) | | + * signal G (period: 0.4s) | | | + * Inspection | | | | + */ +TYPED_TEST( LastKnownStateInspectorTest, inspectSignalsPeriodicallyAndOnChange ) +{ + const SignalID SIG_A = 1; + const SignalID SIG_B = 2; + const SignalID SIG_F = 6; + const SignalID SIG_G = 7; + LastKnownStateInspector inspector( this->mCommandResponses, nullptr ); + auto stateTemplate4 = std::make_shared( + StateTemplateInformation{ "stateTemplate4", + "decoder", + { LastKnownStateSignalInformation{ SIG_A, SignalType::DOUBLE }, + LastKnownStateSignalInformation{ SIG_B, SignalType::INT64 } }, + LastKnownStateUpdateStrategy::ON_CHANGE } ); + auto stateTemplate5 = std::make_shared( + StateTemplateInformation{ "stateTemplate5", + "decoder", + { LastKnownStateSignalInformation{ SIG_F, getSignalType() } }, + LastKnownStateUpdateStrategy::PERIODIC, + 1000 } ); + auto stateTemplate6 = std::make_shared( + StateTemplateInformation{ "stateTemplate6", + "decoder", + { LastKnownStateSignalInformation{ SIG_G, getSignalType() } }, + LastKnownStateUpdateStrategy::PERIODIC, + 400 } ); + + inspector.onStateTemplatesChanged( + std::make_shared( StateTemplateList{ stateTemplate4, stateTemplate5, stateTemplate6 } ) ); + + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command1", stateTemplate4->id, LastKnownStateOperation::ACTIVATE } ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command2", stateTemplate5->id, LastKnownStateOperation::ACTIVATE } ); + inspector.onNewCommandReceived( + LastKnownStateCommandRequest{ "command3", stateTemplate6->id, LastKnownStateOperation::ACTIVATE } ); + + // First is a snapshot triggered by each activate command + inspector.inspectNewSignal( SIG_A, TimePoint{ 50, 50 }, 0.05 ); + inspector.inspectNewSignal( SIG_F, TimePoint{ 50, 50 }, 0 ); + inspector.inspectNewSignal( SIG_G, TimePoint{ 80, 80 }, 1 ); + inspector.inspectNewSignal( SIG_G, TimePoint{ 90, 90 }, 2 ); + auto dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint{ 0, 0 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 3 ); + + inspector.inspectNewSignal( SIG_A, TimePoint{ 500, 500 }, 0.1 ); + inspector.inspectNewSignal( SIG_F, TimePoint{ 500, 500 }, 1 ); + inspector.inspectNewSignal( SIG_G, TimePoint{ 800, 800 }, 11 ); + inspector.inspectNewSignal( SIG_G, TimePoint{ 900, 900 }, 12 ); + dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint{ 1000, 1000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 3 ); + + auto inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate4" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, this->SIG_A ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), SignalType::DOUBLE ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.doubleVal, 0.1 ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 500 ); + + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 1 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate5" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, this->SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), getSignalType() ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 1, getSignalType() ) ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 500 ); + + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 2 ); + + ASSERT_EQ( inspectionOutput.stateTemplateSyncId, "stateTemplate6" ); + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, this->SIG_G ); + ASSERT_EQ( inspectionOutput.signals[0].getType(), getSignalType() ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 12, getSignalType() ) ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 900 ); + + inspector.inspectNewSignal( this->SIG_F, TimePoint{ 1100, 1100 }, 2.0 ); + inspector.inspectNewSignal( this->SIG_G, TimePoint{ 1100, 1100 }, 13 ); + inspector.inspectNewSignal( this->SIG_G, TimePoint{ 1100, 1100 }, 13 ); + inspector.inspectNewSignal( this->SIG_A, TimePoint{ 1100, 1100 }, 0.1 ); + inspector.inspectNewSignal( this->SIG_B, TimePoint{ 1100, 1100 }, 2 ); + dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint{ 1200, 1200 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + // We should not receive update for signal D and E because next period has not complete + // We will receive update from signal B + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, this->SIG_B ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1100 ); + ASSERT_EQ( inspectionOutput.signals[0].getValue().value.int64Val, 2 ); + + // Here we shall expect the update for signal E + dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint{ 1400, 1400 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, this->SIG_G ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1100 ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 13, getSignalType() ) ); + + dataToSend = this->collectNextDataToSendSorted( inspector, TimePoint{ 2000, 2000 } ); + ASSERT_NE( dataToSend, nullptr ); + ASSERT_EQ( dataToSend->stateTemplateCollectedSignals.size(), 1 ); + inspectionOutput = dataToSend->stateTemplateCollectedSignals.at( 0 ); + + ASSERT_EQ( inspectionOutput.signals.size(), 1 ); + ASSERT_EQ( inspectionOutput.signals[0].signalID, this->SIG_F ); + ASSERT_EQ( inspectionOutput.signals[0].receiveTime, 1100 ); + ASSERT_NO_FATAL_FAILURE( + assertSignalValue( inspectionOutput.signals[0].getValue(), 2, getSignalType() ) ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/LastKnownStateSchemaTest.cpp b/test/unit/LastKnownStateSchemaTest.cpp new file mode 100644 index 00000000..6d34c3f1 --- /dev/null +++ b/test/unit/LastKnownStateSchemaTest.cpp @@ -0,0 +1,232 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateSchema.h" +#include "AwsIotConnectivityModule.h" +#include "AwsIotReceiver.h" +#include "LastKnownStateIngestion.h" +#include "LastKnownStateTypes.h" +#include "MqttClientWrapper.h" +#include "SignalTypes.h" +#include "TopicConfig.h" +#include "state_templates.pb.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::Gt; +using ::testing::Return; +using ::testing::StrictMock; + +class LastKnownStateSchemaTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + TopicConfigArgs topicConfigArgs; + mTopicConfig = std::make_unique( "thing-name", topicConfigArgs ); + + mAwsIotModule = std::make_unique( "", "", nullptr, *mTopicConfig ); + + std::shared_ptr nullMqttClient; + + mReceiverLastKnownState = std::make_shared( mAwsIotModule.get(), nullMqttClient, "topic" ); + + mLastKnownStateSchema = std::make_unique( mReceiverLastKnownState ); + + mLastKnownStateSchema->subscribeToLastKnownStateReceived( + [&]( std::shared_ptr lastKnownStateIngestion ) { + mReceivedLastKnownStateIngestion = lastKnownStateIngestion; + } ); + } + + static Aws::Crt::Mqtt5::PublishReceivedEventData + createPublishEvent( const std::string &protoSerializedBuffer ) + { + auto publishPacket = std::make_shared(); + Aws::Crt::Mqtt5::PublishReceivedEventData eventData; + eventData.publishPacket = publishPacket; + publishPacket->WithPayload( Aws::Crt::ByteCursorFromArray( + reinterpret_cast( protoSerializedBuffer.data() ), protoSerializedBuffer.length() ) ); + + return eventData; + } + + std::unique_ptr mTopicConfig; + std::unique_ptr mAwsIotModule; + std::shared_ptr mReceiverLastKnownState; + std::unique_ptr mLastKnownStateSchema; + + std::shared_ptr mReceivedLastKnownStateIngestion; +}; + +TEST_F( LastKnownStateSchemaTest, ingestEmptyLastKnownState ) +{ + std::string protoSerializedBuffer; + + mReceiverLastKnownState->onDataReceived( createPublishEvent( protoSerializedBuffer ) ); + + // It is possible to receive an empty message, because an empty state template list in protobuf + // would be just empty data. + ASSERT_NE( mReceivedLastKnownStateIngestion, nullptr ); +} + +TEST_F( LastKnownStateSchemaTest, ingestLastKnownStateLargerThanLimit ) +{ + std::string protoSerializedBuffer( LAST_KNOWN_STATE_BYTE_SIZE_LIMIT + 1, 'X' ); + + mReceiverLastKnownState->onDataReceived( createPublishEvent( protoSerializedBuffer ) ); + + ASSERT_EQ( mReceivedLastKnownStateIngestion, nullptr ); +} + +TEST_F( LastKnownStateSchemaTest, ingestLastKnownStateWithoutStateTemplates ) +{ + Schemas::LastKnownState::StateTemplates protoLastKnownState; + + std::string protoSerializedBuffer; + ASSERT_TRUE( protoLastKnownState.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + mReceiverLastKnownState->onDataReceived( publishEvent ); + + // This should be false because we just copied the data and it needs to be built first + ASSERT_FALSE( mReceivedLastKnownStateIngestion->isReady() ); + + ASSERT_FALSE( mReceivedLastKnownStateIngestion->build() ); + ASSERT_FALSE( mReceivedLastKnownStateIngestion->isReady() ); +} + +TEST_F( LastKnownStateSchemaTest, ingestLastKnownStateWithSignals ) +{ + Schemas::LastKnownState::StateTemplates protoLastKnownState; + protoLastKnownState.set_version( 456 ); + protoLastKnownState.set_decoder_manifest_sync_id( "decoder1" ); + auto *protoStateTemplateInfo = protoLastKnownState.add_state_templates_to_add(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->add_signal_ids( 1 ); + protoStateTemplateInfo->add_signal_ids( 2 ); + auto periodicUpdateStrategy = new Schemas::LastKnownState::PeriodicUpdateStrategy(); + periodicUpdateStrategy->set_period_ms( 2000 ); + protoStateTemplateInfo->set_allocated_periodic_update_strategy( periodicUpdateStrategy ); + + protoStateTemplateInfo = protoLastKnownState.add_state_templates_to_add(); + protoStateTemplateInfo->set_state_template_sync_id( "lks2" ); + protoStateTemplateInfo->add_signal_ids( 3 ); + protoStateTemplateInfo->add_signal_ids( 4 ); + auto onChangeUpdateStrategy = new Schemas::LastKnownState::OnChangeUpdateStrategy(); + protoStateTemplateInfo->set_allocated_on_change_update_strategy( onChangeUpdateStrategy ); + + protoLastKnownState.add_state_template_sync_ids_to_remove( "lks10" ); + protoLastKnownState.add_state_template_sync_ids_to_remove( "lks11" ); + + std::string protoSerializedBuffer; + ASSERT_TRUE( protoLastKnownState.SerializeToString( &protoSerializedBuffer ) ); + + auto publishEvent = createPublishEvent( protoSerializedBuffer ); + mReceiverLastKnownState->onDataReceived( publishEvent ); + + ASSERT_TRUE( mReceivedLastKnownStateIngestion->build() ); + + ASSERT_TRUE( mReceivedLastKnownStateIngestion->isReady() ); + auto stateTemplatesDiff = mReceivedLastKnownStateIngestion->getStateTemplatesDiff(); + + ASSERT_EQ( stateTemplatesDiff->version, 456 ); + + ASSERT_EQ( stateTemplatesDiff->stateTemplatesToRemove, std::vector( { "lks10", "lks11" } ) ); + + auto &stateTemplatesToAdd = stateTemplatesDiff->stateTemplatesToAdd; + ASSERT_EQ( stateTemplatesToAdd.size(), 2 ); + + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->id, "lks1" ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->updateStrategy, LastKnownStateUpdateStrategy::PERIODIC ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->periodMs, 2000 ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->signals.size(), 2 ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->signals[0].signalID, 1 ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->signals[1].signalID, 2 ); + + ASSERT_EQ( stateTemplatesToAdd.at( 1 )->id, "lks2" ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplatesToAdd.at( 1 )->updateStrategy, LastKnownStateUpdateStrategy::ON_CHANGE ); + ASSERT_EQ( stateTemplatesToAdd.at( 1 )->signals.size(), 2 ); + ASSERT_EQ( stateTemplatesToAdd.at( 1 )->signals[0].signalID, 3 ); + ASSERT_EQ( stateTemplatesToAdd.at( 1 )->signals[1].signalID, 4 ); +} + +TEST_F( LastKnownStateSchemaTest, ingestLastKnownStateWithInvalidDecoderManifest ) +{ + Schemas::LastKnownState::StateTemplates protoLastKnownState; + protoLastKnownState.set_version( 456 ); + // Empty decoder manifest ID. This message should fail to be processed. + protoLastKnownState.set_decoder_manifest_sync_id( "" ); + auto *protoStateTemplateInfo = protoLastKnownState.add_state_templates_to_add(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->add_signal_ids( 1 ); + auto onChangeUpdateStrategy = new Schemas::LastKnownState::OnChangeUpdateStrategy(); + protoStateTemplateInfo->set_allocated_on_change_update_strategy( onChangeUpdateStrategy ); + + std::string protoSerializedBuffer; + ASSERT_TRUE( protoLastKnownState.SerializeToString( &protoSerializedBuffer ) ); + + mReceiverLastKnownState->onDataReceived( createPublishEvent( protoSerializedBuffer ) ); + + ASSERT_FALSE( mReceivedLastKnownStateIngestion->build() ); + ASSERT_FALSE( mReceivedLastKnownStateIngestion->isReady() ); +} + +TEST_F( LastKnownStateSchemaTest, ingestLastKnownStateWithInvalidStateTemplate ) +{ + Schemas::LastKnownState::StateTemplates protoLastKnownState; + protoLastKnownState.set_version( 456 ); + protoLastKnownState.set_decoder_manifest_sync_id( "decoder1" ); + auto *protoStateTemplateInfo = protoLastKnownState.add_state_templates_to_add(); + protoStateTemplateInfo->set_state_template_sync_id( "lks1" ); + protoStateTemplateInfo->add_signal_ids( 1 ); + protoStateTemplateInfo->set_allocated_on_change_update_strategy( + new Schemas::LastKnownState::OnChangeUpdateStrategy() ); + + // No state template sync ID. This state template should be skipped. + protoStateTemplateInfo = protoLastKnownState.add_state_templates_to_add(); + protoStateTemplateInfo->add_signal_ids( 2 ); + protoStateTemplateInfo->set_allocated_on_change_update_strategy( + new Schemas::LastKnownState::OnChangeUpdateStrategy() ); + + std::string protoSerializedBuffer; + ASSERT_TRUE( protoLastKnownState.SerializeToString( &protoSerializedBuffer ) ); + + mReceiverLastKnownState->onDataReceived( createPublishEvent( protoSerializedBuffer ) ); + + ASSERT_TRUE( mReceivedLastKnownStateIngestion->build() ); + + ASSERT_TRUE( mReceivedLastKnownStateIngestion->isReady() ); + auto stateTemplatesDiff = mReceivedLastKnownStateIngestion->getStateTemplatesDiff(); + + ASSERT_EQ( stateTemplatesDiff->version, 456 ); + ASSERT_EQ( stateTemplatesDiff->stateTemplatesToRemove, std::vector() ); + + auto &stateTemplatesToAdd = stateTemplatesDiff->stateTemplatesToAdd; + ASSERT_EQ( stateTemplatesToAdd.size(), 1 ); + + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->id, "lks1" ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->decoderManifestID, "decoder1" ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->updateStrategy, LastKnownStateUpdateStrategy::ON_CHANGE ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->signals.size(), 1 ); + ASSERT_EQ( stateTemplatesToAdd.at( 0 )->signals[0].signalID, 1 ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/LastKnownStateWorkerThreadTest.cpp b/test/unit/LastKnownStateWorkerThreadTest.cpp new file mode 100644 index 00000000..f76f520c --- /dev/null +++ b/test/unit/LastKnownStateWorkerThreadTest.cpp @@ -0,0 +1,361 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "LastKnownStateWorkerThread.h" +#include "CANDataTypes.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommandTypes.h" +#include "DataSenderTypes.h" +#include "ICommandDispatcher.h" +#include "LastKnownStateInspector.h" +#include "LastKnownStateTypes.h" +#include "QueueTypes.h" +#include "SignalTypes.h" +#include "Testing.h" +#include "TimeTypes.h" +#include "WaitUntil.h" +#include "state_templates.pb.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::InvokeArgument; +using ::testing::Return; +using ::testing::Sequence; + +using signalTypes = + ::testing::Types; + +template +class LastKnownStateWorkerThreadTest : public ::testing::Test +{ +public: + void + SetUp() override + { + mCommandResponses = std::make_shared( 100, "Command Responses" ); + mLastKnownStateInspector = std::make_unique( mCommandResponses, nullptr ); + mSignalBufferPtr = std::make_shared( 100, "Signal Buffer" ); + mCollectedSignals = std::make_shared( 2, "LKS Signals" ); + } + void + TearDown() override + { + mLastKnownStateWorkerThread->stop(); + } + + bool + popCommandResponse( std::shared_ptr &commandResponse ) + { + std::shared_ptr senderData; + auto succeeded = mCommandResponses->pop( senderData ); + commandResponse = std::dynamic_pointer_cast( senderData ); + return succeeded; + } + + bool + popCollectedData( std::shared_ptr &collectedData ) + { + std::shared_ptr senderData; + auto succeeded = mCollectedSignals->pop( senderData ); + collectedData = std::dynamic_pointer_cast( senderData ); + return succeeded; + } + +protected: + std::unique_ptr mLastKnownStateInspector; + std::unique_ptr mLastKnownStateWorkerThread; + std::shared_ptr mCommandResponses; + SignalBufferPtr mSignalBufferPtr; + std::shared_ptr mClock = ClockHandler::getClock(); + std::shared_ptr mCollectedSignals; +}; + +class LastKnownStateWorkerDoubleTest : public LastKnownStateWorkerThreadTest +{ +}; + +TYPED_TEST_SUITE( LastKnownStateWorkerThreadTest, signalTypes ); + +TYPED_TEST( LastKnownStateWorkerThreadTest, SuccessfulProcessing ) +{ + this->mLastKnownStateWorkerThread = std::make_unique( + this->mSignalBufferPtr, this->mCollectedSignals, std::move( this->mLastKnownStateInspector ), 1000 ); + ASSERT_TRUE( this->mLastKnownStateWorkerThread->start() ); + + WAIT_ASSERT_TRUE( this->mLastKnownStateWorkerThread->isAlive() ); + + Schemas::LastKnownState::StateTemplates protoLastKnownState; + auto stateTemplateInfo = std::make_shared( + StateTemplateInformation{ "lks1", + "decoder1", + { LastKnownStateSignalInformation{ 1, getSignalType() } }, + LastKnownStateUpdateStrategy::PERIODIC, + 2000 } ); + + this->mLastKnownStateWorkerThread->onStateTemplatesChanged( + std::make_shared( StateTemplateList{ stateTemplateInfo } ) ); + + Timestamp timestamp = this->mClock->systemTimeSinceEpochMs(); + CollectedSignalsGroup collectedSignalsGroup; + collectedSignalsGroup.push_back( CollectedSignal( 1, timestamp, 10, getSignalType() ) ); + this->mSignalBufferPtr->push( CollectedDataFrame( collectedSignalsGroup ) ); + + // Now send the Activate command, which should trigger a collection + LastKnownStateCommandRequest activateCommand; + activateCommand.commandID = "command1"; + activateCommand.stateTemplateID = "lks1"; + activateCommand.operation = LastKnownStateOperation::ACTIVATE; + this->mLastKnownStateWorkerThread->onNewCommandReceived( activateCommand ); + + WAIT_ASSERT_FALSE( this->mCollectedSignals->isEmpty() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, FailOnStartNoSignalBuffer ) +{ + mLastKnownStateWorkerThread = std::make_unique( + nullptr, mCollectedSignals, std::move( mLastKnownStateInspector ), 0 ); + ASSERT_FALSE( mLastKnownStateWorkerThread->start() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, FailOnStartNoCollectedSignalsQueue ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, nullptr, std::move( mLastKnownStateInspector ), 0 ); + ASSERT_FALSE( mLastKnownStateWorkerThread->start() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, ActivateAndDeactivateStateTemplate ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, + mCollectedSignals, + std::make_unique( mCommandResponses, nullptr ), + 50 ); + ASSERT_TRUE( mLastKnownStateWorkerThread->start() ); + WAIT_ASSERT_TRUE( mLastKnownStateWorkerThread->isAlive() ); + + auto stateTemplateInfo = std::make_shared( StateTemplateInformation{ + "lks1", "decoder1", { LastKnownStateSignalInformation{ 1 } }, LastKnownStateUpdateStrategy::PERIODIC, 100 } ); + + mLastKnownStateWorkerThread->onStateTemplatesChanged( + std::make_shared( StateTemplateList{ stateTemplateInfo } ) ); + + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + + // It should not collect any signals until an Activate command is received + DELAY_ASSERT_TRUE( mCollectedSignals->isEmpty() ); + + std::shared_ptr commandResponse; + + // Now send the Activate command, which should trigger a collection + LastKnownStateCommandRequest activateCommand; + activateCommand.commandID = "command1"; + activateCommand.stateTemplateID = "lks1"; + activateCommand.operation = LastKnownStateOperation::ACTIVATE; + activateCommand.receivedTime = mClock->timeSinceEpoch(); + mLastKnownStateWorkerThread->onNewCommandReceived( activateCommand ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + + std::shared_ptr collectedData; + + WAIT_ASSERT_TRUE( popCollectedData( collectedData ) ); + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + WAIT_ASSERT_TRUE( popCollectedData( collectedData ) ); + + // Now send the Deactivate command, which should stop the collection + LastKnownStateCommandRequest deactivateCommand; + deactivateCommand.commandID = "command2"; + deactivateCommand.stateTemplateID = "lks1"; + deactivateCommand.operation = LastKnownStateOperation::DEACTIVATE; + mLastKnownStateWorkerThread->onNewCommandReceived( deactivateCommand ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command2" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + + // Send a FetchSnapshot command, which should send a snapshot even if the state template is deactivated + LastKnownStateCommandRequest fetchSnapshotCommand; + fetchSnapshotCommand.commandID = "command3"; + fetchSnapshotCommand.stateTemplateID = "lks1"; + fetchSnapshotCommand.operation = LastKnownStateOperation::FETCH_SNAPSHOT; + mLastKnownStateWorkerThread->onNewCommandReceived( fetchSnapshotCommand ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command3" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + + WAIT_ASSERT_TRUE( popCollectedData( collectedData ) ); + + // Now that the deactivate command was processed, this signal should not be collected + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + DELAY_ASSERT_TRUE( mCollectedSignals->isEmpty() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, ActivateWithAutoDeactivate ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, mCollectedSignals, std::move( mLastKnownStateInspector ), 900 ); + ASSERT_TRUE( mLastKnownStateWorkerThread->start() ); + WAIT_ASSERT_TRUE( mLastKnownStateWorkerThread->isAlive() ); + + auto stateTemplateInfo = std::make_shared( StateTemplateInformation{ + "lks1", "decoder1", { LastKnownStateSignalInformation{ 1 } }, LastKnownStateUpdateStrategy::PERIODIC, 800 } ); + + mLastKnownStateWorkerThread->onStateTemplatesChanged( + std::make_shared( StateTemplateList{ stateTemplateInfo } ) ); + + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + + std::shared_ptr commandResponse; + + // Now send the Activate command, which should trigger a collection + LastKnownStateCommandRequest activateCommand; + activateCommand.commandID = "command1"; + activateCommand.stateTemplateID = "lks1"; + activateCommand.operation = LastKnownStateOperation::ACTIVATE; + activateCommand.receivedTime = mClock->timeSinceEpoch(); + // Let data to be collected three times (snapshot + 2 periods) and on the fourth time it should be deactivated. + activateCommand.deactivateAfterSeconds = 2; + mLastKnownStateWorkerThread->onNewCommandReceived( activateCommand ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::SUCCEEDED ); + + std::shared_ptr collectedData; + + // First should be a snapshot, then the next two should be the periodic updates, then after that + // auto deactivate should be triggered. + WAIT_ASSERT_TRUE( popCollectedData( collectedData ) ); + + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + WAIT_ASSERT_TRUE( popCollectedData( collectedData ) ); + + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + WAIT_ASSERT_TRUE( popCollectedData( collectedData ) ); + + // Now the auto deactivate time should have passed, so this signal should not be collected + mSignalBufferPtr->push( CollectedDataFrame( + CollectedSignalsGroup{ CollectedSignal( 1, mClock->systemTimeSinceEpochMs(), 10, SignalType::DOUBLE ) } ) ); + DELAY_ASSERT_TRUE( mCollectedSignals->isEmpty() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, SendErrorResponseWhenActivatingMissingStateTemplate ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, mCollectedSignals, std::move( mLastKnownStateInspector ), 0 ); + ASSERT_TRUE( mLastKnownStateWorkerThread->start() ); + WAIT_ASSERT_TRUE( mLastKnownStateWorkerThread->isAlive() ); + + auto stateTemplateInfo = std::make_shared( StateTemplateInformation{ + "lks1", "decoder1", { LastKnownStateSignalInformation{ 1 } }, LastKnownStateUpdateStrategy::PERIODIC, 2000 } ); + + mLastKnownStateWorkerThread->onStateTemplatesChanged( + std::make_shared( StateTemplateList{ stateTemplateInfo } ) ); + + std::shared_ptr commandResponse; + + // Now send the Activate command, which should trigger a collection + LastKnownStateCommandRequest activateCommand; + activateCommand.commandID = "command1"; + activateCommand.stateTemplateID = "invalid_lks"; + activateCommand.operation = LastKnownStateOperation::ACTIVATE; + activateCommand.receivedTime = mClock->timeSinceEpoch(); + mLastKnownStateWorkerThread->onNewCommandReceived( activateCommand ); + + WAIT_ASSERT_TRUE( popCommandResponse( commandResponse ) ); + ASSERT_EQ( commandResponse->id, "command1" ); + ASSERT_EQ( commandResponse->status, CommandStatus::EXECUTION_FAILED ); + ASSERT_EQ( commandResponse->reasonCode, REASON_CODE_STATE_TEMPLATE_OUT_OF_SYNC ); + DELAY_ASSERT_TRUE( mCollectedSignals->isEmpty() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, DataReadyWithoutStateTemplate ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, mCollectedSignals, std::move( mLastKnownStateInspector ), 0 ); + mSignalBufferPtr->subscribeToNewDataAvailable( + std::bind( &LastKnownStateWorkerThread::onNewDataAvailable, mLastKnownStateWorkerThread.get() ) ); + ASSERT_TRUE( mLastKnownStateWorkerThread->start() ); + + Timestamp timestamp = mClock->systemTimeSinceEpochMs(); + + mSignalBufferPtr->push( CollectedDataFrame( { CollectedSignal( 1234, timestamp, 0.1, SignalType::DOUBLE ) } ) ); + + // The input signal buffer should be consumed even when there is no state template because otherwise it will become + // full and new data will be discarded. + WAIT_ASSERT_TRUE( mSignalBufferPtr->isEmpty() ); + ASSERT_TRUE( mCollectedSignals->isEmpty() ); + + // Make sure that even after the first iteration, the worker continues consuming data that is arriving + mSignalBufferPtr->push( CollectedDataFrame( { CollectedSignal( 5678, timestamp, 0.1, SignalType::DOUBLE ) } ) ); + + WAIT_ASSERT_TRUE( mSignalBufferPtr->isEmpty() ); + ASSERT_TRUE( mCollectedSignals->isEmpty() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, EmptyStateTemplate ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, mCollectedSignals, std::move( mLastKnownStateInspector ), 0 ); + ASSERT_TRUE( mLastKnownStateWorkerThread->start() ); + + WAIT_ASSERT_TRUE( mLastKnownStateWorkerThread->isAlive() ); + + mLastKnownStateWorkerThread->onStateTemplatesChanged( std::make_shared( StateTemplateList{} ) ); + + std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); + + ASSERT_TRUE( mCollectedSignals->isEmpty() ); +} + +TEST_F( LastKnownStateWorkerDoubleTest, DtcAndCanRawFrame ) +{ + mLastKnownStateWorkerThread = std::make_unique( + mSignalBufferPtr, mCollectedSignals, std::move( mLastKnownStateInspector ), 1000 ); + ASSERT_TRUE( mLastKnownStateWorkerThread->start() ); + + WAIT_ASSERT_TRUE( mLastKnownStateWorkerThread->isAlive() ); + + auto stateTemplateInfo = std::make_shared( StateTemplateInformation{ + "lks1", "decoder1", { LastKnownStateSignalInformation{ 1 } }, LastKnownStateUpdateStrategy::PERIODIC, 2000 } ); + + mLastKnownStateWorkerThread->onStateTemplatesChanged( + std::make_shared( StateTemplateList{ stateTemplateInfo } ) ); + + Timestamp timestamp = mClock->systemTimeSinceEpochMs(); + std::array buf1 = { 0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x0, 0x0, 0x0 }; + CollectedSignalsGroup collectedSignalsGroup; + mSignalBufferPtr->push( CollectedDataFrame( + collectedSignalsGroup, std::make_shared( 1, 1, timestamp, buf1, sizeof( buf1 ) ) ) ); + + std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); + ASSERT_TRUE( mCollectedSignals->isEmpty() ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/NamedSignalDataSourceTest.cpp b/test/unit/NamedSignalDataSourceTest.cpp new file mode 100644 index 00000000..b7debf4a --- /dev/null +++ b/test/unit/NamedSignalDataSourceTest.cpp @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "NamedSignalDataSource.h" +#include "CollectionInspectionAPITypes.h" +#include "IDecoderDictionary.h" +#include "IDecoderManifest.h" +#include "QueueTypes.h" +#include "SignalTypes.h" +#include "VehicleDataSourceTypes.h" +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class NamedSignalDataSourceTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + mDictionary = std::make_shared(); + mDictionary->customDecoderMethod["5"]["Vehicle.MySignal1"] = + CustomSignalDecoderFormat{ "5", "Vehicle.MySignal1", 1, SignalType::DOUBLE }; + mDictionary->customDecoderMethod["5"]["Vehicle.MySignal2"] = + CustomSignalDecoderFormat{ "5", "Vehicle.MySignal2", 2, SignalType::DOUBLE }; + } + + void + TearDown() override + { + } + + std::shared_ptr mDictionary; +}; + +TEST_F( NamedSignalDataSourceTest, testNoDecoderDictionary ) +{ + auto signalBuffer = std::make_shared( 100, "Signal Buffer" ); + auto signalBufferDistributor = std::make_shared(); + signalBufferDistributor->registerQueue( signalBuffer ); + NamedSignalDataSource namedSignalSource( "5", signalBufferDistributor ); + + namedSignalSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.MySignal1" ), INVALID_SIGNAL_ID ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.MySignal2" ), INVALID_SIGNAL_ID ); + namedSignalSource.ingestSignalValue( 0, "Vehicle.MySignal1", DecodedSignalValue( 123, SignalType::DOUBLE ) ); + std::vector> values; + values.push_back( std::make_pair( "Vehicle.MySignal1", DecodedSignalValue( 456, SignalType::DOUBLE ) ) ); + values.push_back( std::make_pair( "Vehicle.MySignal2", DecodedSignalValue( 789, SignalType::DOUBLE ) ) ); + namedSignalSource.ingestMultipleSignalValues( 1, values ); + CollectedDataFrame collectedDataFrame; + ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); + namedSignalSource.onChangeOfActiveDictionary( nullptr, VehicleDataSourceProtocol::CUSTOM_DECODING ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.MySignal1" ), INVALID_SIGNAL_ID ); + namedSignalSource.ingestSignalValue( 0, "Vehicle.MySignal1", DecodedSignalValue( 123, SignalType::DOUBLE ) ); + ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); +} + +TEST_F( NamedSignalDataSourceTest, wrongInterface ) +{ + auto signalBuffer = std::make_shared( 100, "Signal Buffer" ); + auto signalBufferDistributor = std::make_shared(); + signalBufferDistributor->registerQueue( signalBuffer ); + NamedSignalDataSource namedSignalSource( "2", signalBufferDistributor ); // Unknown interface + namedSignalSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.MySignal1" ), INVALID_SIGNAL_ID ); + namedSignalSource.ingestSignalValue( 0, "Vehicle.MySignal1", DecodedSignalValue( 123, SignalType::DOUBLE ) ); + std::vector> values; + values.push_back( std::make_pair( "Vehicle.MySignal1", DecodedSignalValue( 456, SignalType::DOUBLE ) ) ); + values.push_back( std::make_pair( "Vehicle.MySignal2", DecodedSignalValue( 789, SignalType::DOUBLE ) ) ); + namedSignalSource.ingestMultipleSignalValues( 1, values ); + CollectedDataFrame collectedDataFrame; + ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); +} + +TEST_F( NamedSignalDataSourceTest, testDecoding ) +{ + auto signalBuffer = std::make_shared( 2, "Signal Buffer" ); + auto signalBufferDistributor = std::make_shared(); + signalBufferDistributor->registerQueue( signalBuffer ); + NamedSignalDataSource namedSignalSource( "5", signalBufferDistributor ); + + namedSignalSource.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.SomeOtherSignal" ), INVALID_SIGNAL_ID ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.MySignal1" ), 1 ); + ASSERT_EQ( namedSignalSource.getNamedSignalID( "Vehicle.MySignal2" ), 2 ); + namedSignalSource.ingestSignalValue( + 0, "Vehicle.SomeOtherSignal", DecodedSignalValue( 123, SignalType::DOUBLE ) ); // Unknown signal + CollectedDataFrame collectedDataFrame; + ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); + + namedSignalSource.ingestSignalValue( 0, "Vehicle.MySignal1", DecodedSignalValue( 123, SignalType::DOUBLE ) ); + std::vector> values; + values.push_back( std::make_pair( "Vehicle.MySignal1", DecodedSignalValue( 456, SignalType::DOUBLE ) ) ); + values.push_back( std::make_pair( "Vehicle.MySignal2", DecodedSignalValue( 789, SignalType::DOUBLE ) ) ); + values.push_back( std::make_pair( "Vehicle.SomeOtherSignal", DecodedSignalValue( 222, SignalType::DOUBLE ) ) ); + namedSignalSource.ingestMultipleSignalValues( 1, values ); + namedSignalSource.ingestSignalValue( + 0, "Vehicle.MySignal1", DecodedSignalValue( 555, SignalType::DOUBLE ) ); // Queue full + namedSignalSource.ingestMultipleSignalValues( 1, values ); // Queue full + ASSERT_TRUE( signalBuffer->pop( collectedDataFrame ) ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 1 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[0].signalID, 1 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[0].value.value.doubleVal, 123, 0.0001 ); + ASSERT_TRUE( signalBuffer->pop( collectedDataFrame ) ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 2 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[0].signalID, 1 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[0].value.value.doubleVal, 456, 0.0001 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[0].receiveTime, 1 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[1].signalID, 2 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[1].value.value.doubleVal, 789, 0.0001 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[1].receiveTime, 1 ); + ASSERT_FALSE( signalBuffer->pop( collectedDataFrame ) ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/PersistencyTest.cpp b/test/unit/PersistencyTest.cpp index e164595c..35d12c9e 100644 --- a/test/unit/PersistencyTest.cpp +++ b/test/unit/PersistencyTest.cpp @@ -74,6 +74,15 @@ TEST( PersistencyTest, storeTest ) // getData() return >0, write returns memory_full gmocktest.store( DataType::COLLECTION_SCHEME_LIST ); gmocktest.store( DataType::DECODER_MANIFEST ); +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + auto lastKnownStateIngestionMock = std::make_shared(); + EXPECT_CALL( *lastKnownStateIngestionMock, getData() ).WillRepeatedly( ReturnRef( dataPL ) ); + EXPECT_CALL( *testPersistency, write( _, _, DataType::STATE_TEMPLATE_LIST, _ ) ) + .WillRepeatedly( Return( ErrorCode::SUCCESS ) ); + gmocktest.store( DataType::STATE_TEMPLATE_LIST ); + gmocktest.setLastKnownStateIngestion( lastKnownStateIngestionMock ); + gmocktest.store( DataType::STATE_TEMPLATE_LIST ); +#endif } /** @brief @@ -184,5 +193,117 @@ TEST( PersistencyTest, StoreAndRetrieve ) ASSERT_FALSE( WIFEXITED( ret ) == 0 ); } +#ifdef FWE_FEATURE_STORE_AND_FORWARD +/** @brief + * This test validates Store and Forward campaign persistency with the usage of the store/retrieve APIs. + */ +TEST( CollectionSchemeManagerTest2, StoreAndForwardPersistency ) +{ + int ret = std::system( "mkdir ./testPersist" ); + ASSERT_FALSE( WIFEXITED( ret ) == 0 ); + std::shared_ptr testPersistency = std::make_shared( "./testPersist", 4096 ); + ASSERT_TRUE( testPersistency->init() ); + + Schemas::CollectionSchemesMsg::CollectionSchemes protoCollectionSchemesMsg; + auto *collectionSchemeTestMessage = protoCollectionSchemesMsg.add_collection_schemes(); + collectionSchemeTestMessage->set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + collectionSchemeTestMessage->set_decoder_manifest_sync_id( "model_manifest_13" ); + collectionSchemeTestMessage->set_start_time_ms_epoch( 162144816000 ); + collectionSchemeTestMessage->set_expiry_time_ms_epoch( 262144816000 ); + + // Create an Event/Condition Based CollectionScheme + Schemas::CollectionSchemesMsg::ConditionBasedCollectionScheme *message = + collectionSchemeTestMessage->mutable_condition_based_collection_scheme(); + message->set_condition_minimum_interval_ms( 650 ); + message->set_condition_language_version( 20 ); + message->set_condition_trigger_mode( + Schemas::CollectionSchemesMsg::ConditionBasedCollectionScheme_ConditionTriggerMode_TRIGGER_ALWAYS ); + + // Create store and forward configuration + auto *store_and_forward_configuration = collectionSchemeTestMessage->mutable_store_and_forward_configuration(); + auto *store_and_forward_entry = store_and_forward_configuration->add_partition_configuration(); + auto *storage_options = store_and_forward_entry->mutable_storage_options(); + auto *upload_options = store_and_forward_entry->mutable_upload_options(); + storage_options->set_maximum_size_in_bytes( 1000000 ); + storage_options->set_storage_location( "/storage" ); + storage_options->set_minimum_time_to_live_in_seconds( 1000000 ); + + // Build the AST Tree: + //---------- + + upload_options->mutable_condition_tree()->set_node_signal_id( 10 ); + message->mutable_condition_tree(); + + //---------- + + collectionSchemeTestMessage->set_after_duration_ms( 0 ); + collectionSchemeTestMessage->set_include_active_dtcs( true ); + collectionSchemeTestMessage->set_persist_all_collected_data( true ); + collectionSchemeTestMessage->set_compress_collected_data( true ); + collectionSchemeTestMessage->set_priority( 5 ); + + // Add 3 Signals + Schemas::CollectionSchemesMsg::SignalInformation *signal1 = collectionSchemeTestMessage->add_signal_information(); + signal1->set_signal_id( 19 ); + signal1->set_sample_buffer_size( 5 ); + signal1->set_minimum_sample_period_ms( 500 ); + signal1->set_fixed_window_period_ms( 600 ); + signal1->set_condition_only_signal( true ); + signal1->set_data_partition_id( 1 ); + + Schemas::CollectionSchemesMsg::SignalInformation *signal2 = collectionSchemeTestMessage->add_signal_information(); + signal2->set_signal_id( 17 ); + signal2->set_sample_buffer_size( 10000 ); + signal2->set_minimum_sample_period_ms( 1000 ); + signal2->set_fixed_window_period_ms( 1000 ); + signal2->set_condition_only_signal( false ); + signal2->set_data_partition_id( 1 ); + + Schemas::CollectionSchemesMsg::SignalInformation *signal3 = collectionSchemeTestMessage->add_signal_information(); + signal3->set_signal_id( 3 ); + signal3->set_sample_buffer_size( 1000 ); + signal3->set_minimum_sample_period_ms( 100 ); + signal3->set_fixed_window_period_ms( 100 ); + signal3->set_condition_only_signal( true ); + signal3->set_data_partition_id( 1 ); + + // Add 1 RAW CAN Messages + Schemas::CollectionSchemesMsg::RawCanFrame *can1 = collectionSchemeTestMessage->add_raw_can_frames_to_collect(); + can1->set_can_interface_id( "1230" ); + can1->set_can_message_id( 0x1FF ); + can1->set_sample_buffer_size( 200 ); + can1->set_minimum_sample_period_ms( 255 ); + + std::string protoMessage; + ASSERT_TRUE( protoCollectionSchemesMsg.SerializeToString( &protoMessage ) ); + size_t sizePL = protoMessage.length(); + std::string dataPL = protoMessage; + + CANInterfaceIDTranslator canIDTranslator; + CollectionSchemeManagerWrapper testCollectionSchemeManager( + nullptr, canIDTranslator, std::make_shared( nullptr ) ); + testCollectionSchemeManager.setCollectionSchemePersistency( testPersistency ); + std::vector emptyCP; + ICollectionSchemeListPtr storePL = std::make_shared( emptyCP ); + storePL->copyData( reinterpret_cast( dataPL.c_str() ), sizePL ); + testCollectionSchemeManager.setCollectionSchemeList( storePL ); + testCollectionSchemeManager.store( DataType::COLLECTION_SCHEME_LIST ); + + ret = std::system( "ls -l ./testPersist >test.txt" ); + ASSERT_FALSE( WIFEXITED( ret ) == 0 ); + std::cout << std::ifstream( "test.txt" ).rdbuf(); + + ICollectionSchemeListPtr retrievePL = std::make_shared( emptyCP ); + testCollectionSchemeManager.setCollectionSchemeList( retrievePL ); + testCollectionSchemeManager.retrieve( DataType::COLLECTION_SCHEME_LIST ); + + std::vector orgData = storePL->getData(); + std::vector retData = retrievePL->getData(); + ASSERT_EQ( orgData, retData ); + ret = std::system( "rm -rf ./testPersist" ); + ASSERT_FALSE( WIFEXITED( ret ) == 0 ); +} +#endif + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/RateLimiterTest.cpp b/test/unit/RateLimiterTest.cpp new file mode 100644 index 00000000..6d3523aa --- /dev/null +++ b/test/unit/RateLimiterTest.cpp @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "RateLimiter.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "TimeTypes.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class RateLimiterTest : public ::testing::Test +{ +protected: + std::shared_ptr mClock = ClockHandler::getClock(); +}; + +TEST_F( RateLimiterTest, ConsumeToken ) +{ + uint64_t numTokens = 10; + auto rateLimiter = std::make_shared( numTokens, numTokens ); + + uint64_t consumedTokens = 0; + + uint64_t testDurationSeconds = 3; + auto startTime = mClock->timeSinceEpoch(); + while ( ( mClock->timeSinceEpoch().monotonicTimeMs - startTime.monotonicTimeMs ) / 1000 < testDurationSeconds ) + { + if ( rateLimiter->consumeToken() ) + { + ++consumedTokens; + } + std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) ); + } + ASSERT_TRUE( consumedTokens <= ( numTokens * testDurationSeconds ) ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/RemoteProfilerTest.cpp b/test/unit/RemoteProfilerTest.cpp index 369c5785..c6355db5 100644 --- a/test/unit/RemoteProfilerTest.cpp +++ b/test/unit/RemoteProfilerTest.cpp @@ -5,6 +5,7 @@ #include "IConnectionTypes.h" #include "LogLevel.h" #include "SenderMock.h" +#include "TopicConfig.h" #include "WaitUntil.h" #include #include @@ -40,12 +41,14 @@ checkMetrics( const std::string &data ) std::atomic a( 0 ); TEST( RemoteProfilerTest, MetricsUpload ) { - auto senderMock = std::make_shared>(); - EXPECT_CALL( *senderMock, mockedSendBuffer( _, Gt( 0 ), _ ) ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::Success ) ); + TopicConfigArgs topicConfigArgs; + topicConfigArgs.metricsTopic = "metrics-topic"; + TopicConfig topicConfig( "thing-name", topicConfigArgs ); + auto senderMock = std::make_shared>( topicConfig ); + EXPECT_CALL( *senderMock, mockedSendBuffer( "metrics-topic", _, Gt( 0 ), _ ) ) + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::Success ) ); - auto mockLogSender = std::make_shared(); - RemoteProfiler profiler( senderMock, mockLogSender, 1000, 1000, LogLevel::Trace, "Test" ); + RemoteProfiler profiler( senderMock, 1000, 1000, LogLevel::Trace, "Test" ); profiler.start(); // Generate some cpu load @@ -54,8 +57,8 @@ TEST( RemoteProfilerTest, MetricsUpload ) a++; } - WAIT_ASSERT_GT( senderMock->getSentBufferData().size(), 5U ); - for ( auto sentData : senderMock->getSentBufferData() ) + WAIT_ASSERT_GT( senderMock->getSentBufferDataByTopic( "metrics-topic" ).size(), 5U ); + for ( auto sentData : senderMock->getSentBufferDataByTopic( "metrics-topic" ) ) { ASSERT_NO_FATAL_FAILURE( checkMetrics( sentData.data ) ); } diff --git a/test/unit/SchemaTest.cpp b/test/unit/SchemaTest.cpp index c70522ef..0e678c63 100644 --- a/test/unit/SchemaTest.cpp +++ b/test/unit/SchemaTest.cpp @@ -19,6 +19,7 @@ #include "SenderMock.h" #include "SignalTypes.h" #include "TimeTypes.h" +#include "TopicConfig.h" #include "VehicleDataSourceTypes.h" #include "checkin.pb.h" #include "collection_schemes.pb.h" @@ -28,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -96,7 +96,10 @@ class SchemaTest : public ::testing::Test void SetUp() override { - mAwsIotModule = std::make_unique( "", "", nullptr ); + TopicConfigArgs topicConfigArgs; + mTopicConfig = std::make_unique( "thing-name", topicConfigArgs ); + + mAwsIotModule = std::make_unique( "", "", nullptr, *mTopicConfig ); std::shared_ptr nullMqttClient; @@ -107,8 +110,7 @@ class SchemaTest : public ::testing::Test mCollectionSchemeIngestion = std::make_unique( mReceiverDecoderManifest, mReceiverCollectionSchemeList, - std::make_shared( - mAwsIotModule.get(), nullMqttClient, "topic", Aws::Crt::Mqtt5::QOS::AWS_MQTT5_QOS_AT_MOST_ONCE ) ); + std::make_shared( mAwsIotModule.get(), nullMqttClient, *mTopicConfig ) ); mCollectionSchemeIngestion->subscribeToDecoderManifestUpdate( [&]( const IDecoderManifestPtr &decoderManifest ) { @@ -135,6 +137,8 @@ class SchemaTest : public ::testing::Test receiver->onDataReceived( eventData ); } + std::string mCheckinTopic = "$aws/iotfleetwise/vehicles/thing-name/checkins"; + std::unique_ptr mTopicConfig; std::unique_ptr mAwsIotModule; std::shared_ptr mReceiverDecoderManifest; std::shared_ptr mReceiverCollectionSchemeList; @@ -147,7 +151,7 @@ class SchemaTest : public ::testing::Test TEST_F( SchemaTest, Checkins ) { // Create a dummy AwsIotConnectivityModule object so that we can create dummy IReceiver objects - auto awsIotModule = std::make_shared( "", "", nullptr ); + auto awsIotModule = std::make_shared( "", "", nullptr, *mTopicConfig ); // Create a mock Sender auto senderMock = std::make_shared>(); @@ -164,22 +168,22 @@ TEST_F( SchemaTest, Checkins ) std::vector sampleDocList; Sequence seq; - EXPECT_CALL( *senderMock, mockedSendBuffer( _, Gt( 0 ), _ ) ) + EXPECT_CALL( *senderMock, mockedSendBuffer( mCheckinTopic, _, Gt( 0 ), _ ) ) .Times( 3 ) .InSequence( seq ) - .WillRepeatedly( InvokeArgument<2>( ConnectivityError::Success ) ); - EXPECT_CALL( *senderMock, mockedSendBuffer( _, Gt( 0 ), _ ) ) + .WillRepeatedly( InvokeArgument<3>( ConnectivityError::Success ) ); + EXPECT_CALL( *senderMock, mockedSendBuffer( mCheckinTopic, _, Gt( 0 ), _ ) ) .InSequence( seq ) - .WillOnce( InvokeArgument<2>( ConnectivityError::NoConnection ) ); + .WillOnce( InvokeArgument<3>( ConnectivityError::NoConnection ) ); MockFunction resultCallback; // Test an empty checkin EXPECT_CALL( resultCallback, Call( true ) ).Times( 1 ); collectionSchemeIngestion.sendCheckin( sampleDocList, resultCallback.AsStdFunction() ); - ASSERT_EQ( senderMock->getSentBufferData().size(), 1 ); - ASSERT_NO_FATAL_FAILURE( - assertCheckin( senderMock->getSentBufferData()[0].data, sampleDocList, timeBeforeCheckin ) ); + ASSERT_EQ( senderMock->getSentBufferDataByTopic( mCheckinTopic ).size(), 1 ); + ASSERT_NO_FATAL_FAILURE( assertCheckin( + senderMock->getSentBufferDataByTopic( mCheckinTopic )[0].data, sampleDocList, timeBeforeCheckin ) ); // Add some doc arns sampleDocList.emplace_back( "DocArn1" ); @@ -199,9 +203,9 @@ TEST_F( SchemaTest, Checkins ) // Second call should simulate a offboardconnectivity issue, the checkin message should fail to send. EXPECT_CALL( resultCallback, Call( false ) ).Times( 1 ); collectionSchemeIngestion.sendCheckin( sampleDocList, resultCallback.AsStdFunction() ); - ASSERT_EQ( senderMock->getSentBufferData().size(), 4 ); - ASSERT_NO_FATAL_FAILURE( - assertCheckin( senderMock->getSentBufferData()[3].data, sampleDocList, timeBeforeCheckin ) ); + ASSERT_EQ( senderMock->getSentBufferDataByTopic( mCheckinTopic ).size(), 4 ); + ASSERT_NO_FATAL_FAILURE( assertCheckin( + senderMock->getSentBufferDataByTopic( mCheckinTopic )[3].data, sampleDocList, timeBeforeCheckin ) ); } /** @@ -284,6 +288,16 @@ TEST_F( SchemaTest, DecoderManifestIngestion ) protoOBDPIDSignalB->set_bit_mask_length( 8 ); protoOBDPIDSignalB->set_primitive_type( Schemas::DecoderManifestMsg::PrimitiveType::UINT32 ); + auto protoCustomDecodedSignalA = protoDM.add_custom_decoding_signals(); + protoCustomDecodedSignalA->set_signal_id( 789 ); + protoCustomDecodedSignalA->set_interface_id( "456" ); + protoCustomDecodedSignalA->set_custom_decoding_id( "custom-decoder-0" ); + + auto protoCustomDecodedSignalB = protoDM.add_custom_decoding_signals(); + protoCustomDecodedSignalB->set_signal_id( 111 ); + protoCustomDecodedSignalB->set_interface_id( "456" ); + protoCustomDecodedSignalB->set_custom_decoding_id( "custom-decoder-1" ); + ASSERT_NO_FATAL_FAILURE( sendMessageToReceiver( mReceiverDecoderManifest, protoDM ) ); // This should be false because we just copied the data and it needs to be built first @@ -368,6 +382,16 @@ TEST_F( SchemaTest, DecoderManifestIngestion ) ASSERT_EQ( mReceivedDecoderManifest->getNetworkProtocol( 50000 ), VehicleDataSourceProtocol::RAW_SOCKET ); ASSERT_EQ( mReceivedDecoderManifest->getNetworkProtocol( 123 ), VehicleDataSourceProtocol::OBD ); ASSERT_EQ( mReceivedDecoderManifest->getNetworkProtocol( 567 ), VehicleDataSourceProtocol::OBD ); + + auto customSignalDecoderFormat = mReceivedDecoderManifest->getCustomSignalDecoderFormat( 9999999 ); + ASSERT_EQ( customSignalDecoderFormat.mInterfaceId, INVALID_INTERFACE_ID ); + ASSERT_EQ( customSignalDecoderFormat.mDecoder, INVALID_CUSTOM_SIGNAL_DECODER ); + customSignalDecoderFormat = mReceivedDecoderManifest->getCustomSignalDecoderFormat( 789 ); + ASSERT_EQ( customSignalDecoderFormat.mInterfaceId, "456" ); + ASSERT_EQ( customSignalDecoderFormat.mDecoder, "custom-decoder-0" ); + customSignalDecoderFormat = mReceivedDecoderManifest->getCustomSignalDecoderFormat( 111 ); + ASSERT_EQ( customSignalDecoderFormat.mInterfaceId, "456" ); + ASSERT_EQ( customSignalDecoderFormat.mDecoder, "custom-decoder-1" ); } /** @@ -392,6 +416,10 @@ TEST_F( SchemaTest, SchemaInvalidDecoderManifestTest ) ASSERT_FALSE( mReceivedDecoderManifest->build() ); ASSERT_FALSE( mReceivedDecoderManifest->isReady() ); + + auto customSignalDecoderFormat = mReceivedDecoderManifest->getCustomSignalDecoderFormat( 9999999 ); + ASSERT_EQ( customSignalDecoderFormat.mInterfaceId, INVALID_INTERFACE_ID ); + ASSERT_EQ( customSignalDecoderFormat.mDecoder, INVALID_CUSTOM_SIGNAL_DECODER ); } TEST_F( SchemaTest, CollectionSchemeIngestionList ) @@ -600,6 +628,9 @@ TEST_F( SchemaTest, CollectionSchemeIngestionHeartBeat ) TEST_F( SchemaTest, SchemaCollectionEventBased ) { + const std::string DUMMY_CUSTOM_FUNCTION_NAME = "Dummy_Custom_Function"; + const std::string NOT_DUMMY_CUSTOM_FUNCTION_NAME = "Not_" + DUMMY_CUSTOM_FUNCTION_NAME; + Schemas::CollectionSchemesMsg::CollectionSchemes protoCollectionSchemesMsg; auto collectionSchemeTestMessage = protoCollectionSchemesMsg.add_collection_schemes(); collectionSchemeTestMessage->set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); @@ -665,7 +696,7 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) rightOp->set_allocated_right_child( right_right ); auto *right_rightOp = new Schemas::CommonTypesMsg::ConditionNode_NodeOperator(); right_right->set_allocated_node_operator( right_rightOp ); - right_rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_SMALLER ); + right_rightOp->set_operator_( Schemas::CommonTypesMsg::ConditionNode_NodeOperator_Operator_COMPARE_BIGGER_EQUAL ); //---------- @@ -742,7 +773,7 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) auto *right_left_right_right = new Schemas::CommonTypesMsg::ConditionNode(); right_left_rightOp->set_allocated_right_child( right_left_right_right ); - right_left_right_right->set_node_double_value( 1 ); + right_left_right_right->set_node_string_value( "1" ); auto *right_right_left_left = new Schemas::CommonTypesMsg::ConditionNode(); right_right_leftOp->set_allocated_left_child( right_right_left_left ); @@ -754,11 +785,35 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) auto *right_right_right_left = new Schemas::CommonTypesMsg::ConditionNode(); right_right_rightOp->set_allocated_left_child( right_right_right_left ); - right_right_right_left->set_node_signal_id( SIGNAL_ID_1 ); + right_right_right_left->set_node_string_value( DUMMY_CUSTOM_FUNCTION_NAME + "_1" ); auto *right_right_right_right = new Schemas::CommonTypesMsg::ConditionNode(); right_right_rightOp->set_allocated_right_child( right_right_right_right ); - right_right_right_right->set_node_double_value( 1 ); + + auto *right_right_righ_right_nodeFunction = new Schemas::CommonTypesMsg::ConditionNode_NodeFunction(); + right_right_right_right->set_allocated_node_function( right_right_righ_right_nodeFunction ); + + auto *right_right_righ_right_customFunction = + new Schemas::CommonTypesMsg::ConditionNode_NodeFunction_CustomFunction(); + right_right_righ_right_nodeFunction->set_allocated_custom_function( right_right_righ_right_customFunction ); + right_right_righ_right_customFunction->set_function_name( DUMMY_CUSTOM_FUNCTION_NAME ); + + Schemas::CommonTypesMsg::ConditionNode *right_right_righ_right_customFunctionParam = + right_right_righ_right_customFunction->add_params(); + + auto *right_right_righ_right_customFunctionParam_nodeFunction = + new Schemas::CommonTypesMsg::ConditionNode_NodeFunction(); + right_right_righ_right_customFunctionParam->set_allocated_node_function( + right_right_righ_right_customFunctionParam_nodeFunction ); + + auto *right_right_righ_right_customFunctionParam_isNullFunction = + new Schemas::CommonTypesMsg::ConditionNode_NodeFunction_IsNullFunction(); + right_right_righ_right_customFunctionParam_nodeFunction->set_allocated_is_null_function( + right_right_righ_right_customFunctionParam_isNullFunction ); + + auto *right_right_righ_right_left = new Schemas::CommonTypesMsg::ConditionNode(); + right_right_righ_right_customFunctionParam_isNullFunction->set_allocated_expression( right_right_righ_right_left ); + right_right_righ_right_left->set_node_signal_id( SIGNAL_ID_1 ); //---------- @@ -768,6 +823,82 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) collectionSchemeTestMessage->set_compress_collected_data( true ); collectionSchemeTestMessage->set_priority( 5 ); + // add FetchInformation 1 + Schemas::CollectionSchemesMsg::FetchInformation *fetchInformation1 = + collectionSchemeTestMessage->add_signal_fetch_information(); + fetchInformation1->set_signal_id( SIGNAL_ID_2 ); + fetchInformation1->set_condition_language_version( 0 ); + + auto *timeBasedFetchConfig1 = new Schemas::CollectionSchemesMsg::TimeBasedFetchConfig(); + fetchInformation1->set_allocated_time_based( timeBasedFetchConfig1 ); + timeBasedFetchConfig1->set_max_execution_count( 111 ); + timeBasedFetchConfig1->set_execution_frequency_ms( 222 ); + timeBasedFetchConfig1->set_reset_max_execution_count_interval_ms( 333 ); + + Schemas::CommonTypesMsg::ConditionNode *fetchInformation1Action1 = fetchInformation1->add_actions(); + + auto *fetchInformation1Action1NodeFunction = new Schemas::CommonTypesMsg::ConditionNode_NodeFunction(); + fetchInformation1Action1->set_allocated_node_function( fetchInformation1Action1NodeFunction ); + + auto *fetchInformation1Action1IsNullFunction = + new Schemas::CommonTypesMsg::ConditionNode_NodeFunction_IsNullFunction(); + fetchInformation1Action1NodeFunction->set_allocated_is_null_function( fetchInformation1Action1IsNullFunction ); + + auto *fetchInformation1Action1IsNullFunctionExpression = new Schemas::CommonTypesMsg::ConditionNode(); + fetchInformation1Action1IsNullFunction->set_allocated_expression( + fetchInformation1Action1IsNullFunctionExpression ); + fetchInformation1Action1IsNullFunctionExpression->set_node_signal_id( SIGNAL_ID_3 ); + + // add FetchInformation 2 + Schemas::CollectionSchemesMsg::FetchInformation *fetchInformation2 = + collectionSchemeTestMessage->add_signal_fetch_information(); + fetchInformation2->set_signal_id( SIGNAL_ID_3 ); + fetchInformation2->set_condition_language_version( 0 ); + + auto *conditionBasedFetchConfig2 = new Schemas::CollectionSchemesMsg::ConditionBasedFetchConfig(); + fetchInformation2->set_allocated_condition_based( conditionBasedFetchConfig2 ); + conditionBasedFetchConfig2->set_condition_trigger_mode( + Schemas::CollectionSchemesMsg::ConditionBasedFetchConfig_ConditionTriggerMode_TRIGGER_ALWAYS ); + + auto *conditionBasedFetchConfig2Condition = new Schemas::CommonTypesMsg::ConditionNode(); + conditionBasedFetchConfig2->set_allocated_condition_tree( conditionBasedFetchConfig2Condition ); + conditionBasedFetchConfig2Condition->set_node_boolean_value( true ); + + Schemas::CommonTypesMsg::ConditionNode *fetchInformation2Action1 = fetchInformation2->add_actions(); + + auto *fetchInformation2Action1NodeFunction = new Schemas::CommonTypesMsg::ConditionNode_NodeFunction(); + fetchInformation2Action1->set_allocated_node_function( fetchInformation2Action1NodeFunction ); + + auto *fetchInformation2Action1CustomFunction = + new Schemas::CommonTypesMsg::ConditionNode_NodeFunction_CustomFunction(); + fetchInformation2Action1NodeFunction->set_allocated_custom_function( fetchInformation2Action1CustomFunction ); + fetchInformation2Action1CustomFunction->set_function_name( DUMMY_CUSTOM_FUNCTION_NAME ); + + Schemas::CommonTypesMsg::ConditionNode *fetchInformation2Action2 = fetchInformation2->add_actions(); + + auto *fetchInformation2Action2NodeFunction = new Schemas::CommonTypesMsg::ConditionNode_NodeFunction(); + fetchInformation2Action2->set_allocated_node_function( fetchInformation2Action2NodeFunction ); + + auto *fetchInformation2Action2CustomFunction = + new Schemas::CommonTypesMsg::ConditionNode_NodeFunction_CustomFunction(); + fetchInformation2Action2NodeFunction->set_allocated_custom_function( fetchInformation2Action2CustomFunction ); + fetchInformation2Action2CustomFunction->set_function_name( NOT_DUMMY_CUSTOM_FUNCTION_NAME ); + + // add FetchInformation 3 + Schemas::CollectionSchemesMsg::FetchInformation *fetchInformation3 = + collectionSchemeTestMessage->add_signal_fetch_information(); + fetchInformation3->set_signal_id( SIGNAL_ID_1 ); + fetchInformation3->set_condition_language_version( 0 ); + + auto *conditionBasedFetchConfig3 = new Schemas::CollectionSchemesMsg::ConditionBasedFetchConfig(); + fetchInformation3->set_allocated_condition_based( conditionBasedFetchConfig3 ); + conditionBasedFetchConfig3->set_condition_trigger_mode( + Schemas::CollectionSchemesMsg::ConditionBasedFetchConfig_ConditionTriggerMode_TRIGGER_ONLY_ON_RISING_EDGE ); + + auto *conditionBasedFetchConfig3Condition = new Schemas::CommonTypesMsg::ConditionNode(); + conditionBasedFetchConfig3->set_allocated_condition_tree( conditionBasedFetchConfig3Condition ); + conditionBasedFetchConfig3Condition->set_node_boolean_value( false ); + // Add 3 Signals Schemas::CollectionSchemesMsg::SignalInformation *signal1 = collectionSchemeTestMessage->add_signal_information(); signal1->set_signal_id( SIGNAL_ID_1 ); @@ -824,6 +955,56 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) ASSERT_TRUE( collectionSchemeTest->getAfterDurationMs() == 0 ); ASSERT_TRUE( collectionSchemeTest->isActiveDTCsIncluded() == true ); ASSERT_TRUE( collectionSchemeTest->isTriggerOnlyOnRisingEdge() == false ); + + // check FetchInformation + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().size() == 3 ); + + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).signalID == SIGNAL_ID_2 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).triggerOnlyOnRisingEdge == false ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).maxExecutionPerInterval == 111 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).executionPeriodMs == 222 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).executionIntervalMs == 333 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).condition == nullptr ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).actions.size() == 1 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).actions.at( 0 )->nodeType == + ExpressionNodeType::IS_NULL_FUNCTION ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 0 ).actions.at( 0 )->left->signalID == + SIGNAL_ID_3 ); + + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).signalID == SIGNAL_ID_3 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).triggerOnlyOnRisingEdge == false ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).maxExecutionPerInterval == 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).executionPeriodMs == 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).executionIntervalMs == 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).condition->nodeType == + ExpressionNodeType::BOOLEAN ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).condition->booleanValue == true ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.size() == 2 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.at( 0 )->nodeType == + ExpressionNodeType::CUSTOM_FUNCTION ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.at( 0 )->function.customFunctionName == + DUMMY_CUSTOM_FUNCTION_NAME ); + ASSERT_TRUE( + collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.at( 0 )->function.customFunctionParams.size() == + 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.at( 1 )->nodeType == + ExpressionNodeType::CUSTOM_FUNCTION ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.at( 1 )->function.customFunctionName == + NOT_DUMMY_CUSTOM_FUNCTION_NAME ); + ASSERT_TRUE( + collectionSchemeTest->getAllFetchInformations().at( 1 ).actions.at( 1 )->function.customFunctionParams.size() == + 0 ); + + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).signalID == SIGNAL_ID_1 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).triggerOnlyOnRisingEdge == true ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).maxExecutionPerInterval == 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).executionPeriodMs == 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).executionIntervalMs == 0 ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).condition->nodeType == + ExpressionNodeType::BOOLEAN ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).condition->booleanValue == false ); + ASSERT_TRUE( collectionSchemeTest->getAllFetchInformations().at( 2 ).actions.size() == 0 ); + // Signals ASSERT_TRUE( collectionSchemeTest->getCollectSignals().size() == 3 ); ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).signalID == SIGNAL_ID_1 ); @@ -858,7 +1039,7 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) ASSERT_TRUE( collectionSchemeTest->getMinimumPublishIntervalMs() == 650 ); // Verify the AST - ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().size(), 26 ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().size(), 28 ); //---------- ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).nodeType, ExpressionNodeType::OPERATOR_LOGICAL_AND ); @@ -875,7 +1056,7 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->left->nodeType, ExpressionNodeType::OPERATOR_SMALLER_EQUAL ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->nodeType, - ExpressionNodeType::OPERATOR_SMALLER ); + ExpressionNodeType::OPERATOR_BIGGER_EQUAL ); //---------- ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).left->left->left->nodeType, ExpressionNodeType::SIGNAL ); @@ -916,8 +1097,8 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) ExpressionNodeType::SIGNAL ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->left->right->left->signalID, SIGNAL_ID_1 ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->left->right->right->nodeType, - ExpressionNodeType::FLOAT ); - ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->left->right->right->floatingValue, 1 ); + ExpressionNodeType::STRING ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->left->right->right->stringValue, "1" ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->left->left->nodeType, ExpressionNodeType::SIGNAL ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->left->left->signalID, SIGNAL_ID_1 ); @@ -925,11 +1106,33 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) ExpressionNodeType::FLOAT ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->left->right->floatingValue, 1 ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->right->left->nodeType, - ExpressionNodeType::SIGNAL ); - ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->right->left->signalID, SIGNAL_ID_1 ); + ExpressionNodeType::STRING ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->right->left->stringValue, + DUMMY_CUSTOM_FUNCTION_NAME + "_1" ); ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->right->right->nodeType, - ExpressionNodeType::FLOAT ); - ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->right->right->floatingValue, 1 ); + ExpressionNodeType::CUSTOM_FUNCTION ); + ASSERT_EQ( + collectionSchemeTest->getAllExpressionNodes().at( 0 ).right->right->right->right->function.customFunctionName, + DUMMY_CUSTOM_FUNCTION_NAME ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes() + .at( 0 ) + .right->right->right->right->function.customFunctionParams.size(), + 1 ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes() + .at( 0 ) + .right->right->right->right->function.customFunctionParams[0] + ->nodeType, + ExpressionNodeType::IS_NULL_FUNCTION ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes() + .at( 0 ) + .right->right->right->right->function.customFunctionParams[0] + ->left->nodeType, + ExpressionNodeType::SIGNAL ); + ASSERT_EQ( collectionSchemeTest->getAllExpressionNodes() + .at( 0 ) + .right->right->right->right->function.customFunctionParams[0] + ->left->signalID, + SIGNAL_ID_1 ); //---------- ASSERT_TRUE( collectionSchemeTest->getCondition()->booleanValue == false ); @@ -943,6 +1146,149 @@ TEST_F( SchemaTest, SchemaCollectionEventBased ) #endif } +#ifdef FWE_FEATURE_STORE_AND_FORWARD +TEST_F( SchemaTest, StoreAndForwardConfiguration ) +{ + Schemas::CollectionSchemesMsg::CollectionSchemes protoCollectionSchemesMsg; + auto *collectionSchemeTestMessage = protoCollectionSchemesMsg.add_collection_schemes(); + collectionSchemeTestMessage->set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + collectionSchemeTestMessage->set_decoder_manifest_sync_id( "model_manifest_13" ); + collectionSchemeTestMessage->set_start_time_ms_epoch( 162144816000 ); + collectionSchemeTestMessage->set_expiry_time_ms_epoch( 262144816000 ); + + // Create an Event/Condition Based CollectionScheme + Schemas::CollectionSchemesMsg::ConditionBasedCollectionScheme *message = + collectionSchemeTestMessage->mutable_condition_based_collection_scheme(); + message->set_condition_minimum_interval_ms( 650 ); + message->set_condition_language_version( 20 ); + message->set_condition_trigger_mode( + Schemas::CollectionSchemesMsg::ConditionBasedCollectionScheme_ConditionTriggerMode_TRIGGER_ALWAYS ); + + // Create store and forward configuration + auto *store_and_forward_configuration = collectionSchemeTestMessage->mutable_store_and_forward_configuration(); + auto *store_and_forward_entry = store_and_forward_configuration->add_partition_configuration(); + auto *storage_options = store_and_forward_entry->mutable_storage_options(); + auto *upload_options = store_and_forward_entry->mutable_upload_options(); + storage_options->set_maximum_size_in_bytes( 1000000 ); + storage_options->set_storage_location( "/storage" ); + storage_options->set_minimum_time_to_live_in_seconds( 1000000 ); + + // Build the AST Tree: + //---------- + + upload_options->mutable_condition_tree()->set_node_signal_id( 10 ); + message->mutable_condition_tree(); + + //---------- + + collectionSchemeTestMessage->set_after_duration_ms( 0 ); + collectionSchemeTestMessage->set_include_active_dtcs( true ); + collectionSchemeTestMessage->set_persist_all_collected_data( true ); + collectionSchemeTestMessage->set_compress_collected_data( true ); + collectionSchemeTestMessage->set_priority( 5 ); + + // Add 3 Signals + Schemas::CollectionSchemesMsg::SignalInformation *signal1 = collectionSchemeTestMessage->add_signal_information(); + signal1->set_signal_id( 19 ); + signal1->set_sample_buffer_size( 5 ); + signal1->set_minimum_sample_period_ms( 500 ); + signal1->set_fixed_window_period_ms( 600 ); + signal1->set_condition_only_signal( true ); + signal1->set_data_partition_id( 1 ); + + Schemas::CollectionSchemesMsg::SignalInformation *signal2 = collectionSchemeTestMessage->add_signal_information(); + signal2->set_signal_id( 17 ); + signal2->set_sample_buffer_size( 10000 ); + signal2->set_minimum_sample_period_ms( 1000 ); + signal2->set_fixed_window_period_ms( 1000 ); + signal2->set_condition_only_signal( false ); + signal2->set_data_partition_id( 1 ); + + Schemas::CollectionSchemesMsg::SignalInformation *signal3 = collectionSchemeTestMessage->add_signal_information(); + signal3->set_signal_id( 3 ); + signal3->set_sample_buffer_size( 1000 ); + signal3->set_minimum_sample_period_ms( 100 ); + signal3->set_fixed_window_period_ms( 100 ); + signal3->set_condition_only_signal( true ); + signal3->set_data_partition_id( 1 ); + + // Add 1 RAW CAN Messages + Schemas::CollectionSchemesMsg::RawCanFrame *can1 = collectionSchemeTestMessage->add_raw_can_frames_to_collect(); + can1->set_can_interface_id( "1230" ); + can1->set_can_message_id( 0x1FF ); + can1->set_sample_buffer_size( 200 ); + can1->set_minimum_sample_period_ms( 255 ); + + ASSERT_NO_FATAL_FAILURE( sendMessageToReceiver( mReceiverCollectionSchemeList, protoCollectionSchemesMsg ) ); + + ASSERT_TRUE( mReceivedCollectionSchemeList->build() ); + ASSERT_EQ( mReceivedCollectionSchemeList->getCollectionSchemes().size(), 1 ); + auto collectionSchemeTest = mReceivedCollectionSchemeList->getCollectionSchemes().at( 0 ); + + // isReady should now evaluate to True + ASSERT_TRUE( collectionSchemeTest->isReady() == true ); + + // Confirm that the fields now match the set values in the proto message + ASSERT_FALSE( collectionSchemeTest->getCollectionSchemeID().compare( + "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ) ); + ASSERT_FALSE( collectionSchemeTest->getDecoderManifestID().compare( "model_manifest_13" ) ); + ASSERT_TRUE( collectionSchemeTest->getStartTime() == 162144816000 ); + ASSERT_TRUE( collectionSchemeTest->getExpiryTime() == 262144816000 ); + ASSERT_TRUE( collectionSchemeTest->getAfterDurationMs() == 0 ); + ASSERT_TRUE( collectionSchemeTest->isActiveDTCsIncluded() == true ); + ASSERT_TRUE( collectionSchemeTest->isTriggerOnlyOnRisingEdge() == false ); + + // Signals + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().size() == 3 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).signalID == 19 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).sampleBufferSize == 5 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).minimumSampleIntervalMs == 500 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).fixedWindowPeriod == 600 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).isConditionOnlySignal == true ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 0 ).dataPartitionId == 1 ); + + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 1 ).signalID == 17 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 1 ).sampleBufferSize == 10000 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 1 ).minimumSampleIntervalMs == 1000 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 1 ).fixedWindowPeriod == 1000 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 1 ).isConditionOnlySignal == false ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 1 ).dataPartitionId == 1 ); + + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 2 ).signalID == 3 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 2 ).sampleBufferSize == 1000 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 2 ).minimumSampleIntervalMs == 100 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 2 ).fixedWindowPeriod == 100 ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 2 ).isConditionOnlySignal == true ); + ASSERT_TRUE( collectionSchemeTest->getCollectSignals().at( 2 ).dataPartitionId == 1 ); + + ASSERT_TRUE( collectionSchemeTest->getCollectRawCanFrames().size() == 1 ); + ASSERT_TRUE( collectionSchemeTest->getCollectRawCanFrames().at( 0 ).minimumSampleIntervalMs == 255 ); + ASSERT_TRUE( collectionSchemeTest->getCollectRawCanFrames().at( 0 ).sampleBufferSize == 200 ); + ASSERT_TRUE( collectionSchemeTest->getCollectRawCanFrames().at( 0 ).frameID == 0x1FF ); + ASSERT_TRUE( collectionSchemeTest->getCollectRawCanFrames().at( 0 ).interfaceID == "1230" ); + + ASSERT_TRUE( collectionSchemeTest->isPersistNeeded() == true ); + ASSERT_TRUE( collectionSchemeTest->isCompressionNeeded() == true ); + ASSERT_TRUE( collectionSchemeTest->getPriority() == 5 ); + + // For Event based collectionScheme the getMinimumPublishIntervalMs is the same as condition_minimum_interval_ms + ASSERT_TRUE( collectionSchemeTest->getMinimumPublishIntervalMs() == 650 ); + + // StoreAndForward + ASSERT_EQ( collectionSchemeTest->getStoreAndForwardConfiguration().at( 0 ).uploadOptions.conditionTree->nodeType, + ExpressionNodeType::SIGNAL ); + ASSERT_EQ( collectionSchemeTest->getStoreAndForwardConfiguration().at( 0 ).uploadOptions.conditionTree->signalID, + 10 ); + ASSERT_TRUE( collectionSchemeTest->getStoreAndForwardConfiguration().at( 0 ).storageOptions.maximumSizeInBytes == + 1000000 ); + ASSERT_TRUE( collectionSchemeTest->getStoreAndForwardConfiguration().at( 0 ).storageOptions.storageLocation == + "/storage" ); + ASSERT_TRUE( + collectionSchemeTest->getStoreAndForwardConfiguration().at( 0 ).storageOptions.minimumTimeToLiveInSeconds == + 1000000 ); +} +#endif + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA TEST_F( SchemaTest, SchemaCollectionWithComplexTypes ) { diff --git a/test/unit/SomeipCommandDispatcherTest.cpp b/test/unit/SomeipCommandDispatcherTest.cpp new file mode 100644 index 00000000..cbffbfcc --- /dev/null +++ b/test/unit/SomeipCommandDispatcherTest.cpp @@ -0,0 +1,447 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "SomeipCommandDispatcher.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CommonAPIProxyMock.h" +#include "ExampleSomeipInterfaceProxyMock.h" +#include "ExampleSomeipInterfaceWrapper.h" +#include "ICommandDispatcher.h" +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::An; +using ::testing::ElementsAre; +using ::testing::Invoke; +using ::testing::Matcher; +using ::testing::MockFunction; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrictMock; + +class SomeipCommandDispatcherTest : public ::testing::Test +{ +protected: + SomeipCommandDispatcherTest() + : mCommonAPIProxy( std::make_shared>() ) + , mProxy( std::make_shared>>( mCommonAPIProxy ) ) + , mRawBufferManagerSpy( std::make_shared>( + RawData::BufferManagerConfig::create().get() ) ) + , mLRCEvent( std::make_shared>>() ) + , mProxyStatusEventMock( std::make_shared>>() ) + , mExampleSomeipInterfaceWrapper( std::make_shared( + "", + "", + "", + [this]( std::string, std::string, std::string ) { + return mProxy; + }, + mRawBufferManagerSpy, + true ) ) + , mCommandDispatcher( std::make_shared( mExampleSomeipInterfaceWrapper ) ) + { + } + + void + SetUp() override + { + ON_CALL( *mProxy, getNotifyLRCStatusEvent() ).WillByDefault( ReturnRef( *mLRCEvent ) ); + EXPECT_CALL( *mProxy, getNotifyLRCStatusEvent() ).Times( testing::AnyNumber() ); + ON_CALL( *mProxy, getProxyStatusEvent() ).WillByDefault( ReturnRef( *mProxyStatusEventMock ) ); + EXPECT_CALL( *mProxy, getProxyStatusEvent() ).Times( testing::AnyNumber() ); + ON_CALL( *mProxyStatusEventMock, onFirstListenerAdded( _ ) ) + .WillByDefault( Invoke( [this]( std::function listener ) { + listener( CommonAPI::AvailabilityStatus::AVAILABLE ); + } ) ); + } + + std::shared_ptr> mCommonAPIProxy; + std::shared_ptr>> mProxy; + std::shared_ptr> mRawBufferManagerSpy; + std::shared_ptr>> mLRCEvent; + std::shared_ptr>> mProxyStatusEventMock; + std::shared_ptr mExampleSomeipInterfaceWrapper; + std::shared_ptr mCommandDispatcher; +}; + +TEST_F( SomeipCommandDispatcherTest, getActuatorNames ) +{ + EXPECT_CALL( *mProxyStatusEventMock, onFirstListenerAdded( _ ) ).Times( testing::AnyNumber() ); + + ASSERT_TRUE( mCommandDispatcher->init() ); + auto names = mCommandDispatcher->getActuatorNames(); + ASSERT_EQ( names.size(), mExampleSomeipInterfaceWrapper->getSupportedActuatorInfo().size() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInitSuccessful ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInitUnsuccessful ) +{ + // Mock the BuildProxy function returns nullptr + auto badExampleSomeipInterfaceWrapper = std::make_shared( + "", + "", + "", + [this]( std::string, std::string, std::string ) { + return nullptr; + }, + nullptr, + false ); + auto commandDispatcher = std::make_shared( badExampleSomeipInterfaceWrapper ); + ASSERT_FALSE( commandDispatcher->init() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherWithoutInitShallFail ) +{ + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_INTERNAL_ERROR, "Null proxy" ) ) + .Times( 1 ); + mCommandDispatcher->setActuatorValue( + "Vehicle.actuator1", SignalValueWrapper(), "CmdId123", 0, 0, resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulInt32 ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setInt32Async( 1, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::INT32 ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator1", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulInt64 ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setInt64Async( 1, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::INT64 ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator2", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulBoolean ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setBooleanAsync( true, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( true, SignalType::BOOLEAN ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator3", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulFloat ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setFloatAsync( 1.0, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( 1.0, SignalType::FLOAT ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator4", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulDouble ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setDoubleAsync( 1.0, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( 1.0, SignalType::DOUBLE ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator5", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulInt32LRC ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setInt32LongRunningAsync( _, 1, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const std::string &_commandId, + const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _commandId ); + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::INT32 ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator20", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandSuccessfulString ) +{ + mRawBufferManagerSpy->updateConfig( { { 1, { 1, "", "" } } } ); + std::string stringVal = "dog"; + auto handle = + mRawBufferManagerSpy->push( reinterpret_cast( stringVal.data() ), stringVal.size(), 1234, 1 ); + mRawBufferManagerSpy->increaseHandleUsageHint( 1, handle, RawData::BufferHandleUsageStage::UPLOADING ); + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setStringAsync( "dog", _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const std::string &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetStringAsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::SUCCESS ); + std::promise result; + result.set_value( CommonAPI::CallStatus::SUCCESS ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.value.rawDataVal.handle = handle; + signal.value.rawDataVal.signalId = 1; + signal.type = SignalType::STRING; + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::SUCCEEDED, REASON_CODE_OEM_RANGE_START, "SUCCESS" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator9", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandStringBadSignalId ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + SignalValueWrapper signal; + signal.value.rawDataVal.handle = 2; + signal.value.rawDataVal.signalId = 1; + signal.type = SignalType::STRING; + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_REJECTED, "" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( "Vehicle.actuator9", + signal, + "CmdId123", + ClockHandler::getClock()->systemTimeSinceEpochMs(), + 0, + resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandWithMismatchedValueType ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setInt32Async( _, _, _ ) ).Times( 0 ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::DOUBLE ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_ARGUMENT_TYPE_MISMATCH, "" ) ) + .Times( 1 ); + mCommandDispatcher->setActuatorValue( + "Vehicle.actuator1", signal, "CmdId123", 0, 0, resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeCommandFailed ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setInt32Async( 1, _, _ ) ) + .Times( 1 ) + .WillOnce( Invoke( []( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxy<>::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) -> std::future { + static_cast( _value ); + static_cast( _info ); + _callback( CommonAPI::CallStatus::CONNECTION_FAILED ); + std::promise result; + result.set_value( CommonAPI::CallStatus::CONNECTION_FAILED ); + return result.get_future(); + } ) ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::INT32 ); + MockFunction + resultCallback; + EXPECT_CALL( + resultCallback, + Call( CommandStatus::EXECUTION_FAILED, + REASON_CODE_OEM_RANGE_START + static_cast( CommonAPI::CallStatus::CONNECTION_FAILED ), + "CONNECTION_FAILED" ) ) + .Times( 1 ); + mCommandDispatcher->setActuatorValue( + "Vehicle.actuator1", signal, "CmdId123", 0, 0, resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherInvokeNotSupportedCommand ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mProxy, setInt32Async( _, _, _ ) ).Times( 0 ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::INT32 ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_NOT_SUPPORTED, "" ) ).Times( 1 ); + mCommandDispatcher->setActuatorValue( + "Vehicle.NotSupportedActuator", signal, "CmdId123", 0, 0, resultCallback.AsStdFunction() ); +} + +TEST_F( SomeipCommandDispatcherTest, dispatcherFailedWithProxyUnavailable ) +{ + ASSERT_TRUE( mCommandDispatcher->init() ); + EXPECT_CALL( *mProxy, isAvailable() ).Times( 1 ).WillOnce( Return( false ) ); + EXPECT_CALL( *mProxy, setInt32Async( _, _, _ ) ).Times( 0 ); + SignalValueWrapper signal; + signal.setVal( 1, SignalType::INT32 ); + MockFunction + resultCallback; + EXPECT_CALL( resultCallback, Call( CommandStatus::EXECUTION_FAILED, REASON_CODE_UNAVAILABLE, "Proxy unavailable" ) ) + .Times( 1 ); + mCommandDispatcher->setActuatorValue( + "Vehicle.actuator1", signal, "CmdId123", 0, 0, resultCallback.AsStdFunction() ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/SomeipDataSourceTest.cpp b/test/unit/SomeipDataSourceTest.cpp new file mode 100644 index 00000000..c8b797e2 --- /dev/null +++ b/test/unit/SomeipDataSourceTest.cpp @@ -0,0 +1,226 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "SomeipDataSource.h" +#include "CollectionInspectionAPITypes.h" +#include "CommonAPIProxyMock.h" +#include "ExampleSomeipInterfaceProxyMock.h" +#include "ExampleSomeipInterfaceWrapper.h" +#include "IDecoderDictionary.h" +#include "IDecoderManifest.h" +#include "NamedSignalDataSource.h" +#include "QueueTypes.h" +#include "RawDataBufferManagerSpy.h" +#include "RawDataManager.h" +#include "SignalTypes.h" +#include "VehicleDataSourceTypes.h" +#include "WaitUntil.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::An; +using ::testing::AnyNumber; +using ::testing::ElementsAre; +using ::testing::Invoke; +using ::testing::Matcher; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::ReturnRef; +using ::testing::SaveArg; +using ::testing::StrictMock; + +class SomeipDataSourceTest : public ::testing::Test +{ +protected: + SomeipDataSourceTest() + : mCommonAPIProxy( std::make_shared>() ) + , mXAttributeMock( std::make_shared>>() ) + , mA1AttributeMock( + std::make_shared>>() ) + , mXAttributeChangedEventMock( + std::make_shared>>() ) + , mA1AttributeChangedEventMock( + std::make_shared< + StrictMock>>() ) + , mProxy( std::make_shared>>( mCommonAPIProxy ) ) + , mRawBufferManagerSpy( std::make_shared>( + RawData::BufferManagerConfig::create().get() ) ) + , mExampleSomeipInterfaceWrapper( std::make_shared( + "", + "", + "", + [this]( std::string, std::string, std::string ) { + return mProxy; + }, + mRawBufferManagerSpy, + false ) ) + , mSignalBuffer( std::make_shared( 2, "Signal Buffer" ) ) + , mSignalBufferDistributor( std::make_shared() ) + , mNamedSignalDataSource( std::make_shared( "5", mSignalBufferDistributor ) ) + , mSomeipDataSource( std::make_shared( + mExampleSomeipInterfaceWrapper, mNamedSignalDataSource, mRawBufferManagerSpy, 1000 ) ) + , mDictionary( std::make_shared() ) + { + mSignalBufferDistributor->registerQueue( mSignalBuffer ); + } + + void + SetUp() override + { + ON_CALL( *mProxy, isAvailable() ).WillByDefault( Return( false ) ); + ON_CALL( *mProxy, getXAttribute() ).WillByDefault( ReturnRef( *mXAttributeMock ) ); + ON_CALL( *mXAttributeMock, getChangedEvent() ).WillByDefault( ReturnRef( *mXAttributeChangedEventMock ) ); + ON_CALL( *mXAttributeChangedEventMock, onFirstListenerAdded( _ ) ) + .WillByDefault( Invoke( [this]( std::function listener ) { + mXAttributeListener = listener; + } ) ); + ON_CALL( *mProxy, getA1Attribute() ).WillByDefault( ReturnRef( *mA1AttributeMock ) ); + ON_CALL( *mA1AttributeMock, getChangedEvent() ).WillByDefault( ReturnRef( *mA1AttributeChangedEventMock ) ); + ON_CALL( *mA1AttributeChangedEventMock, onFirstListenerAdded( _ ) ) + .WillByDefault( + Invoke( [this]( std::function listener ) { + mA1AttributeListener = listener; + } ) ); + + mDictionary->customDecoderMethod["5"]["Vehicle.ExampleSomeipInterface.X"] = + CustomSignalDecoderFormat{ "5", "Vehicle.ExampleSomeipInterface.X", 1, SignalType::DOUBLE }; + mDictionary->customDecoderMethod["5"]["Vehicle.ExampleSomeipInterface.A1.A2.A"] = + CustomSignalDecoderFormat{ "5", "Vehicle.ExampleSomeipInterface.A1.A2.A", 2, SignalType::DOUBLE }; + mDictionary->customDecoderMethod["5"]["Vehicle.ExampleSomeipInterface.A1.A2.B"] = + CustomSignalDecoderFormat{ "5", "Vehicle.ExampleSomeipInterface.A1.A2.B", 3, SignalType::DOUBLE }; + mDictionary->customDecoderMethod["5"]["Vehicle.ExampleSomeipInterface.A1.A2.D"] = + CustomSignalDecoderFormat{ "5", "Vehicle.ExampleSomeipInterface.A1.A2.D", 4, SignalType::DOUBLE }; + } + + std::shared_ptr> mCommonAPIProxy; + std::shared_ptr>> mXAttributeMock; + std::shared_ptr>> + mA1AttributeMock; + std::shared_ptr>> mXAttributeChangedEventMock; + std::shared_ptr>> + mA1AttributeChangedEventMock; + std::function mXAttributeListener; + std::function mA1AttributeListener; + std::shared_ptr>> mProxy; + std::shared_ptr> mRawBufferManagerSpy; + std::shared_ptr mExampleSomeipInterfaceWrapper; + std::shared_ptr mSignalBuffer; + std::shared_ptr mSignalBufferDistributor; + std::shared_ptr mNamedSignalDataSource; + std::shared_ptr mSomeipDataSource; + std::shared_ptr mDictionary; +}; + +TEST_F( SomeipDataSourceTest, initUnsuccessful ) +{ + auto badExampleSomeipInterfaceWrapper = std::make_shared( + "", + "", + "", + [this]( std::string, std::string, std::string ) { + return nullptr; + }, + nullptr, + false ); + auto dataSource = std::make_shared( + badExampleSomeipInterfaceWrapper, mNamedSignalDataSource, mRawBufferManagerSpy, 0 ); + ASSERT_FALSE( dataSource->init() ); +} + +TEST_F( SomeipDataSourceTest, initSuccessfulIngestValues ) +{ + // Use a mutex to prevent interruption of the block below. The isAvailable mock will be + // periodically called by another thread, which will push more values to the signal buffer once + // it returns as true. We do not want this in this case as we want to check that a single call + // of the callback will push a single value to the buffer. + std::mutex mutex; + bool isAvailable = false; + EXPECT_CALL( *mProxy, isAvailable() ).Times( AnyNumber() ).WillRepeatedly( Invoke( [&]() { + std::lock_guard lock( mutex ); + return isAvailable; + } ) ); + EXPECT_CALL( *mProxy, getXAttribute() ); + EXPECT_CALL( *mXAttributeMock, getChangedEvent() ); + EXPECT_CALL( *mXAttributeChangedEventMock, onFirstListenerAdded( _ ) ); + EXPECT_CALL( *mProxy, getA1Attribute() ); + EXPECT_CALL( *mA1AttributeMock, getChangedEvent() ); + EXPECT_CALL( *mA1AttributeChangedEventMock, onFirstListenerAdded( _ ) ); + ASSERT_TRUE( mSomeipDataSource->init() ); + ASSERT_TRUE( mXAttributeListener ); + ASSERT_TRUE( mA1AttributeListener ); + + CollectedDataFrame collectedDataFrame; + ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + + // Other protocol: + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + + // Custom protocol: + mNamedSignalDataSource->onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::CUSTOM_DECODING ); + DELAY_ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + + { + std::lock_guard lock( mutex ); + isAvailable = true; + } + DELAY_ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + + { + // Lock the mutex and call each callback, expecting that one of each value is pushed to the + // buffer: + std::lock_guard lock( mutex ); + + mXAttributeListener( 123 ); + ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + ASSERT_EQ( collectedDataFrame.mActiveDTCs, nullptr ); + ASSERT_EQ( collectedDataFrame.mCollectedCanRawFrame, nullptr ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 1 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[0].signalID, 1 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[0].value.value.doubleVal, 123, 0.0001 ); + ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + + v1::commonapi::CommonTypes::a1Struct a1Struct; + a1Struct.setS( "ABC" ); + v1::commonapi::CommonTypes::a2Struct a2Struct; + a2Struct.setA( 456 ); + a2Struct.setB( true ); + a2Struct.setD( 789.012 ); + a1Struct.setA2( a2Struct ); + mA1AttributeListener( a1Struct ); + + ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + ASSERT_EQ( collectedDataFrame.mActiveDTCs, nullptr ); + ASSERT_EQ( collectedDataFrame.mCollectedCanRawFrame, nullptr ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals.size(), 3 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[0].signalID, 2 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[0].value.value.doubleVal, 456, 0.0001 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[1].signalID, 3 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[1].value.value.doubleVal, 1, 0.0001 ); + ASSERT_EQ( collectedDataFrame.mCollectedSignals[2].signalID, 4 ); + ASSERT_NEAR( collectedDataFrame.mCollectedSignals[2].value.value.doubleVal, 789.012, 0.0001 ); + + ASSERT_FALSE( mSignalBuffer->pop( collectedDataFrame ) ); + } + + // Now with the mutex unlocked, wait until the background thread pushes another value: + WAIT_ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + + mNamedSignalDataSource->onChangeOfActiveDictionary( nullptr, VehicleDataSourceProtocol::CUSTOM_DECODING ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/SomeipToCanBridgeTest.cpp b/test/unit/SomeipToCanBridgeTest.cpp new file mode 100644 index 00000000..efb6acf8 --- /dev/null +++ b/test/unit/SomeipToCanBridgeTest.cpp @@ -0,0 +1,313 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "SomeipToCanBridge.h" +#include "CANDataConsumer.h" +#include "CollectionInspectionAPITypes.h" +#include "IDecoderDictionary.h" +#include "MessageTypes.h" +#include "QueueTypes.h" +#include "SignalTypes.h" +#include "SomeipMock.h" +#include "TimeTypes.h" +#include "VehicleDataSourceTypes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SOMEIP_SERVICE_ID 0x1234 +#define SOMEIP_INSTANCE_ID 0x5678 +#define SOMEIP_EVENT_ID 0x9ABC +#define SOMEIP_EVENTGROUP_ID 0xDEF0 +#define SOMEIP_APPLICATION_NAME "someip_app" +#define CAN_ID 0x123 +#define CAN_RECEIVE_TIME 0x0123456789ABCDEF + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::An; +using ::testing::ElementsAre; +using ::testing::Invoke; +using ::testing::Matcher; +using ::testing::Return; +using ::testing::SaveArg; +using ::testing::StrictMock; + +class SomeipToCanBridgeTest : public ::testing::Test +{ +protected: + SomeipToCanBridgeTest() + : mSignalBuffer( std::make_shared( 3, "Signal Buffer" ) ) + , mSignalBufferDistributor( std::make_shared() ) + , mCanConsumer( mSignalBufferDistributor ) + , mSomeipApplicationMock( std::make_shared>() ) + , mSomeipToCanBridge( + SOMEIP_SERVICE_ID, + SOMEIP_INSTANCE_ID, + SOMEIP_EVENT_ID, + SOMEIP_EVENTGROUP_ID, + SOMEIP_APPLICATION_NAME, + 0, + mCanConsumer, + [this]( std::string ) { + return mSomeipApplicationMock; + }, + [this]( std::string ) {} ) + , mMessage( vsomeip::runtime::get()->create_message() ) + { + } + + void + SetUp() override + { + std::unordered_map frameMap; + CANMessageDecoderMethod decoderMethod; + decoderMethod.collectType = CANMessageCollectType::RAW_AND_DECODE; + + decoderMethod.format.mMessageID = CAN_ID; + decoderMethod.format.mSizeInBytes = 8; + + CANSignalFormat sigFormat1; + sigFormat1.mSignalID = 1; + sigFormat1.mIsBigEndian = true; + sigFormat1.mIsSigned = true; + sigFormat1.mFirstBitPosition = 24; + sigFormat1.mSizeInBits = 30; + sigFormat1.mOffset = 0.0; + sigFormat1.mFactor = 1.0; + sigFormat1.mSignalType = SignalType::DOUBLE; + + CANSignalFormat sigFormat2; + sigFormat2.mSignalID = 7; + sigFormat2.mIsBigEndian = true; + sigFormat2.mIsSigned = true; + sigFormat2.mFirstBitPosition = 56; + sigFormat2.mSizeInBits = 31; + sigFormat2.mOffset = 0.0; + sigFormat2.mFactor = 1.0; + sigFormat2.mSignalType = SignalType::DOUBLE; + + decoderMethod.format.mSignals.push_back( sigFormat1 ); + decoderMethod.format.mSignals.push_back( sigFormat2 ); + frameMap[CAN_ID] = decoderMethod; + mDictionary = std::make_shared(); + mDictionary->canMessageDecoderMethod[0] = frameMap; + mDictionary->signalIDsToCollect.emplace( 1 ); + mDictionary->signalIDsToCollect.emplace( 7 ); + + setupCanMessage( CAN_ID, CAN_RECEIVE_TIME, { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 } ); + + mSignalBufferDistributor->registerQueue( mSignalBuffer ); + } + + void + TearDown() override + { + mSomeipToCanBridge.disconnect(); + } + + void + setupInitExpectations() + { + EXPECT_CALL( *mSomeipApplicationMock, init() ).Times( 1 ).WillOnce( Return( true ) ); + EXPECT_CALL( *mSomeipApplicationMock, register_sd_acceptance_handler( _ ) ) + .Times( 1 ) + .WillOnce( SaveArg<0>( &mServiceDiscoveryAcceptanceHandler ) ); + EXPECT_CALL( *mSomeipApplicationMock, + register_availability_handler( + SOMEIP_SERVICE_ID, SOMEIP_INSTANCE_ID, An(), _, _ ) ) + .Times( 1 ) + .WillOnce( SaveArg<2>( &mAvailabilityHandler ) ); + EXPECT_CALL( *mSomeipApplicationMock, request_service( SOMEIP_SERVICE_ID, SOMEIP_INSTANCE_ID, _, _ ) ) + .Times( 1 ); + EXPECT_CALL( *mSomeipApplicationMock, + register_message_handler( SOMEIP_SERVICE_ID, SOMEIP_INSTANCE_ID, SOMEIP_EVENT_ID, _ ) ) + .Times( 1 ) + .WillOnce( SaveArg<3>( &mMessageHandler ) ); + EXPECT_CALL( + *mSomeipApplicationMock, + request_event( + SOMEIP_SERVICE_ID, SOMEIP_INSTANCE_ID, SOMEIP_EVENT_ID, ElementsAre( SOMEIP_EVENTGROUP_ID ), _, _ ) ) + .Times( 1 ); + EXPECT_CALL( *mSomeipApplicationMock, + subscribe( SOMEIP_SERVICE_ID, SOMEIP_INSTANCE_ID, SOMEIP_EVENTGROUP_ID, _, _ ) ) + .Times( 1 ); + EXPECT_CALL( *mSomeipApplicationMock, stop() ).Times( 1 ); + EXPECT_CALL( *mSomeipApplicationMock, start() ).Times( 1 ); + } + + void + setupCanMessage( uint32_t canId, Timestamp timestamp, std::vector payload ) + { + std::vector data = { static_cast( canId >> 24 ), + static_cast( canId >> 16 ), + static_cast( canId >> 8 ), + static_cast( canId ), + static_cast( timestamp >> 56 ), + static_cast( timestamp >> 48 ), + static_cast( timestamp >> 40 ), + static_cast( timestamp >> 32 ), + static_cast( timestamp >> 24 ), + static_cast( timestamp >> 16 ), + static_cast( timestamp >> 8 ), + static_cast( timestamp ) }; + data.insert( data.end(), payload.begin(), payload.end() ); + mMessage->get_payload()->set_data( std::move( data ) ); + } + + void + checkValidMessage( uint64_t expectedTimestamp ) + { + CollectedDataFrame collectedDataFrame; + ASSERT_TRUE( mSignalBuffer->pop( collectedDataFrame ) ); + auto signal = collectedDataFrame.mCollectedSignals[0]; + ASSERT_EQ( signal.value.type, SignalType::DOUBLE ); + ASSERT_EQ( signal.signalID, 1 ); + if ( expectedTimestamp == 0 ) + { + ASSERT_GT( signal.receiveTime, 0 ); + } + else + { + ASSERT_EQ( signal.receiveTime, expectedTimestamp ); + } + ASSERT_DOUBLE_EQ( signal.value.value.doubleVal, 0x10203 ); + signal = collectedDataFrame.mCollectedSignals[1]; + ASSERT_EQ( signal.signalID, 7 ); + ASSERT_EQ( signal.value.type, SignalType::DOUBLE ); + if ( expectedTimestamp == 0 ) + { + ASSERT_GT( signal.receiveTime, 0 ); + } + else + { + ASSERT_EQ( signal.receiveTime, expectedTimestamp ); + } + ASSERT_DOUBLE_EQ( signal.value.value.doubleVal, 0x4050607 ); + auto frame = collectedDataFrame.mCollectedCanRawFrame; + ASSERT_EQ( frame->channelId, 0 ); + ASSERT_EQ( frame->frameID, CAN_ID ); + if ( expectedTimestamp == 0 ) + { + ASSERT_GT( frame->receiveTime, 0 ); + } + else + { + ASSERT_EQ( frame->receiveTime, expectedTimestamp ); + } + ASSERT_EQ( frame->size, 8 ); + for ( auto i = 0; i < 8; i++ ) + { + ASSERT_EQ( frame->data[i], i ); + } + } + + SignalBufferPtr mSignalBuffer; + SignalBufferDistributorPtr mSignalBufferDistributor; + CANDataConsumer mCanConsumer{ mSignalBufferDistributor }; + std::shared_ptr> mSomeipApplicationMock; + SomeipToCanBridge mSomeipToCanBridge; + vsomeip::message_handler_t mMessageHandler; + vsomeip::sd_acceptance_handler_t mServiceDiscoveryAcceptanceHandler; + vsomeip::availability_handler_t mAvailabilityHandler; + std::shared_ptr mMessage; + std::shared_ptr mDictionary; +}; + +TEST_F( SomeipToCanBridgeTest, initFail ) +{ + EXPECT_CALL( *mSomeipApplicationMock, init() ).Times( 1 ).WillRepeatedly( Return( false ) ); + ASSERT_FALSE( mSomeipToCanBridge.init() ); +} + +TEST_F( SomeipToCanBridgeTest, initSuccess ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + vsomeip::remote_info_t remoteInfo{}; + ASSERT_TRUE( mServiceDiscoveryAcceptanceHandler( remoteInfo ) ); + mAvailabilityHandler( SOMEIP_SERVICE_ID, SOMEIP_INSTANCE_ID, true ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageTooShort ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mMessage->get_payload()->set_data( std::vector() ); + mMessageHandler( mMessage ); + ASSERT_TRUE( mSignalBuffer->isEmpty() ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageNoDm ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mMessageHandler( mMessage ); + ASSERT_TRUE( mSignalBuffer->isEmpty() ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageNullDm ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mSomeipToCanBridge.onChangeOfActiveDictionary( nullptr, VehicleDataSourceProtocol::RAW_SOCKET ); + mMessageHandler( mMessage ); + ASSERT_TRUE( mSignalBuffer->isEmpty() ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageOtherDm ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mSomeipToCanBridge.onChangeOfActiveDictionary( nullptr, VehicleDataSourceProtocol::INVALID_PROTOCOL ); + mMessageHandler( mMessage ); + ASSERT_TRUE( mSignalBuffer->isEmpty() ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageValidDm ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mSomeipToCanBridge.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + mMessageHandler( mMessage ); + ASSERT_NO_FATAL_FAILURE( checkValidMessage( CAN_RECEIVE_TIME / 1000 ) ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageNoTimestamp ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mSomeipToCanBridge.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + setupCanMessage( CAN_ID, 0, { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 } ); + mMessageHandler( mMessage ); + ASSERT_NO_FATAL_FAILURE( checkValidMessage( 0 ) ); +} + +TEST_F( SomeipToCanBridgeTest, receiveMessageNonMonotonic ) +{ + setupInitExpectations(); + ASSERT_TRUE( mSomeipToCanBridge.init() ); + mSomeipToCanBridge.onChangeOfActiveDictionary( mDictionary, VehicleDataSourceProtocol::RAW_SOCKET ); + mMessageHandler( mMessage ); + ASSERT_NO_FATAL_FAILURE( checkValidMessage( CAN_RECEIVE_TIME / 1000 ) ); + setupCanMessage( CAN_ID, CAN_RECEIVE_TIME - 1000, { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 } ); + mMessageHandler( mMessage ); + ASSERT_NO_FATAL_FAILURE( checkValidMessage( ( CAN_RECEIVE_TIME - 1000 ) / 1000 ) ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/StoreFileSystemTest.cpp b/test/unit/StoreFileSystemTest.cpp new file mode 100644 index 00000000..327da5c7 --- /dev/null +++ b/test/unit/StoreFileSystemTest.cpp @@ -0,0 +1,124 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StoreFileSystem.h" +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +class FileSystemTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + } + + void + TearDown() override + { + } +}; + +TEST_F( FileSystemTest, errnoToFileErrorTest ) +{ + auto res = errnoToFileError( EACCES, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::AccessDenied ); + EXPECT_EQ( res.msg, "test Access denied" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( EDQUOT, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::DiskFull ); + EXPECT_EQ( res.msg, "test User inode/disk block quota exhausted" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( EINVAL, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::InvalidArguments ); + EXPECT_EQ( res.msg, "test Unknown invalid arguments" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( EISDIR, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::InvalidArguments ); + EXPECT_EQ( res.msg, "test Path cannot be opened for writing because it is a directory" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( ELOOP, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::InvalidArguments ); + EXPECT_EQ( res.msg, "test Too many symbolic links" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( EMFILE, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::TooManyOpenFiles ); + EXPECT_EQ( res.msg, "test Too many open files. Consider raising limits." ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( ENFILE, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::TooManyOpenFiles ); + EXPECT_EQ( res.msg, "test Too many open files. Consider raising limits." ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( ENOENT, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::FileDoesNotExist ); + EXPECT_EQ( res.msg, "test Path does not exist" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( EFBIG, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::InvalidArguments ); + EXPECT_EQ( res.msg, "test File is too large" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( EIO, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::IOError ); + EXPECT_EQ( res.msg, "test Unknown IO error" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( ENOSPC, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::DiskFull ); + EXPECT_EQ( res.msg, "test Disk full" ); + EXPECT_FALSE( res.ok() ); + + res = errnoToFileError( -1, "test" ); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::Unknown ); + EXPECT_EQ( res.msg, "test Unknown error code: -1" ); + EXPECT_FALSE( res.ok() ); +} + +TEST_F( FileSystemTest, PosixFileLikeOpenMissing ) +{ + PosixFileLike missing( "dummy/missing.txt" ); + auto res = missing.open(); + EXPECT_EQ( res.code, aws::store::filesystem::FileErrorCode::FileDoesNotExist ); + EXPECT_EQ( res.msg, " Path does not exist" ); + EXPECT_FALSE( res.ok() ); +} + +TEST_F( FileSystemTest, PosixFileLikeReadEndBeforeBegin ) +{ + PosixFileLike dummy( "dummy" ); + auto res = dummy.read( 10, 0 ); + EXPECT_EQ( res.err().code, aws::store::filesystem::FileErrorCode::InvalidArguments ); + EXPECT_EQ( res.err().msg, "End must be after the beginning" ); + EXPECT_FALSE( res.ok() ); +} + +TEST_F( FileSystemTest, PosixFileLikeReadEndEqualsBegin ) +{ + PosixFileLike dummy( "dummy" ); + auto res = dummy.read( 10, 10 ); + EXPECT_EQ( res.val().size(), 0 ); + EXPECT_TRUE( res.ok() ); +} + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/StoreLoggerTest.cpp b/test/unit/StoreLoggerTest.cpp new file mode 100644 index 00000000..6aa3fec2 --- /dev/null +++ b/test/unit/StoreLoggerTest.cpp @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StoreLogger.h" +#include "LogLevel.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Store +{ + +class LoggerTest : public ::testing::Test +{ +protected: + void + SetUp() override + { + } + + void + TearDown() override + { + } +}; + +TEST_F( LoggerTest, LevelTest ) +{ + gSystemWideLogLevel = LogLevel::Off; + Logger loggerOff; + loggerOff.log( aws::store::logging::LogLevel::Error, "error when off test" ); + gSystemWideLogLevel = LogLevel::Error; + Logger loggerError; + loggerError.log( aws::store::logging::LogLevel::Error, "error when error test" ); + gSystemWideLogLevel = LogLevel::Warning; + Logger loggerWarning; + loggerWarning.log( aws::store::logging::LogLevel::Warning, "warning when warning test" ); + gSystemWideLogLevel = LogLevel::Info; + Logger loggerInfo; + loggerInfo.log( aws::store::logging::LogLevel::Info, "info when info test" ); + gSystemWideLogLevel = LogLevel::Trace; + Logger loggerTrace; + loggerTrace.log( aws::store::logging::LogLevel::Disabled, "disabled when trace test" ); + loggerTrace.log( aws::store::logging::LogLevel::Trace, "trace when trace test" ); + loggerTrace.log( aws::store::logging::LogLevel::Debug, "debug when trace test" ); + loggerTrace.log( aws::store::logging::LogLevel::Info, "info when trace test" ); + loggerTrace.log( aws::store::logging::LogLevel::Warning, "warning when trace test" ); + loggerTrace.log( aws::store::logging::LogLevel::Error, "error when trace test" ); +} + +} // namespace Store +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/StreamForwarderTest.cpp b/test/unit/StreamForwarderTest.cpp new file mode 100644 index 00000000..2f783291 --- /dev/null +++ b/test/unit/StreamForwarderTest.cpp @@ -0,0 +1,513 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StreamForwarder.h" +#include "CANDataTypes.h" +#include "CANInterfaceIDTranslator.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CollectionSchemeIngestion.h" +#include "DataSenderProtoReader.h" +#include "DataSenderProtoWriter.h" +#include "ICollectionScheme.h" +#include "ICollectionSchemeList.h" +#include "IConnectionTypes.h" +#include "OBDDataTypes.h" +#include "RateLimiter.h" +#include "SenderMock.h" +#include "SignalTypes.h" +#include "StreamManager.h" +#include "TelemetryDataSender.h" +#include "Testing.h" +#include "TimeTypes.h" +#include "WaitUntil.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::DoAll; +using ::testing::Gt; +using ::testing::Invoke; +using ::testing::InvokeArgument; +using ::testing::InvokeWithoutArgs; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::SetArgReferee; +using ::testing::StrictMock; +using ::testing::WithArg; +using ::testing::WithArgs; + +class StreamForwarderTest : public ::testing::Test +{ +protected: + std::shared_ptr mStreamManager; + std::shared_ptr mStreamForwarder; + std::shared_ptr mTelemetryDataSender; + std::shared_ptr> mMqttSender; + std::shared_ptr mRateLimiter; + boost::filesystem::path mPersistenceRootDir; + CANInterfaceIDTranslator mCANIDTranslator; + std::shared_ptr mClock; + static constexpr unsigned MAXIMUM_PAYLOAD_SIZE = 400; + PayloadAdaptionConfig mPayloadAdaptionConfigUncompressed{ 80, 70, 90, 10 }; + PayloadAdaptionConfig mPayloadAdaptionConfigCompressed{ 80, 70, 90, 10 }; + + struct CollectedSignals + { + Store::CampaignID campaignID = {}; + TriggeredCollectionSchemeData data = {}; + std::map> signals = {}; + }; + + void + SetUp() override + { + mClock = ClockHandler::getClock(); + mCANIDTranslator.add( "can123" ); + mPersistenceRootDir = getTempDir(); + auto protoWriter = std::make_shared( mCANIDTranslator, nullptr ); + auto protoReader = std::make_shared( mCANIDTranslator ); + mStreamManager = std::make_shared( mPersistenceRootDir.string(), protoWriter, 0 ); + mMqttSender = std::make_shared>(); + EXPECT_CALL( *mMqttSender, getMaxSendSize() ) + .Times( AnyNumber() ) + .WillRepeatedly( Return( MAXIMUM_PAYLOAD_SIZE ) ); + mTelemetryDataSender = std::make_shared( + mMqttSender, protoWriter, mPayloadAdaptionConfigUncompressed, mPayloadAdaptionConfigCompressed ); + mRateLimiter = std::make_shared(); + auto streamForwarderIdleTime = 10; + mStreamForwarder = std::make_shared( + mStreamManager, mTelemetryDataSender, mRateLimiter, streamForwarderIdleTime ); + } + + void + TearDown() override + { + mStreamForwarder->stop(); + boost::filesystem::remove_all( mPersistenceRootDir ); + } + + void + trackNumMqttMessagesSent( std::atomic &messagesSent ) + { + EXPECT_CALL( *mMqttSender, isAlive() ).Times( AnyNumber() ).WillRepeatedly( Return( true ) ); + ON_CALL( *mMqttSender, mockedSendBuffer( _, _, _, _ ) ) + .WillByDefault( DoAll( InvokeWithoutArgs( [&]() { + messagesSent++; + // Sleep to allow the main thread to now cancel or update the forwarding config + std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) ); + } ), + InvokeArgument<3>( ConnectivityError::Success ) ) ); + } + + Aws::IoTFleetWise::ActiveCollectionSchemes + GetMultipleCampaignsWithMultiplePartitions() + { + Aws::IoTFleetWise::ActiveCollectionSchemes out; + + Schemas::CollectionSchemesMsg::CollectionScheme campaign1; + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + scheme.set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/campaign1" ); + scheme.set_campaign_arn( "arn:aws:iam::2.23606797749:user/campaign1" ); + scheme.set_decoder_manifest_sync_id( "model_manifest_13" ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + auto *partition = store_and_forward_configuration->add_partition_configuration(); + + // partition 0 + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + // partition 1 + partition = store_and_forward_configuration->add_partition_configuration(); + storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition1" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 0 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 1 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 2 ); + signalInformation->set_data_partition_id( 1 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 3 ); + signalInformation->set_data_partition_id( 1 ); + scheme.add_raw_can_frames_to_collect(); + + campaign1 = scheme; + } + + Schemas::CollectionSchemesMsg::CollectionScheme campaign2; + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + scheme.set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/campaign2" ); + scheme.set_campaign_arn( "arn:aws:iam::2.23606797749:user/campaign2" ); + scheme.set_decoder_manifest_sync_id( "model_manifest_13" ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + auto *partition = store_and_forward_configuration->add_partition_configuration(); + + // partition 0 + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + // partition 1 + partition = store_and_forward_configuration->add_partition_configuration(); + storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( 1000000 ); + storageOptions->set_storage_location( "partition1" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 4 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 5 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 6 ); + signalInformation->set_data_partition_id( 1 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 7 ); + signalInformation->set_data_partition_id( 1 ); + scheme.add_raw_can_frames_to_collect(); + + campaign2 = scheme; + } + + convertSchemes( { campaign1, campaign2 }, out ); + return out; + } + + inline std::vector + fakeCanFrames() + { + std::array buf = {}; + CollectedCanRawFrame frame( 0, 0, mClock->systemTimeSinceEpochMs(), buf, 12 ); + return { frame }; + } + + inline DTCInfo + fakeDtcInfo() + { + DTCInfo info{}; + info.receiveTime = mClock->systemTimeSinceEpochMs(); + info.mSID = SID::TESTING; + info.mDTCCodes.emplace_back( "code" ); + return info; + } + + static void + convertSchemes( std::vector schemes, + Aws::IoTFleetWise::ActiveCollectionSchemes &convertedSchemes ) + { + Aws::IoTFleetWise::ActiveCollectionSchemes activeSchemes; + for ( auto scheme : schemes ) + { + auto campaign = std::make_shared( +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + std::make_shared() +#endif + ); + campaign->copyData( std::make_shared( scheme ) ); + ASSERT_TRUE( campaign->build() ); + activeSchemes.activeCollectionSchemes.emplace_back( campaign ); + } + convertedSchemes = activeSchemes; + } + + void + storeAll( std::vector collectedSignals ) + { + for ( auto signal : collectedSignals ) + { + ASSERT_EQ( mStreamManager->appendToStreams( signal.data ), Store::StreamManager::ReturnCode::SUCCESS ); + } + } + + void + forwardAll( std::vector collectedSignals ) + { + for ( auto signals : collectedSignals ) + { + for ( auto entry : signals.signals ) + { + mStreamForwarder->beginForward( + signals.campaignID, entry.first, Aws::IoTFleetWise::Store::StreamForwarder::Source::CONDITION ); + } + } + } + + void + forwardAllIoTJob( std::vector collectedSignals, uint64_t endTime ) + { + for ( auto signals : collectedSignals ) + { + mStreamForwarder->beginJobForward( signals.campaignID, endTime ); + } + } + + std::vector + buildTestData( const std::shared_ptr &campaign, + Timestamp triggerTime = 0 ) + { + std::vector collectedSignals; + for ( auto scheme : campaign->activeCollectionSchemes ) + { + CollectedSignals signals; + signals.campaignID = scheme->getCampaignArn(); + + signals.data.eventID = 1234; + signals.data.metadata.collectionSchemeID = scheme->getCollectionSchemeID(); + signals.data.metadata.campaignArn = signals.campaignID; + signals.data.canFrames = fakeCanFrames(); + signals.data.mDTCInfo = fakeDtcInfo(); + for ( auto signal : scheme->getCollectSignals() ) + { + CollectedSignal collectedSignal = { + signal.signalID, mClock->systemTimeSinceEpochMs(), 5, SignalType::UINT8 }; + signals.signals[signal.dataPartitionId].emplace_back( collectedSignal ); + signals.data.signals.emplace_back( collectedSignal ); + signals.data.triggerTime = triggerTime; + } + collectedSignals.emplace_back( signals ); + } + return collectedSignals; + } +}; + +TEST_F( StreamForwarderTest, StoreAndForwardDataFromMultipleCampaignsAndPartitions ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + storeAll( collectedSignals ); + forwardAll( collectedSignals ); + + WAIT_ASSERT_EQ( static_cast( messagesSent ), 4 ); +} + +TEST_F( StreamForwarderTest, ForwardStopsForPartitionWhenRequested ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + // store and forward all partitions + storeAll( collectedSignals ); + forwardAll( collectedSignals ); + WAIT_ASSERT_EQ( static_cast( messagesSent ), 4 ); + messagesSent = 0; + + // stop uploading from one of the campaigns/partitions + mStreamForwarder->cancelForward( campaign->activeCollectionSchemes[0]->getCampaignArn(), + 0, + Aws::IoTFleetWise::Store::StreamForwarder::Source::CONDITION ); + storeAll( collectedSignals ); + WAIT_ASSERT_EQ( static_cast( messagesSent ), 3 ); +} + +TEST_F( StreamForwarderTest, ForwardStopsForCampaignsThatAreRemoved ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + storeAll( collectedSignals ); + + // remove the campaign entirely + mStreamManager->onChangeCollectionSchemeList( {} ); + + forwardAll( collectedSignals ); + std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); // give the thread time to attempt a forward + + ASSERT_EQ( static_cast( messagesSent ), 0 ); +} + +TEST_F( StreamForwarderTest, StoreAndForwardDataFromMultipleCampaignsAndPartitionsIoTJobs ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + storeAll( collectedSignals ); + forwardAllIoTJob( collectedSignals, 0 ); + + WAIT_ASSERT_EQ( static_cast( messagesSent ), 4 ); +} + +TEST_F( StreamForwarderTest, ForwardStopsForIoTJobWhenEndOfStream ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + // store and forward all partitions + storeAll( collectedSignals ); + forwardAllIoTJob( collectedSignals, 0 ); + WAIT_ASSERT_EQ( static_cast( messagesSent ), 4 ); + messagesSent = 0; + + // End of Stream is hit so forwarding has stopped + storeAll( collectedSignals ); + WAIT_ASSERT_EQ( static_cast( messagesSent ), 0 ); +} + +TEST_F( StreamForwarderTest, ForwardWhenBothIotJobAndConditionAreActive ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + // store and forward all partitions + storeAll( collectedSignals ); + forwardAllIoTJob( collectedSignals, 0 ); + forwardAll( collectedSignals ); + WAIT_ASSERT_EQ( static_cast( messagesSent ), 4 ); + messagesSent = 0; + std::this_thread::sleep_for( + std::chrono::milliseconds( 1000 ) ); // give thread time to update mJobCampaignToPartitions + + // End of Stream is hit so forwarding has stopped for IOT_JOB + // stop uploading from one of the campaigns/partitions + mStreamForwarder->cancelForward( campaign->activeCollectionSchemes[0]->getCampaignArn(), + 0, + Aws::IoTFleetWise::Store::StreamForwarder::Source::CONDITION ); + storeAll( collectedSignals ); + WAIT_ASSERT_EQ( static_cast( messagesSent ), 3 ); +} + +TEST_F( StreamForwarderTest, ForwarderStopsForIoTJobWhenEndTime ) +{ + // apply campaign config + auto campaign = + std::make_shared( GetMultipleCampaignsWithMultiplePartitions() ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + auto collectedSignals = buildTestData( campaign ); + + std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); + + // isolate the endTime + auto endTime = mClock->systemTimeSinceEpochMs(); + + // build same test data but with collection triggertime after the endTime + auto collectedSignals2 = buildTestData( campaign, endTime + 1 ); + + auto combinedSignals = collectedSignals; + combinedSignals.insert( combinedSignals.end(), collectedSignals2.begin(), collectedSignals2.end() ); + + std::atomic messagesSent{ 0 }; + trackNumMqttMessagesSent( messagesSent ); + + // start the forwarder thread + ASSERT_TRUE( mStreamForwarder->start() ); + ASSERT_TRUE( mStreamForwarder->isAlive() ); + + storeAll( combinedSignals ); + // endTime will be after the collectedSignals collection trigger time, but before the collectedSignals2 collection + // trigger time + forwardAllIoTJob( combinedSignals, endTime ); + + WAIT_ASSERT_EQ( static_cast( messagesSent ), 4 ); +} +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/StreamManagerTest.cpp b/test/unit/StreamManagerTest.cpp new file mode 100644 index 00000000..327ed795 --- /dev/null +++ b/test/unit/StreamManagerTest.cpp @@ -0,0 +1,1109 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "StreamManager.h" +#include "CANDataTypes.h" +#include "CANInterfaceIDTranslator.h" +#include "Clock.h" +#include "ClockHandler.h" +#include "CollectionInspectionAPITypes.h" +#include "CollectionSchemeIngestion.h" +#include "DataSenderProtoReader.h" +#include "DataSenderProtoWriter.h" +#include "ICollectionScheme.h" +#include "ICollectionSchemeList.h" +#include "OBDDataTypes.h" +#include "SignalTypes.h" +#include "StoreFileSystem.h" +#include "StoreLogger.h" +#include "Testing.h" +#include "collection_schemes.pb.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ + +class StreamManagerTest : public ::testing::Test +{ +protected: + uint64_t partitionMaximumSizeInBytes = 1000000; + + StreamManagerTest() + { + mTranslator.add( "can123" ); + mPersistenceRootDir = getTempDir(); + auto protoWriter = std::make_shared( mTranslator, nullptr ); + auto protoReader = std::make_shared( mTranslator ); + mStreamManager = std::make_shared( mPersistenceRootDir.string(), protoWriter, 0 ); + mProtoReader = std::make_shared( mTranslator ); + setupCampaignTestData(); + } + + CANInterfaceIDTranslator mTranslator; + std::shared_ptr mClock = ClockHandler::getClock(); + std::shared_ptr mLogger = std::make_shared(); + boost::filesystem::path mPersistenceRootDir; + std::shared_ptr mStreamManager; + std::shared_ptr mProtoReader; + + // campaign test data + std::shared_ptr noCampaigns; + std::shared_ptr campaignWithSinglePartition; + std::shared_ptr campaignWithTwoPartitions; + std::shared_ptr campaignWithInvalidStorageLocation; + std::shared_ptr campaignWithOneSecondTTL; + + void + TearDown() override + { + mStreamManager->onChangeCollectionSchemeList( noCampaigns ); + ASSERT_TRUE( boost::filesystem::is_empty( mPersistenceRootDir ) ); + boost::filesystem::remove( mPersistenceRootDir ); + } + + void + setupCampaignTestData() + { + // no campaigns + { + Aws::IoTFleetWise::ActiveCollectionSchemes out; + convertSchemes( {}, out ); + noCampaigns = std::make_shared( out ); + } + // campaign with one partition + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + + scheme.set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_campaign_arn( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_decoder_manifest_sync_id( "model_manifest_13" ); + scheme.set_compress_collected_data( true ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + // partition 0 + auto *partition = store_and_forward_configuration->add_partition_configuration(); + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( partitionMaximumSizeInBytes ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 0 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + + Aws::IoTFleetWise::ActiveCollectionSchemes out; + convertScheme( scheme, out ); + campaignWithSinglePartition = std::make_shared( out ); + } + // campaign with two partitions + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + scheme.set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_campaign_arn( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_decoder_manifest_sync_id( "model_manifest_13" ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + auto *partition = store_and_forward_configuration->add_partition_configuration(); + // partition 0 + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( partitionMaximumSizeInBytes ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + // partition 1 + partition = store_and_forward_configuration->add_partition_configuration(); + storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( partitionMaximumSizeInBytes ); + storageOptions->set_storage_location( "partition1" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 0 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 1 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 2 ); + signalInformation->set_data_partition_id( 1 ); + scheme.add_raw_can_frames_to_collect(); + signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 3 ); + signalInformation->set_data_partition_id( 1 ); + scheme.add_raw_can_frames_to_collect(); + + Aws::IoTFleetWise::ActiveCollectionSchemes out; + convertScheme( scheme, out ); + campaignWithTwoPartitions = std::make_shared( out ); + } + // campaign with invalid storage location + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + + scheme.set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_campaign_arn( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_decoder_manifest_sync_id( "model_manifest_13" ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + // partition 0 + auto *partition = store_and_forward_configuration->add_partition_configuration(); + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( partitionMaximumSizeInBytes ); + storageOptions->set_storage_location( "../" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1000000 ); + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 0 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + + Aws::IoTFleetWise::ActiveCollectionSchemes out; + convertScheme( scheme, out ); + campaignWithInvalidStorageLocation = + std::make_shared( out ); + } + // campaign with no ttl + { + Schemas::CollectionSchemesMsg::CollectionScheme scheme; + + scheme.set_campaign_sync_id( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_campaign_arn( "arn:aws:iam::2.23606797749:user/Development/product_1235/*" ); + scheme.set_decoder_manifest_sync_id( "model_manifest_13" ); + + auto *store_and_forward_configuration = scheme.mutable_store_and_forward_configuration(); + + // partition 0 + auto *partition = store_and_forward_configuration->add_partition_configuration(); + auto *storageOptions = partition->mutable_storage_options(); + storageOptions->set_maximum_size_in_bytes( partitionMaximumSizeInBytes ); + storageOptions->set_storage_location( "partition0" ); + storageOptions->set_minimum_time_to_live_in_seconds( 1 ); + + // map signals to partitions + auto *signalInformation = scheme.add_signal_information(); + signalInformation->set_signal_id( 0 ); + signalInformation->set_data_partition_id( 0 ); + scheme.add_raw_can_frames_to_collect(); + + Aws::IoTFleetWise::ActiveCollectionSchemes out; + convertScheme( scheme, out ); + campaignWithOneSecondTTL = std::make_shared( out ); + } + } + + static void + convertScheme( Schemas::CollectionSchemesMsg::CollectionScheme scheme, + Aws::IoTFleetWise::ActiveCollectionSchemes &convertedSchemes ) + { + convertSchemes( std::vector{ scheme }, convertedSchemes ); + } + + static void + convertSchemes( std::vector schemes, + Aws::IoTFleetWise::ActiveCollectionSchemes &convertedSchemes ) + { + Aws::IoTFleetWise::ActiveCollectionSchemes activeSchemes; + for ( auto scheme : schemes ) + { + auto campaign = std::make_shared( +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + std::make_shared() +#endif + ); + campaign->copyData( std::make_shared( scheme ) ); + ASSERT_TRUE( campaign->build() ); + activeSchemes.activeCollectionSchemes.emplace_back( campaign ); + } + convertedSchemes = activeSchemes; + } + + inline std::vector + fakeCanFrames() + { + std::array buf = {}; + CollectedCanRawFrame frame( 0, 0, mClock->systemTimeSinceEpochMs(), buf, 12 ); + return { frame }; + } + + inline DTCInfo + fakeDtcInfo() + { + DTCInfo info{}; + info.receiveTime = mClock->systemTimeSinceEpochMs(); + info.mSID = SID::TESTING; + info.mDTCCodes.emplace_back( "code" ); + return info; + } + + static inline std::string + getCampaignID( std::shared_ptr campaign ) + { + return campaign->activeCollectionSchemes[0]->getCollectionSchemeID(); + } + + static inline std::string + getCampaignArn( std::shared_ptr campaign ) + { + return campaign->activeCollectionSchemes[0]->getCampaignArn(); + } + + void + verifyStreamLocationAndSize( boost::filesystem::path location, uint64_t size ) + { + std::shared_ptr stream; + getStream( location, stream ); + for ( uint64_t i = 0; i < size; ++i ) + { + auto val_or = stream->read( i, aws::store::stream::ReadOptions{ {}, false, {} } ); + ASSERT_TRUE( val_or.ok() ); + } + auto val_or = stream->read( size, aws::store::stream::ReadOptions{ {}, false, {} } ); + ASSERT_FALSE( val_or.ok() ); + } + + void + getStream( boost::filesystem::path streamLocation, std::shared_ptr &out ) + { + const std::shared_ptr fs = + std::make_shared( streamLocation ); + auto stream_or = aws::store::stream::FileStream::openOrCreate( aws::store::stream::StreamOptions{ + mStreamManager->STREAM_DEFAULT_MIN_SEGMENT_SIZE, + static_cast( partitionMaximumSizeInBytes ), + true, + fs, + mLogger, + aws::store::kv::KVOptions{ + true, + fs, + mLogger, + mStreamManager->KV_STORE_IDENTIFIER, + mStreamManager->KV_COMPACT_AFTER, + }, + } ); + ASSERT_TRUE( stream_or.ok() ); + out = stream_or.val(); + } + + int + getNumStoreFiles() + { + auto numFiles = 0; + for ( boost::filesystem::recursive_directory_iterator it( mPersistenceRootDir ); + it != boost::filesystem::recursive_directory_iterator(); + ++it ) + { + if ( boost::filesystem::is_regular_file( *it ) ) + { + ++numFiles; + } + } + return numFiles; + } + + void + deserialize( std::string record, bool decompress, TriggeredCollectionSchemeData &data ) + { + if ( decompress ) + { + std::string out; + ASSERT_TRUE( snappy::Uncompress( record.data(), record.size(), &out ) ); + record = out; + } + ASSERT_TRUE( mProtoReader->setupVehicleData( record ) ); + ASSERT_TRUE( mProtoReader->deserializeVehicleData( data ) ); + } +}; + +TEST_F( StreamManagerTest, StreamAppendFailsIfEmptyData ) +{ + TriggeredCollectionSchemeData data{}; + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::STREAM_NOT_FOUND ); +} + +TEST_F( StreamManagerTest, StreamHasCamapaign ) +{ + auto campaign = campaignWithSinglePartition; + auto campaignID = getCampaignID( campaign ); + std::string unknownCampaignID = "unknownCampaignID"; + + mStreamManager->onChangeCollectionSchemeList( campaign ); + + ASSERT_TRUE( mStreamManager->hasCampaign( campaignID ) ); + ASSERT_FALSE( mStreamManager->hasCampaign( unknownCampaignID ) ); +} + +TEST_F( StreamManagerTest, StreamAppendsNoSignals ) +{ + auto campaign = campaignWithSinglePartition; + auto campaignID = getCampaignID( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignID; + data.metadata.persist = true; + data.canFrames = {}; + data.signals = {}; + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::EMPTY_DATA ); +} + +TEST_F( StreamManagerTest, StreamAppendFailsWhenStorageLocationIsInvalid ) +{ + auto campaign = campaignWithInvalidStorageLocation; + auto campaignID = getCampaignID( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignID; + data.metadata.persist = true; + data.canFrames = {}; + data.signals = {}; + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::STREAM_NOT_FOUND ); +} + +TEST_F( StreamManagerTest, StreamAppendFailsForNonExistantCampaign ) +{ + auto campaign = campaignWithSinglePartition; + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.metadata.collectionSchemeID = "unknown"; + data.metadata.campaignArn = "unknown"; + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::STREAM_NOT_FOUND ); +} + +TEST_F( StreamManagerTest, StreamAppendOneSignalOnePartition ) +{ + auto campaign = campaignWithSinglePartition; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.eventID = 1234; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 }, + // signals that are not part of the campaign + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 2, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 3, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 4, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); + + // ensure stream files are written to expected location + auto expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[0].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 1 ); + + // verify we can read back the data + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + size_t expectedNumSignals = 2; + ASSERT_EQ( readData.signals.size(), expectedNumSignals ); + for ( size_t i = 0; i < expectedNumSignals; ++i ) + { + ASSERT_EQ( readData.signals[i].receiveTime, data.signals[i].receiveTime ); + ASSERT_EQ( readData.signals[i].signalID, data.signals[i].signalID ); + ASSERT_EQ( readData.signals[i].value.value.doubleVal, + static_cast( data.signals[i].value.value.uint8Val ) ); + } + // CAN frames + ASSERT_EQ( readData.canFrames.size(), data.canFrames.size() ); + for ( size_t i = 0; i < data.canFrames.size(); ++i ) + { + ASSERT_EQ( readData.canFrames[i].receiveTime, data.canFrames[i].receiveTime ); + ASSERT_EQ( readData.canFrames[i].channelId, data.canFrames[i].channelId ); + ASSERT_EQ( readData.canFrames[i].frameID, data.canFrames[i].frameID ); + ASSERT_EQ( readData.canFrames[i].size, data.canFrames[i].size ); + ASSERT_EQ( readData.canFrames[i].data, data.canFrames[i].data ); + } + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); +} + +TEST_F( StreamManagerTest, StreamAppendSignalsAcrossMultiplePartitions ) +{ + auto campaign = campaignWithTwoPartitions; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + // two signals for each of the two partitions + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 2, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 3, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); + + // ensure partition 0 is written to disk at the proper location and contains 1 entry + auto expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[0].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 1 ); + + // ensure partition 1 is written to disk at the proper location and contains 1 entry + expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[1].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 1 ); + + // verify we can read back partition 0 data + { + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data.signals[0].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data.signals[0].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data.signals[0].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data.signals[1].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data.signals[1].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data.signals[1].value.value.uint8Val ) ); + // CAN frames + // all frames will be in partition 0 since it's considered the default + ASSERT_EQ( readData.canFrames.size(), data.canFrames.size() ); + for ( size_t i = 0; i < data.canFrames.size(); ++i ) + { + ASSERT_EQ( readData.canFrames[i].receiveTime, data.canFrames[i].receiveTime ); + ASSERT_EQ( readData.canFrames[i].channelId, data.canFrames[i].channelId ); + ASSERT_EQ( readData.canFrames[i].frameID, data.canFrames[i].frameID ); + ASSERT_EQ( readData.canFrames[i].size, data.canFrames[i].size ); + ASSERT_EQ( readData.canFrames[i].data, data.canFrames[i].data ); + } + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + { + // verify we can read back partition 1 data + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 1, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data.signals[2].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data.signals[2].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data.signals[2].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data.signals[3].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data.signals[3].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data.signals[3].value.value.uint8Val ) ); + // CAN frames + // no frames since this is not the default partition + ASSERT_TRUE( readData.canFrames.empty() ); + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } +} + +TEST_F( StreamManagerTest, StreamMultipleAppendSignalsAcrossMultiplePartitions ) +{ + auto campaign = campaignWithTwoPartitions; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + // two signals for each of the two partitions + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 2, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 3, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); + + TriggeredCollectionSchemeData data2{}; + data2.triggerTime = 12345567; + data2.metadata.collectionSchemeID = campaignID; + data2.metadata.campaignArn = campaignID; + // two signals for each of the two partitions + data2.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 4, SignalType::UINT8 }, + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 5, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 6, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 7, SignalType::UINT8 } }; + data2.canFrames = fakeCanFrames(); + data2.mDTCInfo = fakeDtcInfo(); + ASSERT_EQ( mStreamManager->appendToStreams( data2 ), Store::StreamManager::ReturnCode::SUCCESS ); + + // ensure partition 0 is written to disk at the proper location and contains 2 entries + auto expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[0].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 2 ); + + // ensure partition 1 is written to disk at the proper location and contains 2 entries + expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[1].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 2 ); + + // verify we can read back partition 0 data + + { + // partition 0 entry 1 + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data.signals[0].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data.signals[0].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data.signals[0].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data.signals[1].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data.signals[1].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data.signals[1].value.value.uint8Val ) ); + // CAN frames + // all frames will be in partition 0 since it's considered the default + ASSERT_EQ( readData.canFrames.size(), data.canFrames.size() ); + for ( size_t i = 0; i < data.canFrames.size(); ++i ) + { + ASSERT_EQ( readData.canFrames[i].receiveTime, data.canFrames[i].receiveTime ); + ASSERT_EQ( readData.canFrames[i].channelId, data.canFrames[i].channelId ); + ASSERT_EQ( readData.canFrames[i].frameID, data.canFrames[i].frameID ); + ASSERT_EQ( readData.canFrames[i].size, data.canFrames[i].size ); + ASSERT_EQ( readData.canFrames[i].data, data.canFrames[i].data ); + } + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + + { + // partition 0 entry 2 + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data2.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data2.eventID ); + ASSERT_EQ( readData.triggerTime, data2.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data2.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data2.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data2.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data2.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data2.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data2.signals[0].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data2.signals[0].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data2.signals[0].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data2.signals[1].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data2.signals[1].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data2.signals[1].value.value.uint8Val ) ); + // CAN frames + // all frames will be in partition 0 since it's considered the default + ASSERT_EQ( readData.canFrames.size(), data2.canFrames.size() ); + for ( size_t i = 0; i < data2.canFrames.size(); ++i ) + { + ASSERT_EQ( readData.canFrames[i].receiveTime, data2.canFrames[i].receiveTime ); + ASSERT_EQ( readData.canFrames[i].channelId, data2.canFrames[i].channelId ); + ASSERT_EQ( readData.canFrames[i].frameID, data2.canFrames[i].frameID ); + ASSERT_EQ( readData.canFrames[i].size, data2.canFrames[i].size ); + ASSERT_EQ( readData.canFrames[i].data, data2.canFrames[i].data ); + } + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + + // verify we can read back partition 1 data + + { + // partition 1 entry 1 + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 1, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data.signals[2].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data.signals[2].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data.signals[2].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data.signals[3].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data.signals[3].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data.signals[3].value.value.uint8Val ) ); + // CAN frames + // no frames since this is not the default partition + ASSERT_TRUE( readData.canFrames.empty() ); + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + + { + // partition 1 entry 2 + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 1, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data2.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data2.eventID ); + ASSERT_EQ( readData.triggerTime, data2.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data2.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data2.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data2.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data2.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data2.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data2.signals[2].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data2.signals[2].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data2.signals[2].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data2.signals[3].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data2.signals[3].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data2.signals[3].value.value.uint8Val ) ); + // CAN frames + // no frames since this is not the default partition + ASSERT_TRUE( readData.canFrames.empty() ); + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } +} + +TEST_F( StreamManagerTest, StreamConfigChangeWithSameName ) +{ + // test prereq: campaigns must have same name + ASSERT_EQ( getCampaignID( campaignWithSinglePartition ), getCampaignID( campaignWithTwoPartitions ) ); + + // verify we can set campaign config and append to a stream + { + auto campaign = campaignWithSinglePartition; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.eventID = 1234; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 }, + // signals that are not part of the campaign + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 2, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 3, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 4, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); + + // ensure stream files are written to expected location + auto expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[0].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 1 ); + + // verify we can read back the data + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + size_t expectedNumSignals = 2; + ASSERT_EQ( readData.signals.size(), expectedNumSignals ); + for ( size_t i = 0; i < expectedNumSignals; ++i ) + { + ASSERT_EQ( readData.signals[i].receiveTime, data.signals[i].receiveTime ); + ASSERT_EQ( readData.signals[i].signalID, data.signals[i].signalID ); + ASSERT_EQ( readData.signals[i].value.value.doubleVal, + static_cast( data.signals[i].value.value.uint8Val ) ); + } + // CAN frames + ASSERT_EQ( readData.canFrames.size(), data.canFrames.size() ); + for ( size_t i = 0; i < data.canFrames.size(); ++i ) + { + ASSERT_EQ( readData.canFrames[i].receiveTime, data.canFrames[i].receiveTime ); + ASSERT_EQ( readData.canFrames[i].channelId, data.canFrames[i].channelId ); + ASSERT_EQ( readData.canFrames[i].frameID, data.canFrames[i].frameID ); + ASSERT_EQ( readData.canFrames[i].size, data.canFrames[i].size ); + ASSERT_EQ( readData.canFrames[i].data, data.canFrames[i].data ); + } + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + // verify we can set a new campaign config, with the same name as the previous, and the new config will take effect + { + auto campaign = campaignWithTwoPartitions; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + // two signals for each of the two partitions + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 2, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 3, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); + + // ensure partition 0 is written to disk at the proper location and contains 1 entry + auto expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[0].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 1 ); + + // ensure partition 1 is written to disk at the proper location and contains 1 entry + expectedPartitionLocation = + mPersistenceRootDir / boost::filesystem::path{ mStreamManager->getName( campaignID ) } / + campaign->activeCollectionSchemes[0]->getStoreAndForwardConfiguration()[1].storageOptions.storageLocation; + verifyStreamLocationAndSize( expectedPartitionLocation, 1 ); + + // verify we can read back partition 0 data + { + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data.signals[0].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data.signals[0].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data.signals[0].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data.signals[1].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data.signals[1].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data.signals[1].value.value.uint8Val ) ); + // CAN frames + // all frames will be in partition 0 since it's considered the default + ASSERT_EQ( readData.canFrames.size(), data.canFrames.size() ); + for ( size_t i = 0; i < data.canFrames.size(); ++i ) + { + ASSERT_EQ( readData.canFrames[i].receiveTime, data.canFrames[i].receiveTime ); + ASSERT_EQ( readData.canFrames[i].channelId, data.canFrames[i].channelId ); + ASSERT_EQ( readData.canFrames[i].frameID, data.canFrames[i].frameID ); + ASSERT_EQ( readData.canFrames[i].size, data.canFrames[i].size ); + ASSERT_EQ( readData.canFrames[i].data, data.canFrames[i].data ); + } + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + { + // verify we can read back partition 1 data + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 1, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // verify data contents are correct + ASSERT_EQ( readData.eventID, data.eventID ); + ASSERT_EQ( readData.triggerTime, data.triggerTime ); + // metadata + ASSERT_EQ( readData.metadata.collectionSchemeID, data.metadata.collectionSchemeID ); + ASSERT_EQ( readData.metadata.decoderID, data.metadata.decoderID ); + ASSERT_EQ( readData.metadata.persist, data.metadata.persist ); + ASSERT_EQ( readData.metadata.compress, data.metadata.compress ); + ASSERT_EQ( readData.metadata.priority, data.metadata.priority ); + // signals + ASSERT_EQ( readData.signals.size(), 2 ); + ASSERT_EQ( readData.signals[0].receiveTime, data.signals[2].receiveTime ); + ASSERT_EQ( readData.signals[0].signalID, data.signals[2].signalID ); + ASSERT_EQ( readData.signals[0].value.value.doubleVal, + static_cast( data.signals[2].value.value.uint8Val ) ); + ASSERT_EQ( readData.signals[1].receiveTime, data.signals[3].receiveTime ); + ASSERT_EQ( readData.signals[1].signalID, data.signals[3].signalID ); + ASSERT_EQ( readData.signals[1].value.value.doubleVal, + static_cast( data.signals[3].value.value.uint8Val ) ); + // CAN frames + // no frames since this is not the default partition + ASSERT_TRUE( readData.canFrames.empty() ); + // DTC + ASSERT_FALSE( readData.mDTCInfo.hasItems() ); + } + } +} + +TEST_F( StreamManagerTest, ExtraStreamFilesAreDeleted ) +{ + ASSERT_TRUE( boost::filesystem::is_empty( mPersistenceRootDir ) ); + + // add one campaign/partition to stream manager + auto campaign = campaignWithSinglePartition; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); // creates the kv store file + + TriggeredCollectionSchemeData data{}; + data.eventID = 1234; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); // creates log file + + ASSERT_EQ( getNumStoreFiles(), 2 ); // stream log, kv store + + // create files for a stream that's unknown to stream manager + std::set paths = { mPersistenceRootDir / boost::filesystem::path{ "fake-campaign" } / + boost::filesystem::path{ "fake-storage-location" } / + boost::filesystem::path{ "0.log" }, + mPersistenceRootDir / boost::filesystem::path{ "fake-campaign" } / + boost::filesystem::path{ "fake-storage-location" } / + boost::filesystem::path{ mStreamManager->KV_STORE_IDENTIFIER } }; + for ( auto path : paths ) + { + boost::filesystem::create_directories( path.parent_path() ); + std::ofstream f( path.c_str() ); + f << "Hello World"; + } + ASSERT_EQ( getNumStoreFiles(), 2 + paths.size() ); + + // next time there's a collection scheme change, stream manager performs cleanup + mStreamManager->onChangeCollectionSchemeList( campaign ); + ASSERT_EQ( getNumStoreFiles(), 2 ); +} + +TEST_F( StreamManagerTest, StreamExpiresOldRecordsOnCollectionSchemeChange ) +{ + auto campaign = campaignWithOneSecondTTL; + auto campaignID = getCampaignID( campaign ); + auto campaignArn = getCampaignArn( campaign ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + TriggeredCollectionSchemeData data{}; + data.triggerTime = 12345567; + data.metadata.collectionSchemeID = campaignID; + data.metadata.campaignArn = campaignArn; + // two signals for each of the two partitions + data.signals = { CollectedSignal{ 0, mClock->systemTimeSinceEpochMs(), 0, SignalType::UINT8 }, + CollectedSignal{ 1, mClock->systemTimeSinceEpochMs(), 1, SignalType::UINT8 }, + CollectedSignal{ 2, mClock->systemTimeSinceEpochMs(), 2, SignalType::UINT8 }, + CollectedSignal{ 3, mClock->systemTimeSinceEpochMs(), 3, SignalType::UINT8 } }; + data.canFrames = fakeCanFrames(); + data.mDTCInfo = fakeDtcInfo(); + + // add a record to the stream + ASSERT_EQ( mStreamManager->appendToStreams( data ), Store::StreamManager::ReturnCode::SUCCESS ); + std::string record; + Aws::IoTFleetWise::Store::StreamManager::RecordMetadata metadata; + std::function checkpoint; + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::SUCCESS ); + ASSERT_EQ( metadata.triggerTime, data.triggerTime ); + + checkpoint(); + + TriggeredCollectionSchemeData readData; + deserialize( record, campaign->activeCollectionSchemes[0]->isCompressionNeeded(), readData ); + + // expire records + std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); + mStreamManager->onChangeCollectionSchemeList( campaign ); + + // verify records were removed + ASSERT_EQ( mStreamManager->readFromStream( campaignID, 0, record, metadata, checkpoint ), + Aws::IoTFleetWise::Store::StreamManager::ReturnCode::END_OF_STREAM ); +} + +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/TraceModuleTest.cpp b/test/unit/TraceModuleTest.cpp index af789f3e..e1cba634 100644 --- a/test/unit/TraceModuleTest.cpp +++ b/test/unit/TraceModuleTest.cpp @@ -18,7 +18,15 @@ TEST( TraceModuleTest, TraceModulePrint ) TraceModule::get().setVariable( TraceVariable::READ_SOCKET_FRAMES_0, 20 ); TraceModule::get().setVariable( TraceVariable::READ_SOCKET_FRAMES_0, 15 ); + // S&F Metrics + TraceModule::get().setVariable( TraceVariable::DATA_STORE_BYTES, 15 ); + TraceModule::get().setVariable( TraceVariable::DATA_STORE_SIGNAL_COUNT, 1 ); TraceModule::get().setVariable( TraceVariable::DATA_FORWARD_BYTES, 5 ); + TraceModule::get().setVariable( TraceVariable::DATA_FORWARD_SIGNAL_COUNT, 1 ); + TraceModule::get().setVariable( TraceVariable::DATA_EXPIRED_BYTES, 5 ); + TraceModule::get().setVariable( TraceVariable::DATA_DROPPED_BYTES, 5 ); + TraceModule::get().setVariable( TraceVariable::DATA_STORE_ERROR, 1 ); + TraceModule::get().setVariable( TraceVariable::DATA_FORWARD_ERROR, 1 ); TraceModule::get().sectionBegin( TraceSection::BUILD_MQTT ); std::this_thread::sleep_for( std::chrono::milliseconds( 4 ) ); diff --git a/test/unit/support/CollectionSchemeManagerMock.h b/test/unit/support/CollectionSchemeManagerMock.h index 9d2bea7b..595e2319 100644 --- a/test/unit/support/CollectionSchemeManagerMock.h +++ b/test/unit/support/CollectionSchemeManagerMock.h @@ -20,6 +20,10 @@ #include #include +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +#include "LastKnownStateIngestion.h" +#endif + #define SECOND_TO_MILLISECOND( x ) ( 1000 ) * ( x ) namespace Aws @@ -47,8 +51,22 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager CANInterfaceIDTranslator &canIDTranslator, std::shared_ptr checkinSender, SyncID decoderManifestID, - std::shared_ptr rawDataBufferManager = nullptr ) - : CollectionSchemeManager( schemaPersistencyPtr, canIDTranslator, checkinSender, rawDataBufferManager ) + std::shared_ptr rawDataBufferManager = nullptr +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , + GetActuatorNamesCallback getActuatorNamesCallback = nullptr +#endif + + ) + : CollectionSchemeManager( schemaPersistencyPtr, + canIDTranslator, + checkinSender, + rawDataBufferManager +#ifdef FWE_FEATURE_REMOTE_COMMANDS + , + getActuatorNamesCallback +#endif + ) { mCurrentDecoderManifestID = decoderManifestID; } @@ -83,9 +101,10 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager } void - matrixExtractor( const std::shared_ptr &inspectionMatrix ) + matrixExtractor( const std::shared_ptr &inspectionMatrix, + const std::shared_ptr &fetchMatrix ) { - CollectionSchemeManager::matrixExtractor( inspectionMatrix ); + CollectionSchemeManager::matrixExtractor( inspectionMatrix, fetchMatrix ); } void @@ -94,6 +113,12 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager CollectionSchemeManager::inspectionMatrixUpdater( inspectionMatrix ); } + void + fetchMatrixUpdater( const std::shared_ptr &fetchMatrix ) + { + CollectionSchemeManager::fetchMatrixUpdater( fetchMatrix ); + } + #ifdef FWE_FEATURE_VISION_SYSTEM_DATA void updateRawDataBufferConfigComplexSignals( @@ -105,6 +130,13 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager } #endif + void + updateRawDataBufferConfigStringSignals( + std::unordered_map &updatedSignals ) + { + CollectionSchemeManager::updateRawDataBufferConfigStringSignals( updatedSignals ); + } + void setCollectionSchemePersistency( const std::shared_ptr &collectionSchemePersistency ) { @@ -121,6 +153,34 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager { mCollectionSchemeList = pl; } +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + void + myInvokeStateTemplates() + { + this->onStateTemplatesChanged( mLastKnownStateIngestionTest ); + } + + bool + getmProcessStateTemplates() + { + return mProcessStateTemplates; + } + + void + setLastKnownStateIngestion( std::shared_ptr lastKnownStateIngestion ) + { + mLastKnownStateIngestion = lastKnownStateIngestion; + } + + void + setStateTemplates( std::shared_ptr stateTemplates ) + { + for ( auto &stateTemplate : stateTemplates->stateTemplatesToAdd ) + { + mStateTemplates.emplace( stateTemplate->id, stateTemplate ); + } + } +#endif void setTimeLine( const std::priority_queue, std::greater> &TimeLine ) @@ -194,6 +254,18 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager return CollectionSchemeManager::retrieve( retrieveType ); } +#ifdef FWE_FEATURE_STORE_AND_FORWARD + void + generateInspectionMatrix( std::shared_ptr &inspectionMatrix, + std::shared_ptr &fetchMatrix ) + { + inspectionMatrix = std::make_shared(); + fetchMatrix = std::make_shared(); + this->matrixExtractor( inspectionMatrix, fetchMatrix ); + this->inspectionMatrixUpdater( inspectionMatrix ); + } +#endif + void setmCollectionSchemeAvailable( bool val ) { @@ -243,6 +315,9 @@ class CollectionSchemeManagerWrapper : public CollectionSchemeManager public: IDecoderManifestPtr mDmTest; std::shared_ptr mPlTest; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE + std::shared_ptr mLastKnownStateIngestionTest; +#endif }; class mockCollectionScheme : public CollectionSchemeIngestion @@ -312,5 +387,15 @@ class mockCacheAndPersist : public CacheAndPersist MOCK_METHOD( ErrorCode, read, (uint8_t *const, size_t, DataType, const std::string &), ( override ) ); }; +#ifdef FWE_FEATURE_LAST_KNOWN_STATE +class LastKnownStateIngestionMock : public LastKnownStateIngestion +{ +public: + MOCK_METHOD( bool, build, () ); + MOCK_METHOD( std::shared_ptr, getStateTemplatesDiff, (), ( const ) ); + MOCK_METHOD( const std::vector &, getData, (), ( const ) ); +}; +#endif + } // namespace IoTFleetWise } // namespace Aws diff --git a/test/unit/support/CollectionSchemeManagerTest.h b/test/unit/support/CollectionSchemeManagerTest.h index 9c342ec2..33199bc9 100644 --- a/test/unit/support/CollectionSchemeManagerTest.h +++ b/test/unit/support/CollectionSchemeManagerTest.h @@ -7,6 +7,7 @@ #include "CollectionInspectionWorkerThread.h" #include "CollectionSchemeIngestion.h" #include "CollectionSchemeIngestionList.h" +#include "DataFetchManagerAPITypes.h" #include "DecoderManifestIngestion.h" #include "ICollectionScheme.h" #include "ICollectionSchemeList.h" @@ -52,7 +53,8 @@ class IDecoderManifestTest : public DecoderManifestIngestion SyncID id, std::unordered_map> formatMap, std::unordered_map> signalToFrameAndNodeID, - std::unordered_map signalIDToPIDDecoderFormat + std::unordered_map signalIDToPIDDecoderFormat, + SignalIDToCustomSignalDecoderFormatMap signalIDToCustomDecoderFormat #ifdef FWE_FEATURE_VISION_SYSTEM_DATA , std::unordered_map complexSignalMap = @@ -65,6 +67,7 @@ class IDecoderManifestTest : public DecoderManifestIngestion , mFormatMap( std::move( formatMap ) ) , mSignalToFrameAndNodeID( std::move( signalToFrameAndNodeID ) ) , mSignalIDToPIDDecoderFormat( std::move( signalIDToPIDDecoderFormat ) ) + , mSignalIDToCustomDecoderFormat( std::move( signalIDToCustomDecoderFormat ) ) #ifdef FWE_FEATURE_VISION_SYSTEM_DATA , mComplexSignalMap( std::move( complexSignalMap ) ) , mComplexDataTypeMap( std::move( complexDataTypeMap ) ) @@ -109,6 +112,10 @@ class IDecoderManifestTest : public DecoderManifestIngestion { return VehicleDataSourceProtocol::OBD; } + else if ( signalID < 0x3000 ) + { + return VehicleDataSourceProtocol::CUSTOM_DECODING; + } #ifdef FWE_FEATURE_VISION_SYSTEM_DATA else if ( signalID < 0xFFFFFF00 ) { @@ -130,6 +137,16 @@ class IDecoderManifestTest : public DecoderManifestIngestion return NOT_FOUND_PID_DECODER_FORMAT; } + CustomSignalDecoderFormat + getCustomSignalDecoderFormat( SignalID signalID ) const + { + auto it = mSignalIDToCustomDecoderFormat.find( signalID ); + if ( it == mSignalIDToCustomDecoderFormat.end() ) + { + return INVALID_CUSTOM_SIGNAL_DECODER_FORMAT; + } + return it->second; + } SignalType getSignalType( SignalID signalID ) const override { @@ -173,6 +190,7 @@ class IDecoderManifestTest : public DecoderManifestIngestion std::unordered_map> mFormatMap; std::unordered_map> mSignalToFrameAndNodeID; std::unordered_map mSignalIDToPIDDecoderFormat; + SignalIDToCustomSignalDecoderFormatMap mSignalIDToCustomDecoderFormat; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA std::unordered_map mComplexSignalMap; std::unordered_map mComplexDataTypeMap; @@ -246,6 +264,53 @@ class ICollectionSchemeTest : public CollectionSchemeIngestion , root( root ) { } + ICollectionSchemeTest( SyncID collectionSchemeID, + SyncID decoderManifestID, + uint64_t startTime, + uint64_t expiryTime, + uint32_t minimumPublishIntervalMs, + uint32_t afterDurationMs, + bool activeDTCsIncluded, + bool triggerOnlyOnRisingEdge, + uint32_t priority, + bool persistNeeded, + bool compressionNeeded, + Signals_t collectSignals, + RawCanFrames_t collectRawCanFrames, + ExpressionNode *condition, + FetchInformation_t fetchInformations +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + , + PartialSignalIDLookup partialSignalLookup = PartialSignalIDLookup(), + S3UploadMetadata s3UploadMetadata = S3UploadMetadata() +#endif + ) + : CollectionSchemeIngestion( +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + std::make_shared() +#endif + ) + , collectionSchemeID( collectionSchemeID ) + , decoderManifestID( decoderManifestID ) + , startTime( startTime ) + , expiryTime( expiryTime ) + , minimumPublishIntervalMs( minimumPublishIntervalMs ) + , afterDurationMs( afterDurationMs ) + , activeDTCsIncluded( activeDTCsIncluded ) + , triggerOnlyOnRisingEdge( triggerOnlyOnRisingEdge ) + , priority( priority ) + , persistNeeded( persistNeeded ) + , compressionNeeded( compressionNeeded ) + , signals( collectSignals ) + , rawCanFrms( collectRawCanFrames ) +#ifdef FWE_FEATURE_VISION_SYSTEM_DATA + , partialSignalLookup( partialSignalLookup ) + , s3UploadMetadata( s3UploadMetadata ) +#endif + , root( condition ) + , fetchInformations( fetchInformations ) + { + } const SyncID & getCollectionSchemeID() const { @@ -266,6 +331,41 @@ class ICollectionSchemeTest : public CollectionSchemeIngestion { return expiryTime; } + uint32_t + getMinimumPublishIntervalMs() const override + { + return minimumPublishIntervalMs; + } + uint32_t + getAfterDurationMs() const override + { + return afterDurationMs; + } + bool + isActiveDTCsIncluded() const override + { + return activeDTCsIncluded; + } + bool + isTriggerOnlyOnRisingEdge() const override + { + return triggerOnlyOnRisingEdge; + } + uint32_t + getPriority() const override + { + return priority; + } + bool + isPersistNeeded() const override + { + return persistNeeded; + } + bool + isCompressionNeeded() const override + { + return compressionNeeded; + } const Signals_t & getCollectSignals() const { @@ -281,6 +381,11 @@ class ICollectionSchemeTest : public CollectionSchemeIngestion { return root; } + const FetchInformation_t & + getAllFetchInformations() const override + { + return fetchInformations; + } bool build() override { @@ -311,6 +416,13 @@ class ICollectionSchemeTest : public CollectionSchemeIngestion SyncID decoderManifestID; uint64_t startTime; uint64_t expiryTime; + uint32_t minimumPublishIntervalMs; + uint32_t afterDurationMs; + bool activeDTCsIncluded; + bool triggerOnlyOnRisingEdge; + uint32_t priority; + bool persistNeeded; + bool compressionNeeded; Signals_t signals; RawCanFrames_t rawCanFrms; #ifdef FWE_FEATURE_VISION_SYSTEM_DATA @@ -318,6 +430,7 @@ class ICollectionSchemeTest : public CollectionSchemeIngestion S3UploadMetadata s3UploadMetadata; #endif ExpressionNode *root; + FetchInformation_t fetchInformations; }; class ICollectionSchemeListTest : public CollectionSchemeIngestionList @@ -381,13 +494,14 @@ class OBDOverCANModuleMock : public OBDOverCANModule bool mUpdateFlag; }; -/* mock Collection Inspection Engine class that receive Inspection Matrix update from PM */ +/* mock Collection Inspection Engine class that receive Inspection Matrix and Fetch Matrix update from PM */ class CollectionInspectionWorkerThreadMock : public CollectionInspectionWorkerThread { public: CollectionInspectionWorkerThreadMock() : CollectionInspectionWorkerThread( mEngine ) , mInspectionMatrixUpdateFlag( false ) + , mFetchMatrixUpdateFlag( false ) { } ~CollectionInspectionWorkerThreadMock() @@ -400,20 +514,39 @@ class CollectionInspectionWorkerThreadMock : public CollectionInspectionWorkerTh mInspectionMatrixUpdateFlag = true; } void + onChangeFetchMatrix( const std::shared_ptr &fetchMatrix ) + { + static_cast( fetchMatrix ); + mFetchMatrixUpdateFlag = true; + } + void setInspectionMatrixUpdateFlag( bool flag ) { mInspectionMatrixUpdateFlag = flag; } + void + setFetchMatrixUpdateFlag( bool flag ) + { + mFetchMatrixUpdateFlag = flag; + } bool getInspectionMatrixUpdateFlag() { return mInspectionMatrixUpdateFlag; } + bool + getFetchMatrixUpdateFlag() + { + return mFetchMatrixUpdateFlag; + } private: CollectionInspectionEngine mEngine; // This flag is used for testing whether the listener received the Inspection Matrix update bool mInspectionMatrixUpdateFlag; + + // This flag is used for testing whether the listener received the Fetch Matrix update + bool mFetchMatrixUpdateFlag; }; } // namespace IoTFleetWise diff --git a/test/unit/support/CommandDispatcherMock.h b/test/unit/support/CommandDispatcherMock.h new file mode 100644 index 00000000..26bec1b1 --- /dev/null +++ b/test/unit/support/CommandDispatcherMock.h @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "ICommandDispatcher.h" +#include +#include +#include +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Testing +{ + +class CommandDispatcherMock : public ICommandDispatcher +{ +public: + CommandDispatcherMock() + : ICommandDispatcher() + { + } + + MOCK_METHOD( bool, init, () ); + MOCK_METHOD( void, + setActuatorValue, + ( const std::string &actuatorName, + const SignalValueWrapper &signalValue, + const CommandID &commandId, + Timestamp issuedTimestampMs, + Timestamp executionTimeoutMs, + NotifyCommandStatusCallback notifyStatusCallback ) ); + MOCK_METHOD( std::vector, getActuatorNames, () ); +}; + +} // namespace Testing +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/support/CommonAPIProxyMock.h b/test/unit/support/CommonAPIProxyMock.h new file mode 100644 index 00000000..0c7827fd --- /dev/null +++ b/test/unit/support/CommonAPIProxyMock.h @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#if !defined( COMMONAPI_INTERNAL_COMPILATION ) +#define COMMONAPI_INTERNAL_COMPILATION +#define HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#endif +#include +#ifdef HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#undef COMMONAPI_INTERNAL_COMPILATION +#undef HAS_DEFINED_COMMONAPI_INTERNAL_COMPILATION_HERE +#endif +#include + +class CommonAPIProxyMock : public CommonAPI::Proxy +{ +public: + MOCK_METHOD( const CommonAPI::Address &, getAddress, (), ( const ) ); + MOCK_METHOD( std::future, getCompletionFuture, () ); + MOCK_METHOD( bool, isAvailable, (), ( const ) ); + MOCK_METHOD( bool, isAvailableBlocking, (), ( const ) ); + MOCK_METHOD( CommonAPI::ProxyStatusEvent &, getProxyStatusEvent, () ); + MOCK_METHOD( CommonAPI::InterfaceVersionAttribute &, getInterfaceVersionAttribute, () ); +}; diff --git a/test/unit/support/ConnectivityModuleMock.h b/test/unit/support/ConnectivityModuleMock.h index 405c8f79..f4c00edf 100644 --- a/test/unit/support/ConnectivityModuleMock.h +++ b/test/unit/support/ConnectivityModuleMock.h @@ -20,12 +20,12 @@ class ConnectivityModuleMock : public IConnectivityModule MOCK_METHOD( bool, isAlive, (), ( const, override ) ); std::shared_ptr - createSender( const std::string &topicName, QoS publishQoS = QoS::AT_MOST_ONCE ) override + createSender() override { - return mockedCreateSender( topicName, publishQoS ); + return mockedCreateSender(); }; - MOCK_METHOD( std::shared_ptr, mockedCreateSender, ( const std::string &topicName, QoS publishQoS ) ); + MOCK_METHOD( std::shared_ptr, mockedCreateSender, () ); std::shared_ptr createReceiver( const std::string &topicName ) override diff --git a/test/unit/support/ExampleSomeipInterfaceProxyMock.h b/test/unit/support/ExampleSomeipInterfaceProxyMock.h new file mode 100644 index 00000000..cf845390 --- /dev/null +++ b/test/unit/support/ExampleSomeipInterfaceProxyMock.h @@ -0,0 +1,111 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "v1/commonapi/ExampleSomeipInterfaceProxy.hpp" +#include + +template +class ExampleSomeipInterfaceProxyMock : public v1::commonapi::ExampleSomeipInterfaceProxy<> +{ +public: + ExampleSomeipInterfaceProxyMock( std::shared_ptr proxy ) + : ExampleSomeipInterfaceProxy( proxy ){}; + MOCK_METHOD( const CommonAPI::Address &, getAddress, (), ( const ) ); + MOCK_METHOD( bool, isAvailable, (), ( const ) ); + MOCK_METHOD( bool, isAvailableBlocking, (), ( const ) ); + MOCK_METHOD( CommonAPI::ProxyStatusEvent &, getProxyStatusEvent, () ); + MOCK_METHOD( CommonAPI::InterfaceVersionAttribute &, getInterfaceVersionAttribute, () ); + MOCK_METHOD( std::future, getCompletionFuture, () ); + MOCK_METHOD( v1::commonapi::ExampleSomeipInterfaceProxyBase::XAttribute &, getXAttribute, () ); + MOCK_METHOD( v1::commonapi::ExampleSomeipInterfaceProxyBase::A1Attribute &, getA1Attribute, () ); + MOCK_METHOD( void, + setInt32, + ( int32_t _value, CommonAPI::CallStatus &_internalCallStatus, const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setInt32Async, + ( const int32_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxyBase::SetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setInt64Async, + ( const int64_t &_value, + v1::commonapi::ExampleSomeipInterfaceProxyBase::SetInt64AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setBooleanAsync, + ( const bool &_value, + v1::commonapi::ExampleSomeipInterfaceProxyBase::SetBooleanAsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setFloatAsync, + ( const float &_value, + v1::commonapi::ExampleSomeipInterfaceProxyBase::SetFloatAsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setDoubleAsync, + ( const double &_value, + v1::commonapi::ExampleSomeipInterfaceProxyBase::SetDoubleAsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setStringAsync, + ( const std::string &_value, + v1::commonapi::ExampleSomeipInterfaceProxyBase::SetStringAsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( void, + getInt32, + ( CommonAPI::CallStatus & _internalCallStatus, uint32_t &_value, const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + getInt32Async, + ( v1::commonapi::ExampleSomeipInterfaceProxyBase::GetInt32AsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( v1::commonapi::ExampleSomeipInterfaceProxyBase::NotifyLRCStatusEvent &, getNotifyLRCStatusEvent, () ); + MOCK_METHOD( std::future, + setInt32LongRunningAsync, + ( const std::string &_commandId, + const int32_t &_value, + SetInt32LongRunningAsyncCallback _callback, + const CommonAPI::CallInfo *_info ) ); +}; + +template +class CommonAPIObservableAttributeMock : public CommonAPI::ObservableAttribute +{ +public: + typedef typename CommonAPI::ObservableAttribute::ChangedEvent ChangedEvent; + MOCK_METHOD( ChangedEvent &, getChangedEvent, () ); + MOCK_METHOD( void, + getValue, + ( CommonAPI::CallStatus & _internalCallStatus, T &_value, const CommonAPI::CallInfo *_info ), + ( const ) ); + MOCK_METHOD( std::future, + getValueAsync, + ( std::function attributeAsyncCallback, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( void, + setValue, + ( const T &requestValue, + CommonAPI::CallStatus &_internalCallStatus, + T &responseValue, + const CommonAPI::CallInfo *_info ) ); + MOCK_METHOD( std::future, + setValueAsync, + ( const T &requestValue, + std::function attributeAsyncCallback, + const CommonAPI::CallInfo *_info ) ); +}; + +template +class CommonAPIObservableAttributeChangedEventMock : public CommonAPI::ObservableAttribute::ChangedEvent +{ +public: + MOCK_METHOD( void, onFirstListenerAdded, ( const std::function &_listener ) ); +}; + +template +class CommonAPIEventMock : public CommonAPI::Event +{ +public: + MOCK_METHOD( void, onFirstListenerAdded, ( const std::function &_listener ) ); +}; diff --git a/test/unit/support/SenderMock.h b/test/unit/support/SenderMock.h index 7a394267..a43cfdac 100644 --- a/test/unit/support/SenderMock.h +++ b/test/unit/support/SenderMock.h @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Aws @@ -21,6 +22,17 @@ namespace Testing class SenderMock : public ISender { public: + SenderMock() + { + TopicConfigArgs topicConfigArgs; + mTopicConfig = std::make_unique( "thing-name", topicConfigArgs ); + } + + SenderMock( const TopicConfig &topicConfig ) + : mTopicConfig( std::make_unique( topicConfig ) ) + { + } + struct SentBufferData { std::string data; @@ -32,30 +44,34 @@ class SenderMock : public ISender MOCK_METHOD( size_t, getMaxSendSize, (), ( const, override ) ); void - sendBuffer( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) override + sendBuffer( const std::string &topic, + const std::uint8_t *buf, + size_t size, + OnDataSentCallback callback, + QoS qos = QoS::AT_LEAST_ONCE ) override { + static_cast( qos ); std::lock_guard lock( mSentBufferDataMutex ); - mSentBufferData.push_back( SentBufferData{ std::string( buf, buf + size ), callback } ); - mockedSendBuffer( buf, size, callback ); + mSentBufferData[topic].push_back( SentBufferData{ std::string( buf, buf + size ), callback } ); + mockedSendBuffer( topic, buf, size, callback ); } - void - sendBufferToTopic( __attribute__( ( unused ) ) const std::string &topic, - const std::uint8_t *buf, - size_t size, - OnDataSentCallback callback ) override + std::unordered_map> + getSentBufferData() { - // TODO: Make a map of topic to SentBufferData. For now, mark topic parameter as unused std::lock_guard lock( mSentBufferDataMutex ); - mSentBufferData.push_back( SentBufferData{ std::string( buf, buf + size ), callback } ); - mockedSendBuffer( buf, size, callback ); + return mSentBufferData; } std::vector - getSentBufferData() + getSentBufferDataByTopic( const std::string &topic ) { std::lock_guard lock( mSentBufferDataMutex ); - return mSentBufferData; + if ( mSentBufferData.find( topic ) == mSentBufferData.end() ) + { + return {}; + } + return mSentBufferData[topic]; } void @@ -65,14 +81,23 @@ class SenderMock : public ISender mSentBufferData.clear(); } - MOCK_METHOD( void, mockedSendBuffer, ( const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) ); + MOCK_METHOD( void, + mockedSendBuffer, + ( const std::string &topic, const std::uint8_t *buf, size_t size, OnDataSentCallback callback ) ); MOCK_METHOD( unsigned, getPayloadCountSent, (), ( const, override ) ); + const TopicConfig & + getTopicConfig() const + { + return *mTopicConfig; + } + private: // Record the calls so that we can wait for asynchronous calls to happen. - std::vector mSentBufferData; + std::unordered_map> mSentBufferData; std::mutex mSentBufferDataMutex; + std::unique_ptr mTopicConfig; }; } // namespace Testing diff --git a/test/unit/support/SomeipMock.h b/test/unit/support/SomeipMock.h new file mode 100644 index 00000000..9192a62c --- /dev/null +++ b/test/unit/support/SomeipMock.h @@ -0,0 +1,310 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +class SomeipApplicationMock : public vsomeip::application +{ +public: + MOCK_METHOD( const std::string &, get_name, (), ( const ) ); + MOCK_METHOD( vsomeip_v3::client_t, get_client, (), ( const ) ); + MOCK_METHOD( vsomeip_v3::diagnosis_t, get_diagnosis, (), ( const ) ); + MOCK_METHOD( bool, init, () ); + MOCK_METHOD( void, start, () ); + MOCK_METHOD( void, stop, () ); + MOCK_METHOD( void, process, ( int _number ) ); + MOCK_METHOD( vsomeip_v3::security_mode_e, get_security_mode, (), ( const ) ); + MOCK_METHOD( void, + stop_offer_service, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::major_version_t, + vsomeip_v3::minor_version_t ), + ( override ) ); + + MOCK_METHOD( void, + offer_event, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::event_t, + const std::set &, + vsomeip_v3::event_type_e, + std::chrono::milliseconds, + bool, + bool, + const vsomeip_v3::epsilon_change_func_t &, + vsomeip_v3::reliability_type_e ), + ( override ) ); + + MOCK_METHOD( void, + unregister_subscription_handler, + ( vsomeip_v3::service_t _service, vsomeip_v3::instance_t _instance, vsomeip_v3::eventgroup_t ), + ( override ) ); + MOCK_METHOD( void, + offer_service, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::major_version_t, + vsomeip_v3::minor_version_t ), + ( override ) ); + + MOCK_METHOD( void, + stop_offer_event, + ( vsomeip_v3::service_t _service, vsomeip_v3::instance_t _instance, vsomeip_v3::event_t ), + ( override ) ); + + MOCK_METHOD( void, + request_service, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::major_version_t, + vsomeip_v3::minor_version_t ), + ( override ) ); + + MOCK_METHOD( void, + release_service, + ( vsomeip_v3::service_t _service, vsomeip_v3::instance_t _instance ), + ( override ) ); + + MOCK_METHOD( void, + request_event, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::event_t, + const std::set &, + vsomeip_v3::event_type_e, + vsomeip_v3::reliability_type_e ), + ( override ) ); + + MOCK_METHOD( void, + release_event, + ( vsomeip_v3::service_t _service, vsomeip_v3::instance_t _instance, vsomeip_v3::event_t ), + ( override ) ); + + MOCK_METHOD( void, + subscribe, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::eventgroup_t, + vsomeip_v3::major_version_t, + vsomeip_v3::event_t ), + ( override ) ); + + MOCK_METHOD( void, + unsubscribe, + ( vsomeip_v3::service_t _service, vsomeip_v3::instance_t _instance, vsomeip_v3::eventgroup_t ), + ( override ) ); + + MOCK_METHOD( bool, + is_available, + ( vsomeip_v3::service_t _service, + vsomeip_v3::instance_t _instance, + vsomeip_v3::major_version_t, + vsomeip_v3::minor_version_t ), + ( const, override ) ); + + MOCK_METHOD( bool, + are_available, + ( vsomeip::application::available_t &, + vsomeip::service_t, + vsomeip::instance_t, + vsomeip::major_version_t, + vsomeip::minor_version_t ), + ( const ) ); + + MOCK_METHOD( void, send, (std::shared_ptr)); + + MOCK_METHOD( void, + notify, + (vsomeip::service_t, vsomeip::instance_t, vsomeip::event_t, std::shared_ptr, bool), + ( const ) ); + + MOCK_METHOD( void, clear_all_handler, (), ( override ) ); + MOCK_METHOD( void, + register_message_handler, + (vsomeip::service_t, vsomeip::instance_t, vsomeip::method_t, const vsomeip::message_handler_t &), + ( override ) ); + MOCK_METHOD( void, + notify_one, + (vsomeip::service_t, + vsomeip::instance_t, + vsomeip::event_t, + std::shared_ptr, + vsomeip::client_t, + bool), + ( const ) ); + + MOCK_METHOD( void, + register_availability_handler, + ( vsomeip::service_t, + vsomeip::instance_t, + const vsomeip::availability_handler_t &, + vsomeip::major_version_t, + vsomeip::minor_version_t ), + ( override ) ); + + MOCK_METHOD( + void, + register_subscription_handler, + (vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, const vsomeip::subscription_handler_t &), + ( override ) ); + + MOCK_METHOD( + void, + register_async_subscription_handler, + (vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, const vsomeip::async_subscription_handler_t &), + ( override ) ); + + MOCK_METHOD( bool, is_routing, (), ( const override ) ); + + MOCK_METHOD( void, + unsubscribe, + ( vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, vsomeip::event_t ), + ( override ) ); + + MOCK_METHOD( void, + register_subscription_status_handler, + (vsomeip::service_t, + vsomeip::instance_t, + vsomeip::eventgroup_t, + vsomeip::event_t, + const vsomeip::subscription_status_handler_t &, + bool)); + + MOCK_METHOD( void, + unregister_subscription_status_handler, + ( vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, vsomeip::event_t ), + ( override ) ); + + MOCK_METHOD( void, + unregister_availability_handler, + ( vsomeip::service_t, vsomeip::instance_t, vsomeip::major_version_t, vsomeip::minor_version_t ), + ( override ) ); + + MOCK_METHOD( void, + register_subscription_status_handler, + (vsomeip::service_t, + vsomeip::instance_t, + vsomeip::eventgroup_t, + vsomeip::event_t, + vsomeip::subscription_status_handler_t, + bool), + ( override ) ); + + MOCK_METHOD( void, + subscribe_with_debounce, + (vsomeip::service_t, + vsomeip::instance_t, + vsomeip::eventgroup_t, + vsomeip::major_version_t, + vsomeip::event_t, + const vsomeip_v3::debounce_filter_t &), + ( override ) ); + + MOCK_METHOD( void, + register_message_acceptance_handler, + ( const vsomeip::message_acceptance_handler_t &_handler ), + ( override ) ); + + MOCK_METHOD( (std::map), get_additional_data, (const std::string &), ( override ) ); + + MOCK_METHOD( void, + register_availability_handler, + ( vsomeip::service_t, + vsomeip::instance_t, + const vsomeip::availability_state_handler_t &, + vsomeip::major_version_t, + vsomeip::minor_version_t ), + ( override ) ); + + MOCK_METHOD( + void, + register_subscription_handler, + (vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, const vsomeip::subscription_handler_sec_t &), + ( override ) ); + + MOCK_METHOD( + void, + register_async_subscription_handler, + ( vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, vsomeip::async_subscription_handler_sec_t ), + ( override ) ); + + MOCK_METHOD( void, + set_sd_acceptance_required, + (const vsomeip::remote_info_t &, const std::string &, bool), + ( override ) ); + + MOCK_METHOD( void, set_sd_acceptance_required, (const sd_acceptance_map_type_t &, bool), ( override ) ); + + MOCK_METHOD( void, register_sd_acceptance_handler, (const vsomeip::sd_acceptance_handler_t &), ( override ) ); + + MOCK_METHOD( void, + register_reboot_notification_handler, + (const vsomeip::reboot_notification_handler_t &), + ( override ) ); + + MOCK_METHOD( void, register_routing_ready_handler, (const vsomeip::routing_ready_handler_t &), ( override ) ); + + MOCK_METHOD( void, register_routing_state_handler, (const vsomeip::routing_state_handler_t &), ( override ) ); + + MOCK_METHOD( bool, + update_service_configuration, + (vsomeip::service_t, vsomeip::instance_t, uint16_t, bool, bool, bool), + ( override ) ); + + MOCK_METHOD( void, + update_security_policy_configuration, + (uint32_t, + uint32_t, + std::shared_ptr, + std::shared_ptr, + const vsomeip::security_update_handler_t &), + ( override ) ); + + MOCK_METHOD( void, + remove_security_policy_configuration, + (uint32_t, uint32_t, const vsomeip::security_update_handler_t &), + ( override ) ); + + MOCK_METHOD( + void, + register_subscription_handler, + (vsomeip::service_t, vsomeip::instance_t, vsomeip::eventgroup_t, const vsomeip::subscription_handler_ext_t &), + ( override ) ); + + MOCK_METHOD( void, + register_async_subscription_handler, + (vsomeip::service_t, + vsomeip::instance_t, + vsomeip::eventgroup_t, + const vsomeip::async_subscription_handler_ext_t &), + ( override ) ); + + MOCK_METHOD( void, register_state_handler, (const vsomeip::state_handler_t &)); + MOCK_METHOD( void, unregister_state_handler, () ); + MOCK_METHOD( void, unregister_message_handler, ( vsomeip::service_t, vsomeip::instance_t, vsomeip::method_t ) ); + + MOCK_METHOD( bool, is_routing, () ); + MOCK_METHOD( void, set_routing_state, ( vsomeip::routing_state_e ) ); + MOCK_METHOD( void, + get_offered_services_async, + (vsomeip::offer_type_e, const vsomeip::offered_services_handler_t &)); + MOCK_METHOD( void, set_watchdog_handler, ( const vsomeip::watchdog_handler_t &, std::chrono::seconds ) ); + + MOCK_METHOD( sd_acceptance_map_type_t, get_sd_acceptance_required, () ); + + MOCK_METHOD( void, + register_message_handler_ext, + ( vsomeip::service_t, + vsomeip::instance_t, + vsomeip::method_t, + const vsomeip::message_handler_t &, + vsomeip::handler_registration_type_e ) ); + + MOCK_METHOD( std::shared_ptr, get_configuration, (), ( const ) ); + + MOCK_METHOD( std::shared_ptr, get_policy_manager, (), ( const ) ); +}; diff --git a/test/unit/support/StreamForwarderMock.h b/test/unit/support/StreamForwarderMock.h new file mode 100644 index 00000000..6e66fa97 --- /dev/null +++ b/test/unit/support/StreamForwarderMock.h @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "StreamForwarder.h" +#include "StreamManagerMock.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Testing +{ + +class StreamForwarderMock : public Aws::IoTFleetWise::Store::StreamForwarder +{ +public: + explicit StreamForwarderMock( std::shared_ptr streamManager, + std::shared_ptr dataSender ) + : StreamForwarder( streamManager, dataSender, nullptr ){}; + + MOCK_METHOD( void, registerJobCompletionCallback, ( JobCompletionCallback callback ), ( override ) ); +}; + +} // namespace Testing +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/support/StreamManagerMock.h b/test/unit/support/StreamManagerMock.h new file mode 100644 index 00000000..916ce751 --- /dev/null +++ b/test/unit/support/StreamManagerMock.h @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "StreamManager.h" +#include +#include + +namespace Aws +{ +namespace IoTFleetWise +{ +namespace Testing +{ + +class StreamManagerMock : public Aws::IoTFleetWise::Store::StreamManager +{ +public: + explicit StreamManagerMock( std::shared_ptr protoWriter ) + : StreamManager( "", protoWriter, 0 ){}; + + MOCK_METHOD( Store::StreamManager::ReturnCode, appendToStreams, ( const TriggeredCollectionSchemeData &data ) ); + + MOCK_METHOD( bool, hasCampaign, ( const Aws::IoTFleetWise::Store::CampaignName &campaignName ) ); +}; + +} // namespace Testing +} // namespace IoTFleetWise +} // namespace Aws diff --git a/test/unit/support/static-config-inline-creds.json b/test/unit/support/static-config-inline-creds.json index 01621eee..86d81837 100644 --- a/test/unit/support/static-config-inline-creds.json +++ b/test/unit/support/static-config-inline-creds.json @@ -48,20 +48,9 @@ "mqttConnection": { "endpointUrl": "my-endpoint.my-region.amazonaws.com", "clientId": "ClientId", - "collectionSchemeListTopic": "collection-scheme-list-topic", - "decoderManifestTopic": "decoder-manifest-topic", - "canDataTopic": "can-data", - "checkinTopic": "checkin", "certificate": "MY_INLINE_CERTIFICATE", "privateKey": "MY_INLINE_PRIVATE_KEY", "rootCA": "MY_INLINE_ROOT_CA" - }, - "iWaveGpsExample": { - "nmeaFilePath": "/tmp/engineTestIWaveGPSfile.txt", - "canChannel": "IWAVE-GPS-CAN", - "canFrameId": "1", - "longitudeStartBit": "32", - "latitudeStartBit": "0" } } } diff --git a/test/unit/support/static-config-ok.json b/test/unit/support/static-config-ok.json index 94bcfc20..14c0d715 100644 --- a/test/unit/support/static-config-ok.json +++ b/test/unit/support/static-config-ok.json @@ -33,6 +33,76 @@ }, "interfaceId": "10", "type": "ros2Interface" + }, + { + "someipToCanBridgeInterface": { + "someipServiceId": "0x7777", + "someipInstanceId": "0x5678", + "someipEventId": "0x8778", + "someipEventGroupId": "0x5555", + "someipApplicationName": "someipToCanBridgeInterface" + }, + "interfaceId": "3", + "type": "someipToCanBridgeInterface" + }, + { + "someipCollectionInterface": { + "someipApplicationName": "someipCollectionInterface", + "cyclicUpdatePeriodMs": 200 + }, + "interfaceId": "SOMEIP", + "type": "someipCollectionInterface" + }, + { + "someipCommandInterface": { + "someipApplicationName": "someipCommandInterface" + }, + "interfaceId": "SOMEIP", + "type": "someipCommandInterface" + }, + { + "interfaceId": "NAMED_SIGNAL", + "type": "namedSignalInterface" + }, + { + "interfaceId": "AAOS-VHAL", + "type": "aaosVhalInterface" + }, + { + "iWaveGpsInterface": { + "latitudeSignalName": "Vehicle.CurrentLocation.Latitude", + "longitudeSignalName": "Vehicle.CurrentLocation.Longitude", + "nmeaFilePath": "/dev/null", + "pollIntervalMs": 123 + }, + "interfaceId": "LOCATION", + "type": "iWaveGpsInterface" + }, + { + "externalGpsInterface": { + "latitudeSignalName": "Vehicle.CurrentLocation.Latitude", + "longitudeSignalName": "Vehicle.CurrentLocation.Longitude" + }, + "interfaceId": "LOCATION", + "type": "externalGpsInterface" + }, + { + "interfaceId": "UDS_DTC", + "type": "exampleUDSInterface", + "exampleUDSInterface": { + "configs": [ + { + "targetAddress": "0x01", + "name": "ECM", + "can": { + "interfaceName": "vcan0", + "functionalAddress": "0x7DF", + "physicalRequestID": "0x7E0", + "physicalResponseID": "0x7E8" + } + } + ] + } } ], "staticConfig": { @@ -55,7 +125,8 @@ "readyToPublishDataBufferSize": 10000, "systemWideLogLevel": "Trace", "persistencyUploadRetryIntervalMs": 5000, - "maximumAwsSdkHeapMemoryBytes": 10000000 + "maximumAwsSdkHeapMemoryBytes": 10000000, + "minFetchTriggerIntervalMs": 1000 }, "publishToCloudParameters": { "maxPublishMessageCount": 1000, @@ -64,23 +135,12 @@ "mqttConnection": { "endpointUrl": "my-endpoint.my-region.amazonaws.com", "clientId": "ClientId", - "collectionSchemeListTopic": "collection-scheme-list-topic", - "decoderManifestTopic": "decoder-manifest-topic", "metricsUploadTopic": "aws-iot-fleetwise-metrics-upload", "loggingUploadTopic": "aws-iot-fleetwise-logging-upload", - "canDataTopic": "can-data", - "checkinTopic": "checkin", "certificateFilename": "/tmp/dummyCertificate.pem", "privateKeyFilename": "/tmp/dummyPrivateKey.key", "rootCAFilename": "/tmp/dummyCertificate.pem" }, - "iWaveGpsExample": { - "nmeaFilePath": "/tmp/engineTestIWaveGPSfile.txt", - "canChannel": "IWAVE-GPS-CAN", - "canFrameId": "1", - "longitudeStartBit": "32", - "latitudeStartBit": "0" - }, "remoteProfilerDefaultValues": { "loggingUploadLevelThreshold": "Warning", "metricsUploadIntervalMs": 60000, diff --git a/tools/android-app/README.md b/tools/android-app/README.md index b3ab78e6..e420e0b5 100644 --- a/tools/android-app/README.md +++ b/tools/android-app/README.md @@ -1,5 +1,13 @@ # Android App for AWS IoT FleetWise + +> [!NOTE] +> This guide makes use of "gated" features of AWS IoT FleetWise for which you will need to request +> access. See +> [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for +> more information, or contact the +> [AWS Support Center](https://console.aws.amazon.com/support/home#/). + This app demonstrates AWS IoT FleetWise using an Android smartphone or Android Automotive. - [Android Smartphone User guide](#android-smartphone-user-guide) @@ -8,6 +16,22 @@ This app demonstrates AWS IoT FleetWise using an Android smartphone or Android A ## Android Smartphone User guide +**Prerequisites:** + +- An Android 5.0+ smartphone. +- An [ELM327 Bluetooth OBD adapter](https://www.amazon.com/s?k=elm327+bluetooth). +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. + This guide demonstrates AWS IoT FleetWise using a smartphone with Android 5.0+ and a commonly available [ELM327 Bluetooth OBD adapter](https://www.amazon.com/s?k=elm327+bluetooth). @@ -21,7 +45,7 @@ available [ELM327 Bluetooth OBD adapter](https://www.amazon.com/s?k=elm327+bluet 1. Open the AWS CloudShell: [Launch CloudShell](https://console.aws.amazon.com/cloudshell/home) -1. Run the following command to clone the FWE repo from GitHub: +1. Copy and paste the following command to clone the latest FWE source code from GitHub. ```bash git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge @@ -34,7 +58,9 @@ available [ELM327 Bluetooth OBD adapter](https://www.amazon.com/s?k=elm327+bluet ```bash cd ~/aws-iot-fleetwise-edge/tools/android-app/cloud \ && pip3 install segno \ - && ./provision.sh --s3-qr-code + && ./provision.sh \ + --region us-east-1 \ + --s3-qr-code ``` 1. When the script completes, the path to a QR code image file is given: @@ -74,13 +100,14 @@ available [ELM327 Bluetooth OBD adapter](https://www.amazon.com/s?k=elm327+bluet ```bash ../../cloud/demo.sh \ + --region us-east-1 \ --vehicle-name `cat config/vehicle-name.txt` \ --node-file ../../cloud/obd-nodes.json \ - --node-file externalGpsNodes.json\ + --node-file ../../cloud/custom-nodes-location.json \ --decoder-file ../../cloud/obd-decoders.json \ - --decoder-file externalGpsDecoders.json \ + --decoder-file ../../cloud/custom-decoders-location.json \ --network-interface-file ../../cloud/network-interface-obd.json \ - --network-interface-file network-interface-can-external-gps.json \ + --network-interface-file ../../cloud/network-interface-custom-location.json \ --campaign-file campaign-android-obd.json ``` @@ -100,6 +127,7 @@ available [ELM327 Bluetooth OBD adapter](https://www.amazon.com/s?k=elm327+bluet ../../cloud/clean-up.sh \ && ../../provision.sh \ --vehicle-name `cat config/vehicle-name.txt` \ + --region us-east-1 \ --only-clean-up ``` @@ -113,6 +141,17 @@ privileged VHAL properties. To demonstrate the app accessing privileged VHAL pro **Prerequisites:** +- Access to an AWS Account with administrator privileges. +- Your AWS account has access to AWS IoT FleetWise "gated" features. See + [here](https://docs.aws.amazon.com/iot-fleetwise/latest/developerguide/fleetwise-regions.html) for + more information, or contact the + [AWS Support Center](https://console.aws.amazon.com/support/home#/). +- Logged in to the AWS Console in the `us-east-1` region using the account with administrator + privileges. + - Note: if you would like to use a different region you will need to change `us-east-1` to your + desired region in each place that it is mentioned below. + - Note: AWS IoT FleetWise is currently available in + [these](https://docs.aws.amazon.com/general/latest/gr/iotfleetwise.html) regions. - A local x86_64 Ubuntu 20.04 machine with the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed. @@ -147,7 +186,8 @@ privileged VHAL properties. To demonstrate the app accessing privileged VHAL pro ```bash git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge \ && cd ~/aws-iot-fleetwise-edge/tools/android-app/cloud \ - && ./provision.sh + && ./provision.sh \ + --region us-east-1 ``` 1. Run the following to configure the app with the credentials. This will cause the app to connect @@ -170,13 +210,14 @@ privileged VHAL properties. To demonstrate the app accessing privileged VHAL pro ```bash sudo -H ../../cloud/install-deps.sh \ && ../../cloud/demo.sh \ + --region us-east-1 \ --vehicle-name `cat config/vehicle-name.txt` \ - --node-file externalGpsNodes.json\ - --node-file aaosVhalNodes.json \ - --decoder-file externalGpsDecoders.json \ - --decoder-file aaosVhalDecoders.json \ - --network-interface-file network-interface-can-external-gps.json \ - --network-interface-file network-interface-can-aaos-vhal.json \ + --node-file ../../cloud/custom-nodes-location.json \ + --node-file custom-nodes-aaos-vhal.json \ + --decoder-file ../../cloud/custom-decoders-location.json \ + --decoder-file custom-decoders-aaos-vhal.json \ + --network-interface-file ../../cloud/network-interface-custom-location.json \ + --network-interface-file network-interface-custom-aaos-vhal.json \ --campaign-file campaign-android-aaos-vhal.json ``` @@ -196,6 +237,7 @@ privileged VHAL properties. To demonstrate the app accessing privileged VHAL pro ../../cloud/clean-up.sh \ && ../../provision.sh \ --vehicle-name `cat config/vehicle-name.txt` \ + --region us-east-1 \ --only-clean-up ``` diff --git a/tools/android-app/app/src/main/assets/config-0.json b/tools/android-app/app/src/main/assets/config-0.json index f2ed98ec..5674fe7f 100644 --- a/tools/android-app/app/src/main/assets/config-0.json +++ b/tools/android-app/app/src/main/assets/config-0.json @@ -11,6 +11,18 @@ }, "interfaceId": "0", "type": "obdInterface" + }, + { + "externalGpsInterface": { + "latitudeSignalName": "Vehicle.CurrentLocation.Latitude", + "longitudeSignalName": "Vehicle.CurrentLocation.Longitude" + }, + "interfaceId": "LOCATION", + "type": "externalGpsInterface" + }, + { + "interfaceId": "AAOS-VHAL", + "type": "aaosVhalInterface" } ], "staticConfig": { @@ -40,10 +52,6 @@ "mqttConnection": { "endpointUrl": "MQTT_ENDPOINT_GOES_HERE", "clientId": "VEHICLE_ID_GOES_HERE", - "collectionSchemeListTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/collection_schemes", - "decoderManifestTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/decoder_manifests", - "canDataTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/signals", - "checkinTopic": "$aws/iotfleetwise/vehicles/VEHICLE_ID_GOES_HERE/checkins", "certificate": "CERTIFICATE_GOES_HERE", "privateKey": "PRIVATE_KEY_GOES_HERE", "rootCA": "ROOT_CA_GOES_HERE" diff --git a/tools/android-app/app/src/main/java/com/aws/iotfleetwise/Fwe.java b/tools/android-app/app/src/main/java/com/aws/iotfleetwise/Fwe.java index a2202fdd..cf188fff 100644 --- a/tools/android-app/app/src/main/java/com/aws/iotfleetwise/Fwe.java +++ b/tools/android-app/app/src/main/java/com/aws/iotfleetwise/Fwe.java @@ -5,6 +5,9 @@ import android.content.res.AssetManager; +import java.lang.Double; +import java.util.Map; + public class Fwe { /** * Run FWE. This will block until the `stop` method is called. @@ -51,6 +54,21 @@ public native static int run( */ public native static void ingestCanMessage(String interfaceId, long timestamp, int messageId, byte[] data); + /** + * Ingest signal value by name + * @param timestamp Timestamp of the signal value in milliseconds since the epoch, or zero to use the system time + * @param name Signal name + * @param value Signal value + */ + public native static void ingestSignalValueByName(long timestamp, String name, Object value); + + /** + * Ingest multiple signal values by name + * @param timestamp Timestamp of the signal value in milliseconds since the epoch, or zero to use the system time + * @param values Signal values + */ + public native static void ingestMultipleSignalValuesByName(long timestamp, Map values); + /** * Set the GPS location * @param latitude Latitude diff --git a/tools/android-app/app/src/main/java/com/aws/iotfleetwise/FweApplication.java b/tools/android-app/app/src/main/java/com/aws/iotfleetwise/FweApplication.java index 98dbd9af..d090ae08 100644 --- a/tools/android-app/app/src/main/java/com/aws/iotfleetwise/FweApplication.java +++ b/tools/android-app/app/src/main/java/com/aws/iotfleetwise/FweApplication.java @@ -344,6 +344,10 @@ private void serviceCarProperties() double val = ((Number)propVal.getValue()).doubleValue(); sb.append(val); Fwe.setVehicleProperty(signalId, val); + } else if (clazz.equals(Long.class)) { + long val = ((Number)propVal.getValue()).longValue(); + sb.append(val); + Fwe.setVehicleProperty(signalId, val); } else if (clazz.equals(Integer[].class) || clazz.equals(Long[].class)) { sb.append("["); for (int resultIndex = 0; resultIndex < Array.getLength(propVal.getValue()); resultIndex++) { @@ -354,7 +358,7 @@ private void serviceCarProperties() break; } } - double val = ((Number)Array.get(propVal.getValue(), resultIndex)).doubleValue(); + long val = ((Number)Array.get(propVal.getValue(), resultIndex)).longValue(); if (resultIndex > 0) { sb.append(", "); } diff --git a/tools/android-app/cloud/aaosVhalDecoders.json b/tools/android-app/cloud/aaosVhalDecoders.json deleted file mode 100644 index 382f28b5..00000000 --- a/tools/android-app/cloud/aaosVhalDecoders.json +++ /dev/null @@ -1,8514 +0,0 @@ -[ - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_MODEL_YEAR", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289407235, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_CAPACITY", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504388, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472773, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472773, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472773, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472773, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_BATTERY_CAPACITY", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504390, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472775, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472775, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472775, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472775, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_DOOR_LOCATION", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289407240, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_PORT_LOCATION", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289407241, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_DRIVER_SEAT", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356516106, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 4 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 5 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 6 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472779, - "factor": 1, - "length": 7 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_MULTI_EV_PORT_LOCATIONS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472780, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.INFO_MULTI_EV_PORT_LOCATIONS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289472780, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PERF_ODOMETER", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504644, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PERF_VEHICLE_SPEED", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504647, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PERF_VEHICLE_SPEED_DISPLAY", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504648, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PERF_STEERING_ANGLE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504649, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PERF_REAR_STEERING_ANGLE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504656, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.ENGINE_COOLANT_TEMP", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504897, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.ENGINE_OIL_LEVEL", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289407747, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.ENGINE_OIL_TEMP", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504900, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.ENGINE_RPM", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504901, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 290521862, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 290521862, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 290521862, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 290521862, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 290521862, - "factor": 1, - "length": 4 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FUEL_LEVEL", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504903, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FUEL_DOOR_OPEN", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310600, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.EV_BATTERY_LEVEL", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504905, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.EV_CHARGE_PORT_OPEN", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310602, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.EV_CHARGE_PORT_CONNECTED", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310603, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504908, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.RANGE_REMAINING", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291504904, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 392168201, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 392168201, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 392168201, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 392168201, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.GEAR_SELECTION", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408000, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.CURRENT_GEAR", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408001, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PARKING_BRAKE_ON", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310850, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.PARKING_BRAKE_AUTO_APPLY", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310851, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FUEL_LEVEL_LOW", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310853, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.NIGHT_MODE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310855, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TURN_SIGNAL_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408008, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.IGNITION_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408009, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.ABS_ACTIVE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310858, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TRACTION_CONTROL_ACTIVE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287310859, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356517120, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356517121, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 358614274, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 358614275, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_9", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 9, - "offset": 320865540, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419973, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419974, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419975, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419976, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419977, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419978, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356517131, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SIDE_MIRROR_HEAT_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 339739916, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SIDE_MIRROR_HEAT_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 339739916, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SIDE_MIRROR_HEAT_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 339739916, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_STEERING_WHEEL_HEAT", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408269, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_DISPLAY_UNITS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408270, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356517135, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419984, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356582673, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356582673, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356582673, - "factor": 1, - "length": 2 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356582673, - "factor": 1, - "length": 3 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354419986, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356517139, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_9", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 9, - "offset": 320865556, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DISTANCE_DISPLAY_UNITS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408512, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FUEL_VOLUME_DISPLAY_UNITS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408513, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_DISPLAY_UNITS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408514, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.EV_BATTERY_DISPLAY_UNITS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408515, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287311364, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.VEHICLE_SPEED_DISPLAY_UNITS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289408517, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.ENV_OUTSIDE_TEMPERATURE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 291505923, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REQ_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289475072, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REQ_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289475072, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REPORT_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289475073, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REPORT_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289475073, - "factor": 1, - "length": 1 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_BOOTUP_REASON", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289409538, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DISPLAY_BRIGHTNESS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289409539, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 373295872, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 373295873, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 371198722, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 339741504, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 339741504, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 339741504, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 339741505, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 339741505, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 339741505, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 339741506, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 339741506, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 339741506, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 339741507, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 339741507, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 339741507, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_LOCK", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287312708, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.MIRROR_FOLD", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 287312709, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518784, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518785, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 354421634, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518787, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518788, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518789, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518790, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518791, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518792, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518793, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518794, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518795, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518796, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518797, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518798, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518799, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518800, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518801, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518802, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518803, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518804, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_POS", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289409941, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518806, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518807, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518808, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518809, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518810, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356518832, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_9", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 9, - "offset": 322964416, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_9", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 9, - "offset": 322964417, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_9", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 9, - "offset": 320867268, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HEADLIGHTS_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410560, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HIGH_BEAM_LIGHTS_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410561, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FOG_LIGHTS_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410562, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HAZARD_LIGHTS_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410563, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HEADLIGHTS_SWITCH", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410576, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HIGH_BEAM_LIGHTS_SWITCH", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410577, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.FOG_LIGHTS_SWITCH", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410578, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.HAZARD_LIGHTS_SWITCH", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410579, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.CABIN_LIGHTS_STATE", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410817, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.CABIN_LIGHTS_SWITCH", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 289410818, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356519683, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_0", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_1", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 1, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_2", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 2, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_3", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 3, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_4", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 4, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_5", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 5, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_6", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 6, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_7", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 7, - "offset": 356519684, - "factor": 1, - "length": 0 - } - }, - { - "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_8", - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 8, - "offset": 356519684, - "factor": 1, - "length": 0 - } - } -] diff --git a/tools/android-app/cloud/campaign-android-aaos-vhal.json b/tools/android-app/cloud/campaign-android-aaos-vhal.json index f995fcd7..b9c6e81a 100644 --- a/tools/android-app/cloud/campaign-android-aaos-vhal.json +++ b/tools/android-app/cloud/campaign-android-aaos-vhal.json @@ -62,6 +62,21 @@ { "name": "Vehicle.VHAL.TIRE_PRESSURE_3" }, + { + "name": "Vehicle.VHAL.WHEEL_TICK_0" + }, + { + "name": "Vehicle.VHAL.WHEEL_TICK_1" + }, + { + "name": "Vehicle.VHAL.WHEEL_TICK_2" + }, + { + "name": "Vehicle.VHAL.WHEEL_TICK_3" + }, + { + "name": "Vehicle.VHAL.WHEEL_TICK_4" + }, { "name": "Vehicle.VHAL.GEAR_SELECTION" }, diff --git a/tools/android-app/cloud/custom-decoders-aaos-vhal.json b/tools/android-app/cloud/custom-decoders-aaos-vhal.json new file mode 100644 index 00000000..163b7abf --- /dev/null +++ b/tools/android-app/cloud/custom-decoders-aaos-vhal.json @@ -0,0 +1,4866 @@ +[ + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_MODEL_YEAR", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289407235:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_CAPACITY", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504388:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472773:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472773:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472773:0:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_TYPE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472773:0:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_BATTERY_CAPACITY", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504390:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472775:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472775:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472775:0:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_CONNECTOR_TYPE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472775:0:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_FUEL_DOOR_LOCATION", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289407240:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EV_PORT_LOCATION", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289407241:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_DRIVER_SEAT", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356516106:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:4" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:5" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:6" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_EXTERIOR_DIMENSIONS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472779:0:7" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_MULTI_EV_PORT_LOCATIONS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472780:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.INFO_MULTI_EV_PORT_LOCATIONS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289472780:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PERF_ODOMETER", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504644:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PERF_VEHICLE_SPEED", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504647:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PERF_VEHICLE_SPEED_DISPLAY", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504648:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PERF_STEERING_ANGLE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504649:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PERF_REAR_STEERING_ANGLE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504656:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.ENGINE_COOLANT_TEMP", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504897:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.ENGINE_OIL_LEVEL", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289407747:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.ENGINE_OIL_TEMP", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504900:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.ENGINE_RPM", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504901:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "290521862:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "290521862:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "290521862:0:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "290521862:0:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "290521862:0:4" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FUEL_LEVEL", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504903:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FUEL_DOOR_OPEN", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310600:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.EV_BATTERY_LEVEL", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504905:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.EV_CHARGE_PORT_OPEN", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310602:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.EV_CHARGE_PORT_CONNECTED", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310603:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504908:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.RANGE_REMAINING", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291504904:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "392168201:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "392168201:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "392168201:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "392168201:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.GEAR_SELECTION", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408000:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.CURRENT_GEAR", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408001:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PARKING_BRAKE_ON", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310850:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.PARKING_BRAKE_AUTO_APPLY", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310851:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FUEL_LEVEL_LOW", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310853:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.NIGHT_MODE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310855:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TURN_SIGNAL_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408008:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.IGNITION_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408009:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.ABS_ACTIVE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310858:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TRACTION_CONTROL_ACTIVE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287310859:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_SPEED_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517120:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517121:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_CURRENT_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614274:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_SET_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "358614275:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DEFROSTER_9", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865540:9:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AC_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419973:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_AC_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419974:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_MAX_DEFROST_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419975:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_RECIRC_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419976:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_DUAL_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419977:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419978:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_TEMPERATURE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517131:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SIDE_MIRROR_HEAT_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339739916:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SIDE_MIRROR_HEAT_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339739916:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SIDE_MIRROR_HEAT_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339739916:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_STEERING_WHEEL_HEAT", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408269:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_TEMPERATURE_DISPLAY_UNITS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408270:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ACTUAL_FAN_SPEED_RPM_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517135:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_POWER_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419984:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:0:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_0_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:0:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:1:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:1:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_1_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:1:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:2:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:2:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_2_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:2:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:3:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:3:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_3_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:3:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:4:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:4:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_4_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:4:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:5:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:5:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_5_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:5:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:6:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:6:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_6_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:6:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:7:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:7:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_7_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:7:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:8:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:8:2" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_FAN_DIRECTION_AVAILABLE_8_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356582673:8:3" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_AUTO_RECIRC_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354419986:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_SEAT_VENTILATION_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356517139:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HVAC_ELECTRIC_DEFROSTER_ON_9", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320865556:9:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DISTANCE_DISPLAY_UNITS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408512:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FUEL_VOLUME_DISPLAY_UNITS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408513:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.TIRE_PRESSURE_DISPLAY_UNITS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408514:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.EV_BATTERY_DISPLAY_UNITS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408515:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287311364:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.VEHICLE_SPEED_DISPLAY_UNITS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289408517:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.ENV_OUTSIDE_TEMPERATURE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "291505923:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REQ_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289475072:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REQ_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289475072:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REPORT_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289475073:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_STATE_REPORT_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289475073:0:1" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.AP_POWER_BOOTUP_REASON", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289409538:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DISPLAY_BRIGHTNESS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289409539:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295872:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "373295873:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.DOOR_LOCK_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "371198722:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741504:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741504:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741504:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741505:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741505:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Z_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741505:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741506:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741506:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741506:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741507:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741507:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_Y_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "339741507:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_LOCK", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287312708:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.MIRROR_FOLD", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "287312709:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SELECT_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518784:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_MEMORY_SET_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518785:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_BUCKLED_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "354421634:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518787:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BELT_HEIGHT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518788:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518789:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_FORE_AFT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518790:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518791:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_1_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518792:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518793:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_BACKREST_ANGLE_2_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518794:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518795:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEIGHT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518796:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518797:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_DEPTH_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518798:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518799:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_TILT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518800:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518801:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_FORE_AFT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518802:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518803:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_LUMBAR_SIDE_SUPPORT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518804:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_POS", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289409941:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_HEIGHT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518806:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518807:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_ANGLE_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518808:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518809:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_HEADREST_FORE_AFT_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518810:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.SEAT_OCCUPANCY_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356518832:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_POS_9", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964416:9:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_MOVE_9", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "322964417:9:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.WINDOW_LOCK_9", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "320867268:9:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HEADLIGHTS_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410560:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HIGH_BEAM_LIGHTS_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410561:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FOG_LIGHTS_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410562:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HAZARD_LIGHTS_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410563:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HEADLIGHTS_SWITCH", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410576:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HIGH_BEAM_LIGHTS_SWITCH", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410577:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.FOG_LIGHTS_SWITCH", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410578:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.HAZARD_LIGHTS_SWITCH", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410579:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.CABIN_LIGHTS_STATE", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410817:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.CABIN_LIGHTS_SWITCH", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "289410818:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_STATE_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519683:8:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_0", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:0:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_1", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:1:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_2", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:2:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_3", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:3:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_4", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:4:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_5", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:5:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_6", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:6:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_7", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:7:0" + } + }, + { + "fullyQualifiedName": "Vehicle.VHAL.READING_LIGHTS_SWITCH_8", + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": "356519684:8:0" + } + } +] diff --git a/tools/android-app/cloud/aaosVhalNodes.json b/tools/android-app/cloud/custom-nodes-aaos-vhal.json similarity index 99% rename from tools/android-app/cloud/aaosVhalNodes.json rename to tools/android-app/cloud/custom-nodes-aaos-vhal.json index d5410224..58be6f8c 100644 --- a/tools/android-app/cloud/aaosVhalNodes.json +++ b/tools/android-app/cloud/custom-nodes-aaos-vhal.json @@ -238,35 +238,35 @@ }, { "sensor": { - "dataType": "DOUBLE", + "dataType": "INT64", "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_0", "description": "WHEEL_TICK_0" } }, { "sensor": { - "dataType": "DOUBLE", + "dataType": "INT64", "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_1", "description": "WHEEL_TICK_1" } }, { "sensor": { - "dataType": "DOUBLE", + "dataType": "INT64", "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_2", "description": "WHEEL_TICK_2" } }, { "sensor": { - "dataType": "DOUBLE", + "dataType": "INT64", "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_3", "description": "WHEEL_TICK_3" } }, { "sensor": { - "dataType": "DOUBLE", + "dataType": "INT64", "fullyQualifiedName": "Vehicle.VHAL.WHEEL_TICK_4", "description": "WHEEL_TICK_4" } diff --git a/tools/android-app/cloud/externalGpsDecoders.json b/tools/android-app/cloud/externalGpsDecoders.json deleted file mode 100644 index c422a421..00000000 --- a/tools/android-app/cloud/externalGpsDecoders.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "fullyQualifiedName": "Vehicle.CurrentLocation.Longitude", - "type": "CAN_SIGNAL", - "interfaceId": "EXTERNAL-GPS-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 0, - "offset": -2000.0, - "factor": 0.001, - "length": 32 - } - }, - { - "fullyQualifiedName": "Vehicle.CurrentLocation.Latitude", - "type": "CAN_SIGNAL", - "interfaceId": "EXTERNAL-GPS-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": true, - "isSigned": true, - "startBit": 32, - "offset": -2000.0, - "factor": 0.001, - "length": 32 - } - } -] diff --git a/tools/android-app/cloud/gen-aaos-vhal-info.py b/tools/android-app/cloud/gen-aaos-vhal-info.py index eec315a7..f2dda913 100644 --- a/tools/android-app/cloud/gen-aaos-vhal-info.py +++ b/tools/android-app/cloud/gen-aaos-vhal-info.py @@ -78,34 +78,33 @@ ) fqn = "Vehicle.VHAL." + full_name fqns += fqn + "\n" + custom_decoder = ( + f"{prop_id}" + f":{0 if area_index == -1 else area_index}" + f":{0 if result_index == -1 else result_index}" + ) decoders.append( { "fullyQualifiedName": fqn, - "type": "CAN_SIGNAL", - "interfaceId": "AAOS-VHAL-CAN", - "canSignal": { - "messageId": 1, - "isBigEndian": True, - "isSigned": True, - "startBit": (0 if area_index == -1 else area_index), - "offset": prop_id, - "factor": 1, - "length": (0 if result_index == -1 else result_index), + "type": "CUSTOM_DECODING_SIGNAL", + "interfaceId": "AAOS-VHAL", + "customDecodingSignal": { + "id": custom_decoder, }, } ) nodes.append( { "sensor": { - "dataType": "DOUBLE", + "dataType": "INT64" if "INT64" in prop_type else "DOUBLE", "fullyQualifiedName": fqn, "description": full_name, } } ) -with open("aaosVhalDecoders.json", "w") as fp: +with open("custom-decoders-aaos-vhal.json", "w") as fp: json.dump(decoders, fp, indent=2) -with open("aaosVhalNodes.json", "w") as fp: +with open("custom-nodes-aaos-vhal.json", "w") as fp: json.dump(nodes, fp, indent=2) with open("aaos-vhal-fqns.txt", "w") as fp: fp.write(fqns) diff --git a/tools/android-app/cloud/network-interface-can-aaos-vhal.json b/tools/android-app/cloud/network-interface-can-aaos-vhal.json deleted file mode 100644 index 2869ccf3..00000000 --- a/tools/android-app/cloud/network-interface-can-aaos-vhal.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "interfaceId": "AAOS-VHAL-CAN", - "type": "CAN_INTERFACE", - "canInterface": { - "name": "can0", - "protocolName": "CAN", - "protocolVersion": "2.0B" - } - } -] diff --git a/tools/android-app/cloud/network-interface-can-external-gps.json b/tools/android-app/cloud/network-interface-can-external-gps.json deleted file mode 100644 index 16c57ec0..00000000 --- a/tools/android-app/cloud/network-interface-can-external-gps.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "interfaceId": "EXTERNAL-GPS-CAN", - "type": "CAN_INTERFACE", - "canInterface": { - "name": "can0", - "protocolName": "CAN", - "protocolVersion": "2.0B" - } - } -] diff --git a/tools/android-app/cloud/network-interface-custom-aaos-vhal.json b/tools/android-app/cloud/network-interface-custom-aaos-vhal.json new file mode 100644 index 00000000..4197a85a --- /dev/null +++ b/tools/android-app/cloud/network-interface-custom-aaos-vhal.json @@ -0,0 +1,9 @@ +[ + { + "interfaceId": "AAOS-VHAL", + "type": "CUSTOM_DECODING_INTERFACE", + "customDecodingInterface": { + "name": "AaosVhalInterface" + } + } +] diff --git a/tools/android-app/cloud/provision.sh b/tools/android-app/cloud/provision.sh index 50253c2c..a8178dbf 100755 --- a/tools/android-app/cloud/provision.sh +++ b/tools/android-app/cloud/provision.sh @@ -11,7 +11,7 @@ S3_PRESIGNED_URL_EXPIRY=86400 # One day ENDPOINT_URL="" ENDPOINT_URL_OPTION="" REGION="us-east-1" -TOPIC_PREFIX="\$aws/iotfleetwise/" +IOT_FLEETWISE_TOPIC_PREFIX="\$aws/iotfleetwise/" TIMESTAMP=`date +%s` VEHICLE_NAME="fwdemo-android-${TIMESTAMP}" @@ -41,19 +41,19 @@ parse_args() { REGION=$2 shift ;; - --topic-prefix) - TOPIC_PREFIX=$2 + --iotfleetwise-topic-prefix) + IOT_FLEETWISE_TOPIC_PREFIX=$2 shift ;; --help) echo "Usage: $0 [OPTION]" - echo " --s3-qr-code Store credentials in S3 and generate QR code with pre-signed provisioning URL" - echo " --s3-bucket Existing S3 bucket name" - echo " --s3-key-prefix S3 bucket prefix" - echo " --s3-presigned-url-expiry S3 presigned URL expiry, default: ${S3_PRESIGNED_URL_EXPIRY}" - echo " --endpoint-url The endpoint URL used for AWS CLI calls" - echo " --region The region used for AWS CLI calls, default: ${REGION}" - echo " --topic-prefix IoT MQTT topic prefix, default: ${TOPIC_PREFIX}" + echo " --s3-qr-code Store credentials in S3 and generate QR code with pre-signed provisioning URL" + echo " --s3-bucket Existing S3 bucket name" + echo " --s3-key-prefix S3 bucket prefix" + echo " --s3-presigned-url-expiry S3 presigned URL expiry, default: ${S3_PRESIGNED_URL_EXPIRY}" + echo " --endpoint-url The endpoint URL used for AWS CLI calls" + echo " --region The region used for AWS CLI calls, default: ${REGION}" + echo " --iotfleetwise-topic-prefix Prefix for AWS IoT FleetWise MQTT topics, default: ${IOT_FLEETWISE_TOPIC_PREFIX}" exit 0 ;; esac @@ -103,7 +103,7 @@ echo {} | jq ".vehicle_name=\"${VEHICLE_NAME}\"" \ | jq ".endpoint_url=\"${MQTT_ENDPOINT_URL}\"" \ | jq ".certificate=\"${CERTIFICATE}\"" \ | jq ".private_key=\"${PRIVATE_KEY}\"" \ - | jq ".mqtt_topic_prefix=\"${TOPIC_PREFIX}\"" \ + | jq ".mqtt_topic_prefix=\"${IOT_FLEETWISE_TOPIC_PREFIX}\"" \ > config/creds.json if ${S3_QR_CODE}; then diff --git a/tools/build-fwe-cross-android.sh b/tools/build-fwe-cross-android.sh index b698ab02..687a6286 100755 --- a/tools/build-fwe-cross-android.sh +++ b/tools/build-fwe-cross-android.sh @@ -4,6 +4,9 @@ set -euo pipefail +WITH_LAST_KNOWN_STATE_SUPPORT="false" +WITH_CUSTOM_FUNCTION_EXAMPLES="false" + SCRIPT_DIR="$(dirname "$(realpath "$0")")" source ${SCRIPT_DIR}/install-deps-versions.sh @@ -23,10 +26,18 @@ parse_args() { NATIVE_PREFIX="$2" shift ;; + --with-lks-support) + WITH_LAST_KNOWN_STATE_SUPPORT="true" + ;; + --with-custom-function-examples) + WITH_CUSTOM_FUNCTION_EXAMPLES="true" + ;; --help) echo "Usage: $0 [OPTION]" - echo " --archs Space separated list of archs in the format :" - echo " --native-prefix Native install prefix, default ${NATIVE_PREFIX}" + echo " --archs Space separated list of archs in the format :" + echo " --native-prefix Native install prefix, default ${NATIVE_PREFIX}" + echo " --with-lks-support Build with LastKnownState support" + echo " --with-custom-function-examples Build with custom function examples" exit 0 ;; esac @@ -39,6 +50,23 @@ parse_args "$@" : ${FWE_ADDITIONAL_CMAKE_ARGS:=""} export PATH=/usr/local/android_sdk/cmake/${VERSION_CMAKE}/bin:${NATIVE_PREFIX}/bin:${PATH} + +CMAKE_OPTIONS=" + -DFWE_STATIC_LINK=On + -DFWE_STRIP_SYMBOLS=On + -DFWE_FEATURE_EXTERNAL_GPS=On + -DFWE_FEATURE_AAOS_VHAL=On + -DFWE_BUILD_EXECUTABLE=Off + -DFWE_BUILD_ANDROID_SHARED_LIBRARY=On + -DCMAKE_BUILD_TYPE=Release + -DBUILD_TESTING=Off" +if ${WITH_LAST_KNOWN_STATE_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_LAST_KNOWN_STATE=On" +fi +if ${WITH_CUSTOM_FUNCTION_EXAMPLES}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES=On" +fi + mkdir -p build && cd build build() { @@ -47,15 +75,8 @@ build() { mkdir -p ${TARGET_ARCH} && cd ${TARGET_ARCH} LDFLAGS=-L/usr/local/${HOST_PLATFORM}/lib cmake \ - -DFWE_STATIC_LINK=On \ - -DFWE_STRIP_SYMBOLS=On \ - -DFWE_FEATURE_EXTERNAL_GPS=On \ - -DFWE_FEATURE_AAOS_VHAL=On \ - -DFWE_BUILD_EXECUTABLE=Off \ - -DFWE_BUILD_ANDROID_SHARED_LIBRARY=On \ + ${CMAKE_OPTIONS} \ ${FWE_ADDITIONAL_CMAKE_ARGS} \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=Off \ -DANDROID_ABI=${TARGET_ARCH} \ -DANDROID_PLATFORM=android-${VERSION_ANDROID_API} \ -DCMAKE_ANDROID_NDK=/usr/local/android_sdk/ndk/${VERSION_ANDROID_NDK} \ diff --git a/tools/build-fwe-cross-arm64.sh b/tools/build-fwe-cross-arm64.sh index 363cb4db..0b98f181 100755 --- a/tools/build-fwe-cross-arm64.sh +++ b/tools/build-fwe-cross-arm64.sh @@ -6,6 +6,12 @@ set -eo pipefail WITH_GREENGRASSV2_SUPPORT="false" WITH_ROS2_SUPPORT="false" +WITH_SOMEIP_SUPPORT="false" +WITH_SNF_SUPPORT="false" +WITH_LAST_KNOWN_STATE_SUPPORT="false" +WITH_GENERIC_DTC_SUPPORT="false" +WITH_CUSTOM_FUNCTION_EXAMPLES="false" +WITH_REMOTE_COMMANDS_SUPPORT="false" parse_args() { while [ "$#" -gt 0 ]; do @@ -16,10 +22,34 @@ parse_args() { --with-ros2-support) WITH_ROS2_SUPPORT="true" ;; + --with-someip-support) + WITH_SOMEIP_SUPPORT="true" + ;; + --with-store-and-forward-support) + WITH_SNF_SUPPORT="true" + ;; + --with-lks-support) + WITH_LAST_KNOWN_STATE_SUPPORT="true" + ;; + --with-uds-dtc-example) + WITH_GENERIC_DTC_SUPPORT="true" + ;; + --with-custom-function-examples) + WITH_CUSTOM_FUNCTION_EXAMPLES="true" + ;; + --with-remote-commands-support) + WITH_REMOTE_COMMANDS_SUPPORT="true" + ;; --help) echo "Usage: $0 [OPTION]" - echo " --with-greengrassv2-support Build with Greengrass V2 support" - echo " --with-ros2-support Build with ROS2 support" + echo " --with-greengrassv2-support Build with Greengrass V2 support" + echo " --with-ros2-support Build with ROS2 support" + echo " --with-someip-support Build with SOME/IP support" + echo " --with-store-and-forward-support Build with Store and Forward support" + echo " --with-lks-support Build with LastKnownState support" + echo " --with-uds-dtc-example Build with UDS DTC Example" + echo " --with-custom-function-examples Build with custom function examples" + echo " --with-remote-commands-support Build with remote commands support" exit 0 ;; esac @@ -44,6 +74,24 @@ fi if ${WITH_ROS2_SUPPORT}; then CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_ROS2=On" fi +if ${WITH_SOMEIP_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_SOMEIP=On" +fi +if ${WITH_SNF_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_STORE_AND_FORWARD=On" +fi +if ${WITH_LAST_KNOWN_STATE_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_LAST_KNOWN_STATE=On" +fi +if ${WITH_GENERIC_DTC_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_UDS_DTC_EXAMPLE=On" +fi +if ${WITH_CUSTOM_FUNCTION_EXAMPLES}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES=On" +fi +if ${WITH_REMOTE_COMMANDS_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_REMOTE_COMMANDS=On" +fi if ${WITH_ROS2_SUPPORT}; then BUILD_DIR=build/iotfleetwise @@ -56,3 +104,9 @@ else make -j`nproc` cd .. fi + +if ${WITH_SOMEIP_SUPPORT}; then + cp ${BUILD_DIR}/can-to-someip tools/can-to-someip + cp ${BUILD_DIR}/someipigen.so tools/someipigen + cp ${BUILD_DIR}/someip_device_shadow_editor.so tools/someip_device_shadow_editor +fi diff --git a/tools/build-fwe-cross-armhf.sh b/tools/build-fwe-cross-armhf.sh index ecab32d2..a682d146 100755 --- a/tools/build-fwe-cross-armhf.sh +++ b/tools/build-fwe-cross-armhf.sh @@ -7,6 +7,12 @@ set -eo pipefail WITH_GREENGRASSV2_SUPPORT="false" WITH_IWAVE_GPS_SUPPORT="false" WITH_ROS2_SUPPORT="false" +WITH_SOMEIP_SUPPORT="false" +WITH_SNF_SUPPORT="false" +WITH_LAST_KNOWN_STATE_SUPPORT="false" +WITH_GENERIC_DTC_SUPPORT="false" +WITH_CUSTOM_FUNCTION_EXAMPLES="false" +WITH_REMOTE_COMMANDS_SUPPORT="false" parse_args() { while [ "$#" -gt 0 ]; do @@ -20,11 +26,35 @@ parse_args() { --with-ros2-support) WITH_ROS2_SUPPORT="true" ;; + --with-someip-support) + WITH_SOMEIP_SUPPORT="true" + ;; + --with-store-and-forward-support) + WITH_SNF_SUPPORT="true" + ;; + --with-lks-support) + WITH_LAST_KNOWN_STATE_SUPPORT="true" + ;; + --with-uds-dtc-example) + WITH_GENERIC_DTC_SUPPORT="true" + ;; + --with-custom-function-examples) + WITH_CUSTOM_FUNCTION_EXAMPLES="true" + ;; + --with-remote-commands-support) + WITH_REMOTE_COMMANDS_SUPPORT="true" + ;; --help) echo "Usage: $0 [OPTION]" - echo " --with-greengrassv2-support Build with Greengrass V2 support" - echo " --with-iwave-gps-support Build with iWave GPS support" - echo " --with-ros2-support Build with ROS2 support" + echo " --with-greengrassv2-support Build with Greengrass V2 support" + echo " --with-ros2-support Build with ROS2 support" + echo " --with-someip-support Build with SOME/IP support" + echo " --with-iwave-gps-support Build with iWave GPS support" + echo " --with-store-and-forward-support Build with Store and Forward support" + echo " --with-lks-support Build with LastKnownState support" + echo " --with-uds-dtc-example Build with UDS DTC Example" + echo " --with-custom-function-examples Build with custom function examples" + echo " --with-remote-commands-support Build with remote commands support" exit 0 ;; esac @@ -52,6 +82,24 @@ fi if ${WITH_ROS2_SUPPORT}; then CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_ROS2=On" fi +if ${WITH_SOMEIP_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_SOMEIP=On" +fi +if ${WITH_SNF_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_STORE_AND_FORWARD=On" +fi +if ${WITH_LAST_KNOWN_STATE_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_LAST_KNOWN_STATE=On" +fi +if ${WITH_GENERIC_DTC_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_UDS_DTC_EXAMPLE=On" +fi +if ${WITH_CUSTOM_FUNCTION_EXAMPLES}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES=On" +fi +if ${WITH_REMOTE_COMMANDS_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_REMOTE_COMMANDS=On" +fi if ${WITH_ROS2_SUPPORT}; then BUILD_DIR=build/iotfleetwise @@ -64,3 +112,9 @@ else make -j`nproc` cd .. fi + +if ${WITH_SOMEIP_SUPPORT}; then + cp ${BUILD_DIR}/can-to-someip tools/can-to-someip + cp ${BUILD_DIR}/someipigen.so tools/someipigen + cp ${BUILD_DIR}/someip_device_shadow_editor.so tools/someip_device_shadow_editor +fi diff --git a/tools/build-fwe-native.sh b/tools/build-fwe-native.sh index 85c0795f..3b06ebce 100755 --- a/tools/build-fwe-native.sh +++ b/tools/build-fwe-native.sh @@ -6,6 +6,12 @@ set -eo pipefail WITH_GREENGRASSV2_SUPPORT="false" WITH_ROS2_SUPPORT="false" +WITH_SOMEIP_SUPPORT="false" +WITH_SNF_SUPPORT="false" +WITH_LAST_KNOWN_STATE_SUPPORT="false" +WITH_GENERIC_DTC_SUPPORT="false" +WITH_CUSTOM_FUNCTION_EXAMPLES="false" +WITH_REMOTE_COMMANDS_SUPPORT="false" parse_args() { while [ "$#" -gt 0 ]; do @@ -16,10 +22,34 @@ parse_args() { --with-ros2-support) WITH_ROS2_SUPPORT="true" ;; + --with-someip-support) + WITH_SOMEIP_SUPPORT="true" + ;; + --with-store-and-forward-support) + WITH_SNF_SUPPORT="true" + ;; + --with-lks-support) + WITH_LAST_KNOWN_STATE_SUPPORT="true" + ;; + --with-uds-dtc-example) + WITH_GENERIC_DTC_SUPPORT="true" + ;; + --with-custom-function-examples) + WITH_CUSTOM_FUNCTION_EXAMPLES="true" + ;; + --with-remote-commands-support) + WITH_REMOTE_COMMANDS_SUPPORT="true" + ;; --help) echo "Usage: $0 [OPTION]" - echo " --with-greengrassv2-support Build with Greengrass V2 support" - echo " --with-ros2-support Build with ROS2 support" + echo " --with-greengrassv2-support Build with Greengrass V2 support" + echo " --with-ros2-support Build with ROS2 support" + echo " --with-someip-support Build with SOME/IP support" + echo " --with-store-and-forward-support Build with Store and Forward support" + echo " --with-lks-support Build with LastKnownState support" + echo " --with-uds-dtc-example Build with UDS DTC Example" + echo " --with-custom-function-examples Build with custom function examples" + echo " --with-remote-commands-support Build with remote commands support" exit 0 ;; esac @@ -44,6 +74,24 @@ fi if ${WITH_ROS2_SUPPORT}; then CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_ROS2=On" fi +if ${WITH_SOMEIP_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_SOMEIP=On" +fi +if ${WITH_SNF_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_STORE_AND_FORWARD=On" +fi +if ${WITH_LAST_KNOWN_STATE_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_LAST_KNOWN_STATE=On" +fi +if ${WITH_GENERIC_DTC_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_UDS_DTC_EXAMPLE=On" +fi +if ${WITH_CUSTOM_FUNCTION_EXAMPLES}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_CUSTOM_FUNCTION_EXAMPLES=On" +fi +if ${WITH_REMOTE_COMMANDS_SUPPORT}; then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DFWE_FEATURE_REMOTE_COMMANDS=On" +fi if ${WITH_ROS2_SUPPORT}; then BUILD_DIR=build/iotfleetwise @@ -56,3 +104,9 @@ else make -j`nproc` cd .. fi + +if ${WITH_SOMEIP_SUPPORT}; then + cp ${BUILD_DIR}/can-to-someip tools/can-to-someip + cp ${BUILD_DIR}/someipigen.so tools/someipigen + cp ${BUILD_DIR}/someip_device_shadow_editor.so tools/someip_device_shadow_editor +fi diff --git a/tools/can-to-someip/.gitignore b/tools/can-to-someip/.gitignore new file mode 100644 index 00000000..1257ee11 --- /dev/null +++ b/tools/can-to-someip/.gitignore @@ -0,0 +1 @@ +can-to-someip diff --git a/tools/can-to-someip/README.md b/tools/can-to-someip/README.md new file mode 100644 index 00000000..a754980c --- /dev/null +++ b/tools/can-to-someip/README.md @@ -0,0 +1,8 @@ +# CAN to SOME/IP + +There are two versions of this tool provided: one written in Python `can-to-someip.py` using +[pysomeip](https://github.com/afflux/pysomeip) and one written in C++ using +[vsomeip](https://github.com/COVESA/vsomeip). + +Using the C++ version may be desirable if the UNIX domain socket functionality of `vsomeip` is +required. diff --git a/tools/can-to-someip/can-to-someip.cmake b/tools/can-to-someip/can-to-someip.cmake new file mode 100644 index 00000000..583b846a --- /dev/null +++ b/tools/can-to-someip/can-to-someip.cmake @@ -0,0 +1,21 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +add_executable(can-to-someip + tools/can-to-someip/main.cpp +) + +install(TARGETS can-to-someip + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +target_include_directories(can-to-someip PUBLIC + ${VSOMEIP_INCLUDE_DIRS} +) + +target_link_libraries(can-to-someip + ${VSOMEIP_LIBRARIES} + Boost::system + Boost::thread + Boost::filesystem + Boost::program_options +) diff --git a/tools/can-to-someip/can-to-someip.py b/tools/can-to-someip/can-to-someip.py new file mode 100755 index 00000000..c3ca94a4 --- /dev/null +++ b/tools/can-to-someip/can-to-someip.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import asyncio +import logging +import socket +import textwrap + +import can +from someip.sd import ServiceDiscoveryProtocol +from someip.service import SimpleEventgroup, SimpleService + +log = logging.getLogger(__name__) + + +class CanEventGroup(SimpleEventgroup): + def __init__(self, service: SimpleService, event_group_id: int, event_id: int): + super().__init__(service, id=event_group_id, interval=None) + self._event_id = event_id + self._queue = asyncio.Queue() + + async def add_message(self, data: bytes): + # We can't just send the data here because we need to set the data in self.values and + # then call self.notify_once. But self.notify_once happens asynchronously and it doesn't + # copy the data. So by the time it sends the data it might be already overwritten. + # We need to queue it and have them sent one by one by a single coroutine. + await self._queue.put(data) + + async def run_send_messages(self): + try: + while True: + data = await self._queue.get() + self.values[self._event_id] = data + # Using the non-public self._notify_all method as self.notify_once doesn't return + # the coroutine so we can't wait for it to finish. + await self._notify_all(events=[self._event_id], label="event") + except asyncio.CancelledError: + pass + + +class CanListener(can.Listener): + def __init__(self, event_loop: asyncio.AbstractEventLoop, can_service) -> None: + super().__init__() + self._event_loop = event_loop + self._can_service = can_service + + def on_message_received(self, message: can.Message): + timestamp_us = int(message.timestamp * 1e6) + payload = b"".join( + [ + message.arbitration_id.to_bytes(4, "big"), + timestamp_us.to_bytes(8, "big"), + bytes(message.data), + ] + ) + asyncio.run_coroutine_threadsafe( + self._can_service.event_group.add_message(payload), self._event_loop + ) + + +async def create_service( + local_addr: str, + multicast_addr: str, + port: int, + service_id: int, + instance_id: int, + event_id: int, + event_group_id: int, +): + log.info( + "Creating SOME/IP service" + f" service_id=0x{service_id:02X}" + f" instance_id=0x{instance_id:02X}" + f" event_id=0x{event_id:02X}" + f" event_group_id=0x{event_group_id:02X}" + ) + sd_trsp_u, sd_trsp_m, sd_prot = await ServiceDiscoveryProtocol.create_endpoints( + family=socket.AF_INET, local_addr=local_addr, multicast_addr=multicast_addr + ) + sd_prot.timings.CYCLIC_OFFER_DELAY = 2 + + class_service_id = service_id + + class CanService(SimpleService): + service_id = class_service_id # This can only be set as class variable + version_major = 0 # interface_version + version_minor = 0 + + def __init__(self, instance_id: int, event_group_id: int, event_id: int): + super().__init__(instance_id) + self.event_group = CanEventGroup(self, event_group_id=event_group_id, event_id=event_id) + self.register_eventgroup(self.event_group) + + can_service: CanService + _, can_service = await CanService.create_unicast_endpoint( + instance_id=instance_id, + local_addr=(local_addr, port), + event_group_id=event_group_id, + event_id=event_id, + ) + can_service.start_announce(sd_prot.announcer) + + return sd_trsp_u, sd_trsp_m, sd_prot, can_service + + +async def run(sd_trsp_u, sd_trsp_m, sd_prot, can_service): + sd_prot.start() + + asyncio.get_event_loop().create_task(can_service.event_group.run_send_messages()) + + try: + while True: + await asyncio.sleep(1) + except asyncio.CancelledError: + pass + finally: + sd_prot.stop() + sd_trsp_u.close() + sd_trsp_m.close() + can_service.stop() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + class CustomArgumentParser(argparse.ArgumentParser): + def format_help(self): + return super().format_help() + textwrap.dedent( + """ + The CAN data and metadata are sent as SOME/IP payload in the following format: + _________________________________________________________ + | CAN ID | Timestamp (in us) | CAN data | + |___________|_____________________|_______________________| + 4 bytes 8 bytes variable length + + CAN ID and Timestamp are unsigned integers encoded in network byte order (big endian). + + CAN ID is in the SocketCAN format: https://github.com/linux-can/can-utils/blob/88f0c753343bd863dd3110812d6b4698c4700b26/include/linux/can.h#L66-L78 + """ # noqa: E501 + ) + + parser = CustomArgumentParser( + prog="can-to-someip", + description="Listen to CAN messages and offer them as a SOME/IP service", + ) + parser.add_argument( + "-i", + "--can-interface", + required=True, + help="The CAN interface to listen to, e.g. vcan0", + ) + parser.add_argument( + "--local-addr", + required=True, + help="The unicast IP address of this SOME/IP service. It should match the address of an" + " existing network interface.", + ) + parser.add_argument( + "--local-port", + type=int, + default=0, + help="The port this SOME/IP service will listen to", + ) + parser.add_argument( + "--multicast-addr", + required=True, + help="The multicast address that will be used for service discovery, e.g. 224.224.224.245", + ) + parser.add_argument( + "--service-id", + type=int, + default=0x7777, + help="The service id that will be announced to other SOME/IP applications", + ) + parser.add_argument( + "--instance-id", + type=int, + default=0x5678, + help="The instance id of this service that will be announced. Only a single instance will" + " be created.", + ) + parser.add_argument( + "--event-id", + type=int, + default=0x8778, + metavar="[0x8000-0xFFFE]", + help="ID of SOME/IP event that will be offered." + " All CAN data is sent with the same event ID.", + ) + parser.add_argument( + "--event-group-id", + type=int, + default=0x5555, + help="ID of SOME/IP event group that will be offered." + " Other applications will be able to subscribe to this event group.", + ) + args = parser.parse_args() + + if args.event_id < 0x8000 or args.event_id > 0xFFFE: + raise ValueError("Event ID must be in the range [0x8000-0xFFFE]") + + (sd_trsp_u, sd_trsp_m, sd_prot, can_service) = asyncio.get_event_loop().run_until_complete( + create_service( + local_addr=args.local_addr, + multicast_addr=args.multicast_addr, + port=args.local_port, + service_id=args.service_id, + instance_id=args.instance_id, + event_id=args.event_id, + event_group_id=args.event_group_id, + ) + ) + + with can.interface.Bus(args.can_interface, bustype="socketcan") as can_bus: + # The listener runs on another thread, so we need to explicitly pass the main thread's event + # loop + notifier = can.Notifier(can_bus, [CanListener(asyncio.get_event_loop(), can_service)]) + try: + asyncio.get_event_loop().run_until_complete( + run(sd_trsp_u, sd_trsp_m, sd_prot, can_service) + ) + except KeyboardInterrupt: + pass diff --git a/tools/can-to-someip/main.cpp b/tools/can-to-someip/main.cpp new file mode 100644 index 00000000..b1d76d32 --- /dev/null +++ b/tools/can-to-someip/main.cpp @@ -0,0 +1,247 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MULTI_RX_SIZE 10 + +static volatile sig_atomic_t gRunning = 1; + +static void +signalHandler( int signum ) +{ + static_cast( signum ); + gRunning = 0; +} + +static uint16_t +stringToU16( const std::string &value ) +{ + try + { + return static_cast( std::stoul( value, nullptr, 0 ) ); + } + catch ( const std::exception &e ) + { + throw std::runtime_error( "Error parsing value " + value + " to uint16_t\n" ); + } +} + +static uint64_t +extractTimestamp( struct msghdr *msgHeader ) +{ + struct cmsghdr *currentHeader = CMSG_FIRSTHDR( msgHeader ); + uint64_t timestamp = 0; + while ( currentHeader != nullptr ) + { + if ( currentHeader->cmsg_type == SO_TIMESTAMPING ) + { + // With linux kernel 5.1 new return scm_timestamping64 was introduced + scm_timestamping *timestampArray = (scm_timestamping *)( CMSG_DATA( currentHeader ) ); + // From https://www.kernel.org/doc/Documentation/networking/timestamping.txt + // Most timestamps are passed in ts[0]. Hardware timestamps are passed in ts[2]. + timestamp = static_cast( ( static_cast( timestampArray->ts[0].tv_sec ) * 1000000 ) + + ( static_cast( timestampArray->ts[0].tv_nsec ) / 1000 ) ); + break; + } + currentHeader = CMSG_NXTHDR( msgHeader, currentHeader ); + } + if ( timestamp == 0 ) // other timestamp are invalid(=0) + { + timestamp = + std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ) + .count(); + } + return timestamp; +} + +static int +openCanSocket( const std::string &interfaceName ) +{ + std::cout << "Opening CAN interface " << interfaceName << "\n"; + struct sockaddr_can interfaceAddress = {}; + struct ifreq interfaceRequest = {}; + auto canSocket = socket( PF_CAN, SOCK_RAW, CAN_RAW ); + if ( canSocket < 0 ) + { + std::cout << "Error creating CAN socket\n"; + return -1; + } + // Try to enable can_fd mode + int canFdEnabled = 1; + (void)setsockopt( canSocket, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &canFdEnabled, sizeof( canFdEnabled ) ); + + // Set the IF Name, address + if ( interfaceName.size() >= sizeof( interfaceRequest.ifr_name ) ) + { + std::cout << "Interface name too long\n"; + return -1; + } + (void)strncpy( interfaceRequest.ifr_name, interfaceName.c_str(), sizeof( interfaceRequest.ifr_name ) - 1U ); + if ( ioctl( canSocket, SIOCGIFINDEX, &interfaceRequest ) != 0 ) + { + std::cout << "CAN Interface with name " << interfaceName << " is not accessible\n"; + close( canSocket ); + return -1; + } + + // Try to enable timestamping + const int timestampFlags = ( SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RX_SOFTWARE | + SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_RAW_HARDWARE ); + (void)setsockopt( canSocket, SOL_SOCKET, SO_TIMESTAMPING, ×tampFlags, sizeof( timestampFlags ) ); + + interfaceAddress.can_family = AF_CAN; + interfaceAddress.can_ifindex = interfaceRequest.ifr_ifindex; + + if ( bind( canSocket, (struct sockaddr *)&interfaceAddress, sizeof( interfaceAddress ) ) < 0 ) + { + std::cout << "Failed to bind socket\n"; + close( canSocket ); + return -1; + } + return canSocket; +} + +int +main( int argc, char **argv ) +{ + try + { + std::cout << "can-to-someip\n"; + + // clang-format off + boost::program_options::options_description argsDescription( + "Listen to CAN messages and offer them as a SOME/IP service\n" + "The CAN data and metadata are sent as SOME/IP payload in the following format:\n" + "___________________________________________________________\n" + "| CAN ID | Timestamp (in us) | CAN data |\n" + "|___________|_____________________|_______________________|\n" + " 4 bytes 8 bytes variable length\n" + "CAN ID and Timestamp are unsigned integers encoded in network byte order (big endian).\n" + "CAN ID is in the SocketCAN format: https://github.com/linux-can/can-utils/blob/88f0c753343bd863dd3110812d6b4698c4700b26/include/linux/can.h#L66-L78\n" + "Options" ); + argsDescription.add_options() + ( "can-interface", boost::program_options::value()->default_value( "vcan0" ), "The CAN interface to listen to" ) + ( "service-id", boost::program_options::value()->default_value( "0x7777" ), "The service id that will be announced to other SOME/IP applications" ) + ( "instance-id", boost::program_options::value()->default_value( "0x5678" ), "The instance id of this service that will be announced. Only a single instance will be created." ) + ( "event-id", boost::program_options::value()->default_value( "0x8778" ), "ID of SOME/IP event that will be offered. All CAN data is sent with the same event ID." ) + ( "event-group-id", boost::program_options::value()->default_value( "0x5555" ), "ID of SOME/IP event group that will be offered. Other applications will be able to subscribe to this event group." ) + ( "help", "Print this help message" ); + // clang-format on + boost::program_options::variables_map args; + boost::program_options::store( boost::program_options::parse_command_line( argc, argv, argsDescription ), + args ); + boost::program_options::notify( args ); + if ( args.count( "help" ) > 0 ) + { + std::cout << argsDescription << "\n"; + return 0; + } + + std::string interfaceName = args["can-interface"].as(); + uint16_t serviceId = stringToU16( args["service-id"].as() ); + uint16_t instanceId = stringToU16( args["instance-id"].as() ); + uint16_t eventId = stringToU16( args["event-id"].as() ); + uint16_t eventGroupId = stringToU16( args["event-group-id"].as() ); + + struct sigaction signalAction = {}; + signalAction.sa_handler = signalHandler; + sigaction( SIGINT, &signalAction, 0 ); + sigaction( SIGTERM, &signalAction, 0 ); + + auto canSocket = openCanSocket( interfaceName ); + if ( canSocket < 0 ) + { + return -1; + } + + auto someipApp = vsomeip::runtime::get()->create_application( "can-to-someip" ); + someipApp->init(); + someipApp->offer_service( serviceId, instanceId ); + std::set eventGroup; + eventGroup.insert( eventGroupId ); + someipApp->offer_event( serviceId, instanceId, eventId, eventGroup, vsomeip::event_type_e::ET_FIELD ); + std::thread someipThread( [someipApp]() { + someipApp->start(); + } ); + + while ( gRunning != 0 ) + { + // In one syscall receive up to MULTI_RX_SIZE frames at once + struct canfd_frame frame[MULTI_RX_SIZE] + { + }; + struct iovec frame_buffer[MULTI_RX_SIZE] + { + }; + struct mmsghdr msg[MULTI_RX_SIZE] + { + }; + char cmsgReturnBuffer[MULTI_RX_SIZE][CMSG_SPACE( sizeof( struct scm_timestamping ) )]{}; + for ( int i = 0; i < MULTI_RX_SIZE; i++ ) + { + frame_buffer[i].iov_base = &frame[i]; + frame_buffer[i].iov_len = sizeof( frame ); + msg[i].msg_hdr.msg_iov = &frame_buffer[i]; + msg[i].msg_hdr.msg_iovlen = 1; + msg[i].msg_hdr.msg_control = &cmsgReturnBuffer[i]; + msg[i].msg_hdr.msg_controllen = sizeof( cmsgReturnBuffer[i] ); + } + auto nmsgs = recvmmsg( canSocket, &msg[0], MULTI_RX_SIZE, 0, nullptr ); // blocking call + if ( nmsgs < 0 ) + { + break; + } + + for ( int i = 0; i < nmsgs; i++ ) + { + auto timestamp = extractTimestamp( &msg[i].msg_hdr ); + std::vector data; + uint32_t canIdBigEndian = htobe32( frame[i].can_id ); + uint64_t timestampBigEndian = htobe64( timestamp ); + data.insert( data.end(), + reinterpret_cast( &canIdBigEndian ), + reinterpret_cast( &canIdBigEndian ) + sizeof( canIdBigEndian ) ); + data.insert( data.end(), + reinterpret_cast( ×tampBigEndian ), + reinterpret_cast( ×tampBigEndian ) + sizeof( timestampBigEndian ) ); + data.insert( data.end(), frame[i].data, frame[i].data + frame[i].len ); + auto payload = vsomeip::runtime::get()->create_payload(); + payload->set_data( std::move( data ) ); + someipApp->notify( serviceId, instanceId, eventId, payload ); + } + } + someipApp->stop(); + someipThread.join(); + (void)close( canSocket ); + return 0; + } + catch ( const std::exception &e ) + { + std::cout << e.what(); + return -1; + } +} diff --git a/tools/cansim/can_command_server.py b/tools/cansim/can_command_server.py new file mode 100755 index 00000000..1c396c36 --- /dev/null +++ b/tools/cansim/can_command_server.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import asyncio +import struct +import time +from threading import Thread + +import can + + +class CanCommandServer: + command_configs = [ + { + "canRequestId": 0x00000123, # standard 11-bit CAN ID + "canResponseId": 0x00000456, # standard 11-bit CAN ID + "actuatorName": "Vehicle.actuator6", + "argumentType": ">i", # big-endian int32 + "inProgressCount": 0, + "status": 1, # SUCCEEDED + "reasonCode": 0x1234, + "reasonDescription": "hello", + "responseDelay": 1, + }, + { + "canRequestId": 0x80000789, # extended 29-bit CAN ID (MSB sets extended) + "canResponseId": 0x80000ABC, # extended 29-bit CAN ID (MSB sets extended) + "actuatorName": "Vehicle.actuator7", + "argumentType": ">d", # big-endian double + "inProgressCount": 10, + "status": 1, # SUCCEEDED + "reasonCode": 0x5678, + "reasonDescription": "goodbye", + "responseDelay": 1, + }, + ] + args = {} # used to record the passed arguments for each execution + + def is_extended_id(self, can_id): + return (can_id & 0x80000000) != 0 + + def mask_id(self, can_id): + return (0x1FFFFFFF if self.is_extended_id(can_id) else 0x7FF) & can_id + + def pop_string(self): + for i in range(len(self.data)): + if self.data[i] == 0x00: + break + else: + raise RuntimeError("Could not find null terminator") + string_data = self.data[0:i] + self.data = self.data[i + 1 :] + return string_data.decode("utf-8") + + def pop_arg(self, struct_type): + dummy = struct.pack(struct_type, 0) + arg_data = self.data[0 : len(dummy)] + self.data = self.data[len(dummy) :] + return struct.unpack(struct_type, arg_data)[0] + + def push_string(self, val): + val += "\0" + self.data += val.encode("utf-8") + + def push_arg(self, struct_type, val): + self.data += struct.pack(struct_type, val) + + def handle_message(self, receive_msg): + for config in self.command_configs: + if receive_msg.arbitration_id == self.mask_id( + config["canRequestId"] + ) and receive_msg.is_extended_id == self.is_extended_id(config["canRequestId"]): + break + else: + return + + try: + self.data = bytearray(receive_msg.data) + command_id = self.pop_string() + issued_timestamp_ms = self.pop_arg(">Q") + execution_timeout_ms = self.pop_arg(">Q") + arg_value = self.pop_arg(config["argumentType"]) + print( + f"Received request for {config['actuatorName']}" + f" with command id {command_id}, value {arg_value}," + f" issued timestamp {issued_timestamp_ms}, execution timeout {execution_timeout_ms}" + ) + self.args[command_id] = arg_value + execution_state = {"in_progress_counter": config["inProgressCount"]} + + def send_response(): + status = 10 if execution_state["in_progress_counter"] > 0 else config["status"] + print( + f"Sending response to request for {config['actuatorName']}" + f" with command id {command_id}, status {status}," + f" reason code {config['reasonCode']}," + f" reason description {config['reasonDescription']}" + ) + self.data = bytearray() + send_msg = can.Message() + send_msg.arbitration_id = self.mask_id(config["canResponseId"]) + send_msg.is_extended_id = self.is_extended_id(config["canResponseId"]) + send_msg.is_fd = True + self.push_string(command_id) + self.push_arg("B", status) + self.push_arg(">I", config["reasonCode"]) + self.push_string(config["reasonDescription"]) + send_msg.data = self.data + send_msg.dlc = len(self.data) + self.can_bus.send(send_msg) + if execution_state["in_progress_counter"] > 0: + execution_state["in_progress_counter"] -= 1 + self.loop.call_later(config["responseDelay"], send_response) + + self.loop.call_later(config["responseDelay"], send_response) + except Exception as e: + print(e) + + def __init__(self, interface): + self.can_bus = can.Bus(interface, interface="socketcan", fd=True) + self.loop = asyncio.new_event_loop() + can.Notifier(bus=self.can_bus, listeners=[self.handle_message], loop=self.loop) + self.thread = Thread(name="CanCommandServer", target=self.loop.run_forever) + self.thread.start() + + def stop(self): + async def stop_loop(): + self.loop.stop() + + asyncio.run_coroutine_threadsafe(stop_loop(), self.loop) + self.thread.join() + self.can_bus.shutdown() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Runs a server to respond to CAN command messages") + parser.add_argument("-i", "--interface", required=True, help="CAN interface, e.g. vcan0") + args = parser.parse_args() + command_server = CanCommandServer(args.interface) + try: + while True: + time.sleep(60) + except KeyboardInterrupt: + pass + command_server.stop() diff --git a/tools/cansim/canigen.py b/tools/cansim/canigen.py index 8eb5c536..737582e7 100755 --- a/tools/cansim/canigen.py +++ b/tools/cansim/canigen.py @@ -41,7 +41,7 @@ def __init__( self._sig_names = [] self._obd_answer_reverse_order = obd_answer_reverse_order fd = False - self._values = {"sig": {}, "pid": {}, "dtc": {}} + self._values = {"sig": {}, "pid": {}, "dtc": {}, "dtc_snapshots": {}, "dtc_ext_data": {}} if database_filename is not None: self._db = cantools.database.load_file(database_filename) for msg in self._db.messages: @@ -72,6 +72,10 @@ def __init__( self._dtc_names.append(dtc_name) if dtc_name not in self._values["dtc"]: self._values["dtc"][dtc_name] = 0.0 + if dtc_name not in self._values["dtc_snapshots"]: + self._values["dtc_snapshots"][dtc_name] = {} + if dtc_name not in self._values["dtc_ext_data"]: + self._values["dtc_ext_data"][dtc_name] = {} self._threads = [] if database_filename is not None: @@ -239,36 +243,105 @@ def _obd_thread(self, ecu): # print(ecu['name']+' rx: '+str(rx)) sid = rx.pop(0) - tx = [sid | 0x40] - if ecu.get("require_broadcast_requests", False) and res[0][0] != isotp_socket_func: - tx = [0x7F, sid, 0x11] # NRC Service not supported - elif sid == 0x01: # PID - while len(rx) > 0: - pid_num = rx.pop(-1 if self._obd_answer_reverse_order else 0) - if (pid_num % 0x20) == 0: # Supported PIDs - supported, data = self._get_supported_pids(pid_num, ecu) + try: + tx = [sid | 0x40] + if ecu.get("require_broadcast_requests", False) and res[0][0] != isotp_socket_func: + tx = [0x7F, sid, 0x11] # NRC Service not supported + elif sid == 0x01: # OBD PID + while len(rx) > 0: + pid_num = rx.pop(-1 if self._obd_answer_reverse_order else 0) + if (pid_num % 0x20) == 0: # Supported PIDs + supported, data = self._get_supported_pids(pid_num, ecu) + if ( + pid_num == 0 + or supported + or not ecu.get("ignore_unsupported_pid_requests", False) + ): + tx += [pid_num] + data + else: + data = self._encode_pid_data(pid_num, ecu) + if data is not None: + tx += [pid_num] + data + elif sid == 0x03: # OBD DTCs + num_dtcs = 0 + dtc_data = [] + for dtc_name in ecu["dtcs"]: if ( - pid_num == 0 - or supported - or not ecu.get("ignore_unsupported_pid_requests", False) + ecu["dtcs"][dtc_name].get("type", "OBD") == "OBD" + and self._values["dtc"][dtc_name] ): - tx += [pid_num] + data + dtc_num = int(ecu["dtcs"][dtc_name]["num"], 16) + dtc_data.append((dtc_num >> 8) & 0xFF) + dtc_data.append(dtc_num & 0xFF) + num_dtcs += 1 + tx += [num_dtcs] + dtc_data + elif sid == 0x19: # UDS ReadDTCInformation + subfn = rx.pop(0) + tx += [subfn] + if subfn == 0x02: # reportDTCByStatusMask + status_mask = rx.pop(0) + status_availability_mask = 0xFF # All bits supported + tx.append(status_availability_mask) + for dtc_name in ecu["dtcs"]: + dtc_status = int(self._values["dtc"][dtc_name]) & 0xFF + if ( + ecu["dtcs"][dtc_name].get("type", "OBD") == "UDS" + and dtc_status & status_mask + ): + dtc_num = int(ecu["dtcs"][dtc_name]["num"], 16) + tx.append((dtc_num >> 16) & 0xFF) + tx.append((dtc_num >> 8) & 0xFF) + tx.append(dtc_num & 0xFF) + tx.append(dtc_status) + elif subfn == 0x03: # reportDTCSnapshotIdentification + for dtc_name in self._values.get("dtc_snapshots", {}): + if ( + dtc_name in ecu["dtcs"] + and ecu["dtcs"][dtc_name].get("type", "OBD") == "UDS" + ): + dtc_num = int(ecu["dtcs"][dtc_name]["num"], 16) + for record_number in self._values["dtc_snapshots"][dtc_name].keys(): + tx.append((dtc_num >> 16) & 0xFF) + tx.append((dtc_num >> 8) & 0xFF) + tx.append(dtc_num & 0xFF) + tx.append(int(record_number)) + elif subfn in [ + 0x04, + 0x06, + ]: # reportDTCSnapshotRecordByDTCNumber | reportDTCExtDataRecordByDTCNumber + dtc_num = rx.pop(0) << 16 | rx.pop(0) << 8 | rx.pop(0) + record_num = rx.pop(0) + vals = ( + self._values.get("dtc_snapshots", {}) + if subfn == 0x04 + else self._values.get("dtc_ext_data", {}) + ) + for dtc_name in vals: + if ( + dtc_name in ecu["dtcs"] + and ecu["dtcs"][dtc_name].get("type", "OBD") == "UDS" + and dtc_num == int(ecu["dtcs"][dtc_name]["num"], 16) + and str(record_num) in vals[dtc_name] + ): + dtc_status = int(self._values["dtc"][dtc_name]) & 0xFF + data = [] + for byte in vals[dtc_name][str(record_num)]: + data.append(int(byte, 16)) + tx.append((dtc_num >> 16) & 0xFF) + tx.append((dtc_num >> 8) & 0xFF) + tx.append(dtc_num & 0xFF) + tx.append(dtc_status) + tx.append(record_num) + tx += data # Includes number of DIDs as first byte + break + else: + tx = [0x7F, sid, 0x31] # NRC request out of range else: - data = self._encode_pid_data(pid_num, ecu) - if data is not None: - tx += [pid_num] + data - elif sid == 0x03: # DTCs - num_dtcs = 0 - dtc_data = [] - for dtc_name in ecu["dtcs"]: - if self._values["dtc"][dtc_name]: - dtc_num = int(ecu["dtcs"][dtc_name]["num"], 16) - dtc_data.append((dtc_num >> 8) & 0xFF) - dtc_data.append(dtc_num & 0xFF) - num_dtcs += 1 - tx += [num_dtcs] + dtc_data - else: - tx = [0x7F, sid, 0x11] # NRC Service not supported + tx = [0x7F, sid, 0x12] # NRC subfunction not supported + else: + tx = [0x7F, sid, 0x11] # NRC Service not supported + except IndexError: + tx = [0x7F, sid, 0x13] # NRC incorrectMessageLengthOrInvalidFormat # print(ecu['name']+' tx: '+str(tx)) if len(tx) > 1: try: @@ -299,6 +372,12 @@ def set_pid(self, name, value): def set_dtc(self, name, value): self._values["dtc"][name] = value + def set_dtc_snapshot(self, name, record_number, data): + self._values["dtc_snapshots"][name][str(record_number)] = data + + def set_dtc_ext_data(self, name, record_number, data): + self._values["dtc_ext_data"][name][str(record_number)] = data + def get_value(self, val_type, name): return self._values[val_type][name] @@ -311,6 +390,12 @@ def get_pid(self, name): def get_dtc(self, name): return self._values["dtc"][name] + def get_dtc_snapshot(self, name, record_number): + return self._values["dtc_snapshot"][name][str(record_number)] + + def get_dtc_ext_data(self, name, record_number): + return self._values["dtc_ext_data"][name][str(record_number)] + def load_values(self, filename): self._values = self._load_json(filename) diff --git a/tools/cansim/cansim.py b/tools/cansim/cansim.py index f3d4fca3..0290b701 100755 --- a/tools/cansim/cansim.py +++ b/tools/cansim/cansim.py @@ -20,15 +20,21 @@ database_filename=None if args.only_obd else "hscan.dbc", obd_config_filename="obd_config.json", ) +NETWORK_TYPE = "NetworkType" BRAKE_PRESSURE_SIGNAL = "DemoBrakePedalPressure" ENGINE_TORQUE_SIGNAL = "DemoEngineTorque" def set_with_print(func, name, val): - print(str(datetime.datetime.now()) + " Set " + name + " to " + str(val)) + print(str(datetime.datetime.now()), f"Set {name} to {val}") func(name, val) +def set_multiple_arguments_with_print(func, name, val1, val2): + print(str(datetime.datetime.now()), f"Set {name} to {val1}: {val2}") + func(name, val1, val2) + + try: while True: set_with_print(can_sim.set_sig, BRAKE_PRESSURE_SIGNAL, 0) @@ -40,6 +46,13 @@ def set_with_print(func, name, val): set_with_print(can_sim.set_pid, "ENGINE_COOLANT_TEMPERATURE", 80 + i) set_with_print(can_sim.set_pid, "THROTTLE_POSITION", (i % 4) * 100) set_with_print(can_sim.set_dtc, "ECM_DTC1", i / 5 >= 1) + set_with_print(can_sim.set_dtc, "ECM_DTC3", 0xAF) + set_multiple_arguments_with_print( + can_sim.set_dtc_snapshot, "ECM_DTC3", 1, ["0x01", "0xAA", "0xBB", hex(i)] + ) + set_multiple_arguments_with_print( + can_sim.set_dtc_ext_data, "ECM_DTC3", 1, ["0x01", "0xCC", "0xDD", hex(i * 2)] + ) time.sleep(5) if i < 6 or i > 9: # trigger is > 7000 so trigger @@ -47,6 +60,11 @@ def set_with_print(func, name, val): set_with_print(can_sim.set_sig, ENGINE_TORQUE_SIGNAL, i * 100) time.sleep(0.5) set_with_print(can_sim.set_sig, BRAKE_PRESSURE_SIGNAL, i * 200) + if i % 4 < 2: # change network type every 10 seconds + set_with_print(can_sim.set_sig, NETWORK_TYPE, 0) + else: + set_with_print(can_sim.set_sig, NETWORK_TYPE, 1) + except KeyboardInterrupt: print("Stopping...") can_sim.stop() diff --git a/tools/cansim/obd_config.json b/tools/cansim/obd_config.json index ebf91675..ef663b52 100644 --- a/tools/cansim/obd_config.json +++ b/tools/cansim/obd_config.json @@ -30,8 +30,10 @@ "ENGINE_OIL_TEMPERATURE" : {"num": "0x5C", "scale": 1.0 , "offset": 40.0, "size": 1} }, "dtcs": { - "ECM_DTC1": {"num": "0x0123"}, - "ECM_DTC2": {"num": "0x4567"} + "ECM_DTC1": {"num": "0x0123", "type": "OBD"}, + "ECM_DTC2": {"num": "0x4567", "type": "OBD"}, + "ECM_DTC3": {"num": "0xAAA123", "type": "UDS"}, + "ECM_DTC4": {"num": "0xBBB456", "type": "UDS"} } }, { @@ -44,8 +46,10 @@ "ENGINE_REFERENCE_PERCENT_TORQUE" : {"num": "0x63", "scale": 1.0 , "offset": 0.0, "size": 2} }, "dtcs": { - "TCU_DTC1": {"num": "0x89AB"}, - "TCU_DTC2": {"num": "0xCDEF"} + "TCU_DTC1": {"num": "0x89AB", "type": "OBD"}, + "TCU_DTC2": {"num": "0xCDEF", "type": "OBD"}, + "TCU_DTC3": {"num": "0xCCC789", "type": "UDS"}, + "TCU_DTC4": {"num": "0xDDDABC", "type": "UDS"} } } ] diff --git a/tools/cfn-templates/fwdemo.yml b/tools/cfn-templates/fwdemo.yml index e4584b0a..0a457578 100644 --- a/tools/cfn-templates/fwdemo.yml +++ b/tools/cfn-templates/fwdemo.yml @@ -142,9 +142,28 @@ Resources: systemctl stop unattended-upgrades systemctl disable unattended-upgrades + # Wait for any existing package install to finish + i=0 + while true; do + if sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; then + i=0 + else + i=`expr $i + 1` + if expr $i \>= 10 > /dev/null; then + break + fi + fi + sleep 1 + done + + print_process_list() { + ps aux # Print processes on error in case apt lock was still taken + } + trap print_process_list ERR + # Upgrade system and reboot if required - apt update -o DPkg::Lock::Timeout=120 - apt upgrade -y -o DPkg::Lock::Timeout=120 + apt update + apt upgrade -y if [ -f /var/run/reboot-required ]; then # Delete the UserData info file so that we run again after reboot rm -f /var/lib/cloud/instances/*/sem/config_scripts_user @@ -153,8 +172,8 @@ Resources: fi # Install helper scripts: - apt update -o DPkg::Lock::Timeout=120 - apt install -y -o DPkg::Lock::Timeout=120 python3-setuptools + apt update + apt install -y python3-setuptools mkdir -p /opt/aws/bin wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz python3 -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-py3-latest.tar.gz @@ -162,6 +181,7 @@ Resources: # On error, signal back to cfn: error_handler() { + print_process_list /opt/aws/bin/cfn-signal --success false --stack ${AWS::StackName} --resource Ec2Instance --region ${AWS::Region} } trap error_handler ERR @@ -174,8 +194,8 @@ Resources: printf "RateLimitBurst=0\nSystemMaxUse=1G\n" >> /etc/systemd/journald.conf # Install packages - apt update -o DPkg::Lock::Timeout=120 - apt install -y -o DPkg::Lock::Timeout=120 wget ec2-instance-connect htop jq unzip + apt update + apt install -y wget ec2-instance-connect htop jq unzip # Install AWS CLI: curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" @@ -230,7 +250,7 @@ Resources: --log-level ${FweLogLevel} \ --vehicle-name "${AWS::StackName}" \ --endpoint-url "${IoTThing.iotEndpoint}" \ - --topic-prefix '${IoTMqttTopicPrefix}' \ + --iotfleetwise-topic-prefix '${IoTMqttTopicPrefix}' \ --can-bus0 "vcan0" \ ${!VISION_SYSTEM_DATA_OPTIONS} else @@ -247,7 +267,7 @@ Resources: --output-config-file "/etc/aws-iot-fleetwise/config-$((i+j)).json" \ --vehicle-name "${AWS::StackName}-$((i+j))" \ --endpoint-url "${IoTThing.iotEndpoint}" \ - --topic-prefix '${IoTMqttTopicPrefix}' \ + --iotfleetwise-topic-prefix '${IoTMqttTopicPrefix}' \ --can-bus0 "vcan$((i+j))" \ --persistency-path "/var/aws-iot-fleetwise/fwe$((i+j))" \ ${!VISION_SYSTEM_DATA_OPTIONS}; \ @@ -263,10 +283,6 @@ Resources: # Signal init complete: /opt/aws/bin/cfn-signal --stack ${AWS::StackName} --resource Ec2Instance --region ${AWS::Region} - - # Re-enable unattended upgrades - systemctl enable unattended-upgrades - systemctl start unattended-upgrades Ec2Instance: Type: AWS::EC2::Instance CreationPolicy: diff --git a/tools/cfn-templates/fwdev.yml b/tools/cfn-templates/fwdev.yml index 5f19a13b..d71a77cf 100644 --- a/tools/cfn-templates/fwdev.yml +++ b/tools/cfn-templates/fwdev.yml @@ -115,15 +115,34 @@ Resources: Fn::Base64: !Sub - | #!/bin/bash - set -euo pipefail + set -xeuo pipefail # Disable unattended upgrades systemctl stop unattended-upgrades systemctl disable unattended-upgrades + # Wait for any existing package install to finish + i=0 + while true; do + if sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; then + i=0 + else + i=`expr $i + 1` + if expr $i \>= 10 > /dev/null; then + break + fi + fi + sleep 1 + done + + print_process_list() { + ps aux # Print processes on error in case apt lock was still taken + } + trap print_process_list ERR + # Upgrade system and reboot if required - apt update -o DPkg::Lock::Timeout=120 - apt upgrade -y -o DPkg::Lock::Timeout=120 + apt update + apt upgrade -y if [ -f /var/run/reboot-required ]; then # Delete the UserData info file so that we run again after reboot rm -f /var/lib/cloud/instances/*/sem/config_scripts_user @@ -132,8 +151,8 @@ Resources: fi # Install helper scripts: - apt update -o DPkg::Lock::Timeout=120 - apt install -y -o DPkg::Lock::Timeout=120 python3-setuptools + apt update + apt install -y python3-setuptools mkdir -p /opt/aws/bin wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz python3 -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-py3-latest.tar.gz @@ -141,13 +160,14 @@ Resources: # On error, signal back to cfn: error_handler() { + print_process_list /opt/aws/bin/cfn-signal --success false --stack ${AWS::StackName} --resource Ec2Instance --region ${AWS::Region} } trap error_handler ERR # Install packages - apt update -o DPkg::Lock::Timeout=120 - apt install -y -o DPkg::Lock::Timeout=120 ec2-instance-connect htop jq unzip zip + apt update + apt install -y ec2-instance-connect htop jq unzip zip # Install AWS CLI: curl "https://awscli.amazonaws.com/awscli-exe-linux-${Arch}.zip" -o "awscliv2.zip" @@ -158,9 +178,6 @@ Resources: # Signal init complete: /opt/aws/bin/cfn-signal --stack ${AWS::StackName} --resource Ec2Instance --region ${AWS::Region} - # Re-enable unattended upgrades - systemctl enable unattended-upgrades - systemctl start unattended-upgrades - Arch: !FindInMap [InstanceArchMap, !Ref Ec2InstanceType, Arch] Ec2Instance: Type: AWS::EC2::Instance diff --git a/tools/cfn-templates/vision-system-data-jupyter.yml b/tools/cfn-templates/vision-system-data-jupyter.yml index 926a3e4b..bd41eb80 100644 --- a/tools/cfn-templates/vision-system-data-jupyter.yml +++ b/tools/cfn-templates/vision-system-data-jupyter.yml @@ -345,9 +345,28 @@ Resources: systemctl stop unattended-upgrades systemctl disable unattended-upgrades + # Wait for any existing package install to finish + i=0 + while true; do + if sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock >/dev/null 2>&1; then + i=0 + else + i=`expr $i + 1` + if expr $i \>= 10 > /dev/null; then + break + fi + fi + sleep 1 + done + + print_process_list() { + ps aux # Print processes on error in case apt lock was still taken + } + trap print_process_list ERR + # Upgrade system and reboot if required - apt update -o DPkg::Lock::Timeout=120 - apt upgrade -y -o DPkg::Lock::Timeout=120 + apt update + apt upgrade -y if [ -f /var/run/reboot-required ]; then # Delete the UserData info file so that we run again after reboot rm -f /var/lib/cloud/instances/*/sem/config_scripts_user @@ -356,8 +375,8 @@ Resources: fi # Install helper scripts: configsets=OnCreate - apt update -o DPkg::Lock::Timeout=120 - apt install -y -o DPkg::Lock::Timeout=120 python3-setuptools python3-pip git markdown + apt update + apt install -y python3-setuptools python3-pip git markdown mkdir -p /opt/aws/bin wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz python3 -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-py3-latest.tar.gz @@ -368,6 +387,7 @@ Resources: rm amazon-cloudwatch-agent.deb # On error, signal back to cfn: error_handler() { + print_process_list /opt/aws/bin/cfn-signal --success false --stack ${AWS::StackName} --resource ${Resource} --region ${AWS::Region} } function error_exit { @@ -376,8 +396,8 @@ Resources: } trap error_handler ERR # Install packages - apt update -o DPkg::Lock::Timeout=120 - apt install -y -o DPkg::Lock::Timeout=120 ec2-instance-connect htop jq unzip zip + apt update + apt install -y ec2-instance-connect htop jq unzip zip # Install AWS CLI: curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" unzip -q awscliv2.zip diff --git a/tools/cloud/.gitignore b/tools/cloud/.gitignore index 6e25c27c..f590a583 100644 --- a/tools/cloud/.gitignore +++ b/tools/cloud/.gitignore @@ -3,5 +3,6 @@ demo.env collected-data-* ros2-nodes.json ros2-decoders.json +last_known_state_message_pb2.py can-nodes.json can-decoders.json diff --git a/tools/cloud/campaign-math.json b/tools/cloud/campaign-math.json new file mode 100644 index 00000000..31ac39c2 --- /dev/null +++ b/tools/cloud/campaign-math.json @@ -0,0 +1,18 @@ +{ + "compression": "SNAPPY", + "collectionScheme": { + "conditionBasedCollectionScheme": { + "conditionLanguageVersion": 1, + "expression": "custom_function('pow', custom_function('pow', $variable.`Vehicle.ECM.DemoEngineTorque`, 2) + custom_function('pow', $variable.`Vehicle.ABS.DemoBrakePedalPressure`, 2), 0.5) > 100", + "triggerMode": "RISING_EDGE" + } + }, + "signalsToCollect": [ + { + "name": "Vehicle.ECM.DemoEngineTorque" + }, + { + "name": "Vehicle.ABS.DemoBrakePedalPressure" + } + ] +} diff --git a/tools/cloud/campaign-multi-rising-edge-trigger.json b/tools/cloud/campaign-multi-rising-edge-trigger.json new file mode 100644 index 00000000..e13bba13 --- /dev/null +++ b/tools/cloud/campaign-multi-rising-edge-trigger.json @@ -0,0 +1,15 @@ +{ + "compression": "SNAPPY", + "collectionScheme": { + "conditionBasedCollectionScheme": { + "conditionLanguageVersion": 1, + "expression": "custom_function('MULTI_RISING_EDGE_TRIGGER', 'ALARM1', $variable.`Vehicle.ECM.DemoEngineTorque` > 500, 'ALARM2', $variable.`Vehicle.ABS.DemoBrakePedalPressure` > 7000)", + "triggerMode": "ALWAYS" + } + }, + "signalsToCollect": [ + { + "name": "Vehicle.MultiRisingEdgeTrigger" + } + ] +} diff --git a/tools/cloud/campaign-obd-and-location-heartbeat.json b/tools/cloud/campaign-obd-and-location-heartbeat.json new file mode 100644 index 00000000..69fbd6f9 --- /dev/null +++ b/tools/cloud/campaign-obd-and-location-heartbeat.json @@ -0,0 +1,36 @@ +{ + "compression": "SNAPPY", + "diagnosticsMode": "SEND_ACTIVE_DTCS", + "spoolingMode": "TO_DISK", + "collectionScheme": { + "timeBasedCollectionScheme": { + "periodMs": 10000 + } + }, + "signalsToCollect": [ + { + "name": "Vehicle.OBD.EngineSpeed" + }, + { + "name": "Vehicle.OBD.Speed" + }, + { + "name": "Vehicle.OBD.AmbientAirTemperature" + }, + { + "name": "Vehicle.OBD.EngineCoolantTemperature" + }, + { + "name": "Vehicle.OBD.ThrottlePosition" + }, + { + "name": "Vehicle.OBD.FuelLevel" + }, + { + "name": "Vehicle.CurrentLocation.Latitude" + }, + { + "name": "Vehicle.CurrentLocation.Longitude" + } + ] +} diff --git a/tools/cloud/campaign-someip-heartbeat.json b/tools/cloud/campaign-someip-heartbeat.json new file mode 100644 index 00000000..fb53b56c --- /dev/null +++ b/tools/cloud/campaign-someip-heartbeat.json @@ -0,0 +1,23 @@ +{ + "compression": "SNAPPY", + "spoolingMode": "TO_DISK", + "collectionScheme": { + "timeBasedCollectionScheme": { + "periodMs": 10000 + } + }, + "signalsToCollect": [ + { + "name": "Vehicle.ExampleSomeipInterface.X" + }, + { + "name": "Vehicle.ExampleSomeipInterface.A1.A2.A" + }, + { + "name": "Vehicle.ExampleSomeipInterface.A1.A2.B" + }, + { + "name": "Vehicle.ExampleSomeipInterface.A1.A2.D" + } + ] +} diff --git a/tools/cloud/campaign-store-only-no-upload.json b/tools/cloud/campaign-store-only-no-upload.json new file mode 100644 index 00000000..9f2f1390 --- /dev/null +++ b/tools/cloud/campaign-store-only-no-upload.json @@ -0,0 +1,33 @@ +{ + "compression": "SNAPPY", + "diagnosticsMode": "OFF", + "spoolingMode": "TO_DISK", + "collectionScheme": { + "timeBasedCollectionScheme": { + "periodMs": 10000 + } + }, + "postTriggerCollectionDuration": 1000, + "signalsToCollect": [ + { + "name": "Vehicle.ECM.DemoEngineTorque", + "dataPartitionId": "engine" + } + ], + "dataPartitions": [ + { + "id": "engine", + "storageOptions": { + "maximumSize": { + "unit": "MB", + "value": 10 + }, + "storageLocation": "engine_data", + "minimumTimeToLive": { + "unit": "DAYS", + "value": 7 + } + } + } + ] +} diff --git a/tools/cloud/campaign-uds-dtc-condition-based-fetch.json b/tools/cloud/campaign-uds-dtc-condition-based-fetch.json new file mode 100644 index 00000000..e5d1ad14 --- /dev/null +++ b/tools/cloud/campaign-uds-dtc-condition-based-fetch.json @@ -0,0 +1,29 @@ +{ + "compression": "SNAPPY", + "spoolingMode": "TO_DISK", + "signalsToFetch": [ + { + "fullyQualifiedName": "Vehicle.ECU1.DTC_INFO", + "signalFetchConfig": { + "conditionBased": { + "conditionExpression": "$variable.`Vehicle.ECM.DemoEngineTorque` > 890", + "triggerMode": "ALWAYS" + } + }, + "actions": ["custom_function(\"DTC_QUERY\", -1, 4, -1)"] + } + ], + "signalsToCollect": [ + { + "name": "Vehicle.ECU1.DTC_INFO" + }, + { + "name": "Vehicle.ECM.DemoEngineTorque" + } + ], + "collectionScheme": { + "timeBasedCollectionScheme": { + "periodMs": 10000 + } + } +} diff --git a/tools/cloud/campaign-uds-dtc-time-based-fetch.json b/tools/cloud/campaign-uds-dtc-time-based-fetch.json new file mode 100644 index 00000000..da17fa1d --- /dev/null +++ b/tools/cloud/campaign-uds-dtc-time-based-fetch.json @@ -0,0 +1,31 @@ +{ + "compression": "SNAPPY", + "spoolingMode": "TO_DISK", + "signalsToFetch": [ + { + "fullyQualifiedName": "Vehicle.ECU1.DTC_INFO", + "signalFetchConfig": { + "timeBased": { + "executionFrequencyMs": 5000 + } + }, + "actions": [ + "custom_function(\"DTC_QUERY\", -1, 4, -1)", + "custom_function(\"DTC_QUERY\", -1, 6, -1)" + ] + } + ], + "signalsToCollect": [ + { + "name": "Vehicle.ECU1.DTC_INFO" + } + ], + "collectionScheme": { + "conditionBasedCollectionScheme": { + "conditionLanguageVersion": 1, + "expression": "!isNull($variable.`Vehicle.ECU1.DTC_INFO`)", + "minimumTriggerIntervalMs": 1000, + "triggerMode": "ALWAYS" + } + } +} diff --git a/tools/cloud/campaign-upload-critical-during-hard-braking.json b/tools/cloud/campaign-upload-critical-during-hard-braking.json new file mode 100644 index 00000000..a00c6bad --- /dev/null +++ b/tools/cloud/campaign-upload-critical-during-hard-braking.json @@ -0,0 +1,34 @@ +{ + "compression": "SNAPPY", + "diagnosticsMode": "OFF", + "spoolingMode": "TO_DISK", + "collectionScheme": { + "timeBasedCollectionScheme": { + "periodMs": 10000 + } + }, + "postTriggerCollectionDuration": 1000, + "signalsToCollect": [ + { + "name": "Vehicle.ABS.DemoBrakePedalPressure", + "dataPartitionId": "critical" + } + ], + "dataPartitions": [ + { + "id": "critical", + "storageOptions": { + "maximumSize": { + "unit": "MB", + "value": 10 + }, + "storageLocation": "critical_data", + "minimumTimeToLive": { + "unit": "DAYS", + "value": 7 + } + }, + "uploadOptions": { "expression": "$variable.`Vehicle.ABS.DemoBrakePedalPressure` > 7000" } + } + ] +} diff --git a/tools/cloud/campaign-upload-during-wifi.json b/tools/cloud/campaign-upload-during-wifi.json new file mode 100644 index 00000000..bd451a34 --- /dev/null +++ b/tools/cloud/campaign-upload-during-wifi.json @@ -0,0 +1,34 @@ +{ + "compression": "SNAPPY", + "diagnosticsMode": "OFF", + "spoolingMode": "TO_DISK", + "collectionScheme": { + "timeBasedCollectionScheme": { + "periodMs": 30000 + } + }, + "postTriggerCollectionDuration": 1000, + "signalsToCollect": [ + { + "name": "Vehicle.Connectivity.NetworkType", + "dataPartitionId": "basic" + } + ], + "dataPartitions": [ + { + "id": "basic", + "storageOptions": { + "maximumSize": { + "unit": "MB", + "value": 10 + }, + "storageLocation": "basic_data", + "minimumTimeToLive": { + "unit": "DAYS", + "value": 7 + } + }, + "uploadOptions": { "expression": "$variable.`Vehicle.Connectivity.NetworkType` == 1" } + } + ] +} diff --git a/tools/cloud/clean-up.sh b/tools/cloud/clean-up.sh index c5601fbd..fe414cf6 100755 --- a/tools/cloud/clean-up.sh +++ b/tools/cloud/clean-up.sh @@ -4,6 +4,7 @@ set -euo pipefail +SCRIPT_DIR="$(dirname "$(realpath "$0")")" ENDPOINT_URL="" ENDPOINT_URL_OPTION="" REGION="us-east-1" @@ -189,67 +190,10 @@ if [ "${SIGNAL_CATALOG}" != "" ]; then || FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} delete-signal-catalog" fi -# The role might not exist depending on which type of campaign was created, so only try to delete -# it if it exists. -echo "Checking if role exists: ${SERVICE_ROLE}" -if ! GET_ROLE_OUTPUT=$(aws iam get-role --region ${REGION} --role-name ${SERVICE_ROLE} 2>&1); then - if ! echo ${GET_ROLE_OUTPUT} | grep -q "NoSuchEntity"; then - echo ${GET_ROLE_OUTPUT} - FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} get-role" - fi -else - echo "Deleting service role and policy..." - ATTACHED_POLICIES=$( - aws iam list-attached-role-policies \ - --region ${REGION} \ - --role-name ${SERVICE_ROLE} --query 'AttachedPolicies[].PolicyArn' --output text - ) - for POLICY_ARN in $ATTACHED_POLICIES; do - echo "Detaching policy: $POLICY_ARN from role: $SERVICE_ROLE" - aws iam detach-role-policy \ - --region ${REGION} \ - --role-name ${SERVICE_ROLE} --policy-arn ${POLICY_ARN} \ - || FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} detach-role-policy" - - MAX_ATTEMPTS=60 - for i in $(seq 1 $MAX_ATTEMPTS); do - echo "Deleting policy ${POLICY_ARN}. This might take a while until detach-role-policy operation propagates" - if aws iam delete-policy --region ${REGION} --policy-arn ${POLICY_ARN}; then - break - elif $i -eq $MAX_ATTEMPTS; then - FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} delete-policy" - else - sleep 1 - fi - done - done - - INLINE_POLICIES=$( - aws iam list-role-policies \ - --region ${REGION} \ - --role-name ${SERVICE_ROLE} --query 'PolicyNames[]' --output text - ) - for POLICY_NAME in $INLINE_POLICIES; do - echo "Deleting inline policy: $POLICY_NAME from role: $SERVICE_ROLE" - aws iam delete-role-policy \ - --region ${REGION} \ - --role-name ${SERVICE_ROLE} --policy-name ${POLICY_NAME} \ - || FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} delete-role-policy" - done - - - MAX_ATTEMPTS=60 - for i in $(seq 1 $MAX_ATTEMPTS); do - echo "Deleting service role ${SERVICE_ROLE}. This may take a while until the policy deletion propagates." - if aws iam delete-role --region ${REGION} --role-name ${SERVICE_ROLE}; then - break - elif $i -eq $MAX_ATTEMPTS; then - FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} delete-role" - else - sleep 1 - fi - done -fi +${SCRIPT_DIR}/manage-service-role.sh \ + --service-role ${SERVICE_ROLE} \ + --clean-up \ + || FAILED_CLEAN_UP_STEPS="${FAILED_CLEAN_UP_STEPS} delete-service-role" if [ "${FAILED_CLEAN_UP_STEPS}" != "" ]; then echo "Failed to clean up the following resources: ${FAILED_CLEAN_UP_STEPS}" diff --git a/tools/cloud/custom-decoders-can-actuators.json b/tools/cloud/custom-decoders-can-actuators.json new file mode 100644 index 00000000..3e74f5f5 --- /dev/null +++ b/tools/cloud/custom-decoders-can-actuators.json @@ -0,0 +1,18 @@ +[ + { + "fullyQualifiedName": "Vehicle.actuator6", + "interfaceId": "CAN_ACTUATORS", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator6" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator7", + "interfaceId": "CAN_ACTUATORS", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator7" + } + } +] diff --git a/tools/cloud/custom-decoders-location.json b/tools/cloud/custom-decoders-location.json new file mode 100644 index 00000000..3c1fd27d --- /dev/null +++ b/tools/cloud/custom-decoders-location.json @@ -0,0 +1,18 @@ +[ + { + "fullyQualifiedName": "Vehicle.CurrentLocation.Longitude", + "interfaceId": "LOCATION", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.CurrentLocation.Longitude" + } + }, + { + "fullyQualifiedName": "Vehicle.CurrentLocation.Latitude", + "interfaceId": "LOCATION", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.CurrentLocation.Latitude" + } + } +] diff --git a/tools/cloud/custom-decoders-multi-rising-edge-trigger.json b/tools/cloud/custom-decoders-multi-rising-edge-trigger.json new file mode 100644 index 00000000..c0393835 --- /dev/null +++ b/tools/cloud/custom-decoders-multi-rising-edge-trigger.json @@ -0,0 +1,10 @@ +[ + { + "fullyQualifiedName": "Vehicle.MultiRisingEdgeTrigger", + "interfaceId": "NAMED_SIGNAL", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.MultiRisingEdgeTrigger" + } + } +] diff --git a/tools/cloud/custom-decoders-someip.json b/tools/cloud/custom-decoders-someip.json new file mode 100644 index 00000000..2080952e --- /dev/null +++ b/tools/cloud/custom-decoders-someip.json @@ -0,0 +1,98 @@ +[ + { + "fullyQualifiedName": "Vehicle.actuator1", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator1" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator2", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator2" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator3", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator3" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator4", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator4" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator5", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator5" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator9", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator9" + } + }, + { + "fullyQualifiedName": "Vehicle.actuator20", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.actuator20" + } + }, + { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.X", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ExampleSomeipInterface.X" + } + }, + { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2.A", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ExampleSomeipInterface.A1.A2.A" + } + }, + { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2.B", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ExampleSomeipInterface.A1.A2.B" + } + }, + { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2.D", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ExampleSomeipInterface.A1.A2.D" + } + }, + { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.S", + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ExampleSomeipInterface.A1.S" + } + } +] diff --git a/tools/cloud/custom-decoders-uds-dtc.json b/tools/cloud/custom-decoders-uds-dtc.json new file mode 100644 index 00000000..cc123995 --- /dev/null +++ b/tools/cloud/custom-decoders-uds-dtc.json @@ -0,0 +1,10 @@ +[ + { + "fullyQualifiedName": "Vehicle.ECU1.DTC_INFO", + "interfaceId": "UDS_DTC", + "type": "CUSTOM_DECODING_SIGNAL", + "customDecodingSignal": { + "id": "Vehicle.ECU1.DTC_INFO" + } + } +] diff --git a/tools/cloud/custom-nodes-can-actuators.json b/tools/cloud/custom-nodes-can-actuators.json new file mode 100644 index 00000000..eabca562 --- /dev/null +++ b/tools/cloud/custom-nodes-can-actuators.json @@ -0,0 +1,16 @@ +[ + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator6", + "description": "Vehicle actuator6", + "dataType": "INT32" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator7", + "description": "Vehicle actuator7", + "dataType": "DOUBLE" + } + } +] diff --git a/tools/android-app/cloud/externalGpsNodes.json b/tools/cloud/custom-nodes-location.json similarity index 100% rename from tools/android-app/cloud/externalGpsNodes.json rename to tools/cloud/custom-nodes-location.json diff --git a/tools/cloud/custom-nodes-multi-rising-edge-trigger.json b/tools/cloud/custom-nodes-multi-rising-edge-trigger.json new file mode 100644 index 00000000..728000aa --- /dev/null +++ b/tools/cloud/custom-nodes-multi-rising-edge-trigger.json @@ -0,0 +1,15 @@ +[ + { + "branch": { + "fullyQualifiedName": "Vehicle", + "description": "Vehicle" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.MultiRisingEdgeTrigger", + "description": "Vehicle.MultiRisingEdgeTrigger", + "dataType": "STRING" + } + } +] diff --git a/tools/cloud/custom-nodes-someip.json b/tools/cloud/custom-nodes-someip.json new file mode 100644 index 00000000..9cca36c9 --- /dev/null +++ b/tools/cloud/custom-nodes-someip.json @@ -0,0 +1,104 @@ +[ + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator1", + "description": "Vehicle actuator1", + "dataType": "INT32" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator2", + "description": "Vehicle actuator2", + "dataType": "INT64" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator3", + "description": "Vehicle actuator3", + "dataType": "BOOLEAN" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator4", + "description": "Vehicle actuator4", + "dataType": "FLOAT" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator5", + "description": "Vehicle actuator5", + "dataType": "DOUBLE" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator9", + "description": "Vehicle actuator9", + "dataType": "STRING" + } + }, + { + "actuator": { + "fullyQualifiedName": "Vehicle.actuator20", + "description": "Vehicle actuator20", + "dataType": "INT32" + } + }, + { + "branch": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface", + "description": "Vehicle.ExampleSomeipInterface" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.X", + "description": "Vehicle.ExampleSomeipInterface.X", + "dataType": "INT32" + } + }, + { + "branch": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1", + "description": "Vehicle.ExampleSomeipInterface.A1" + } + }, + { + "branch": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2", + "description": "Vehicle.ExampleSomeipInterface.A1.A2" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2.A", + "description": "Vehicle.ExampleSomeipInterface.A1.A2.A", + "dataType": "INT32" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2.B", + "description": "Vehicle.ExampleSomeipInterface.A1.A2.B", + "dataType": "BOOLEAN" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.A2.D", + "description": "Vehicle.ExampleSomeipInterface.A1.A2.D", + "dataType": "DOUBLE" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.ExampleSomeipInterface.A1.S", + "description": "Vehicle.ExampleSomeipInterface.A1.S", + "dataType": "STRING" + } + } +] diff --git a/tools/cloud/custom-nodes-uds-dtc.json b/tools/cloud/custom-nodes-uds-dtc.json new file mode 100644 index 00000000..7877bd94 --- /dev/null +++ b/tools/cloud/custom-nodes-uds-dtc.json @@ -0,0 +1,21 @@ +[ + { + "branch": { + "fullyQualifiedName": "Vehicle", + "description": "Vehicle" + } + }, + { + "branch": { + "fullyQualifiedName": "Vehicle.ECU1", + "description": "Vehicle.ECU1" + } + }, + { + "sensor": { + "fullyQualifiedName": "Vehicle.ECU1.DTC_INFO", + "description": "Vehicle.ECU1.DTC_INFO", + "dataType": "STRING" + } + } +] diff --git a/tools/cloud/dbc-to-decoders.py b/tools/cloud/dbc-to-decoders.py index 4a9c5813..3c0f230f 100755 --- a/tools/cloud/dbc-to-decoders.py +++ b/tools/cloud/dbc-to-decoders.py @@ -84,7 +84,7 @@ signal_to_add["messageId"] = message.frame_id # In a DBC file, the start bit indicates the LSB for little endian and MSB for big endian - # signals. AWS IoT Fleetwise considers start bit to always be the LSB regardless of + # signals. AWS IoT FleetWise considers start bit to always be the LSB regardless of # endianess. That is why we need to convert the value obtained from DBC. if signal.byte_order == "big_endian": pos = 7 - (signal.start % 8) + (signal.length - 1) diff --git a/tools/cloud/demo.sh b/tools/cloud/demo.sh index 1fcd9d87..9c81b50a 100755 --- a/tools/cloud/demo.sh +++ b/tools/cloud/demo.sh @@ -36,13 +36,14 @@ BATCH_SIZE=$((`nproc`*4)) HEALTH_CHECK_RETRIES=360 # About 30mins MAX_ATTEMPTS_ON_REGISTRATION_FAILURE=5 FORCE_REGISTRATION=false -MIN_CLI_VERSION="aws-cli/2.13.39" +MIN_CLI_VERSION="2.13.39" CREATED_SIGNAL_CATALOG_NAME="" CREATED_S3_BUCKET="" CREATED_CAMPAIGN_NAMES="" INCLUDE_SIGNALS="" EXCLUDE_SIGNALS="" DATA_DESTINATION="TIMESTREAM" +IOT_TOPIC="iotfleetwise-data-${DISAMBIGUATOR}" parse_args() { while [ "$#" -gt 0 ]; do @@ -112,27 +113,32 @@ parse_args() { EXCLUDE_SIGNALS="$2" shift ;; + --iot-topic) + IOT_TOPIC="$2" + shift + ;; --help) echo "Usage: $0 [OPTION]" - echo " --vehicle-name Vehicle name" - echo " --fleet-size Size of fleet, default: ${FLEET_SIZE}. When greater than 1," - echo " the instance number will be appended to each" - echo " Vehicle name after a '-', e.g. ${DEFAULT_VEHICLE_NAME}-42" - echo " --node-file Node JSON file. Can be used multiple times for multiple files." - echo " --decoder-file Decoder JSON file. Can be used multiple times for multiple files." - echo " --network-interface-file Network interface JSON file. Can be used multiple times for multiple files." - echo " --campaign-file Campaign JSON file. Can be used multiple times for multiple files." - echo " --data-destination Data destination, either TIMESTREAM or S3, default: ${DATA_DESTINATION}" - echo " --s3-format Either JSON or PARQUET, default: ${S3_FORMAT}" - echo " --bucket-name S3 bucket name, if not specified a new bucket will be created" - echo " --set-bucket-policy Sets the required bucket policy" - echo " --clean-up Delete created resources" - echo " --skip-account-registration Don't check account registration nor try to register it. Most features should work without registration." - echo " --include-signals Comma separated list of signals to include in HTML plot" - echo " --exclude-signals Comma separated list of signals to exclude from HTML plot" - echo " --endpoint-url The endpoint URL used for AWS CLI calls" - echo " --service-principal AWS service principal for policies, default: ${SERVICE_PRINCIPAL}" - echo " --region The region used for AWS CLI calls, default: ${REGION}" + echo " --vehicle-name Vehicle name" + echo " --fleet-size Size of fleet, default: ${FLEET_SIZE}. When greater than 1," + echo " the instance number will be appended to each" + echo " Vehicle name after a '-', e.g. ${DEFAULT_VEHICLE_NAME}-42" + echo " --node-file Node JSON file. Can be used multiple times for multiple files." + echo " --decoder-file Decoder JSON file. Can be used multiple times for multiple files." + echo " --network-interface-file Network interface JSON file. Can be used multiple times for multiple files." + echo " --campaign-file Campaign JSON file. Can be used multiple times for multiple files." + echo " --data-destination Data destination, either TIMESTREAM, S3 or IOT_TOPIC, default: ${DATA_DESTINATION}" + echo " --s3-format Either JSON or PARQUET, default: ${S3_FORMAT}" + echo " --bucket-name S3 bucket name, if not specified a new bucket will be created" + echo " --set-bucket-policy Sets the required bucket policy" + echo " --clean-up Delete created resources" + echo " --skip-account-registration Don't check account registration nor try to register it. Most features should work without registration." + echo " --include-signals Comma separated list of signals to include in HTML plot" + echo " --exclude-signals Comma separated list of signals to exclude from HTML plot" + echo " --endpoint-url The endpoint URL used for AWS CLI calls" + echo " --service-principal AWS service principal for policies, default: ${SERVICE_PRINCIPAL}" + echo " --region The region used for AWS CLI calls, default: ${REGION}" + echo " --iot-topic The IoT topic to publish vehicle data to, default: ${IOT_TOPIC} when --data-destination IOT_TOPIC is used" exit 0 ;; esac @@ -180,10 +186,10 @@ echo "Vehicles: ${VEHICLES[@]}" # AWS CLI v1.x has a double base64 encoding issue echo "Checking AWS CLI version..." -CLI_VERSION=`aws --version | grep -Eo "aws-cli/[[:digit:]]+.[[:digit:]]+.[[:digit:]]+"` +CLI_VERSION=`aws --version | sed -nE 's#^aws-cli/([0-9]+\.[0-9]+\.[0-9]+).*$#\1#p'` echo "${CLI_VERSION}" - -if [[ "${CLI_VERSION}" < "${MIN_CLI_VERSION}" ]]; then +CLI_VERSION_OK=`python3 -c "from packaging.version import Version;print(Version('${CLI_VERSION}') >= Version('${MIN_CLI_VERSION}'))" 2> /dev/null` +if [ "${CLI_VERSION_OK}" != "True" ]; then echo "Error: Please update AWS CLI to ${MIN_CLI_VERSION} or newer" >&2 exit -1 fi @@ -416,65 +422,21 @@ elif [ "${DATA_DESTINATION}" == "TIMESTREAM" ]; then # Timestream --retention-properties "{\"MemoryStoreRetentionPeriodInHours\":2, \ \"MagneticStoreRetentionPeriodInDays\":2}" | jq -r .Table.Arn ) - echo "Creating service role..." - SERVICE_ROLE_TRUST_POLICY=$(cat << EOF -{ -"Version": "2012-10-17", -"Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ - "$SERVICE_PRINCIPAL" - ] - }, - "Action": "sts:AssumeRole" - } -] -} -EOF -) - SERVICE_ROLE_ARN=`aws iam create-role \ - --role-name "${SERVICE_ROLE}" \ - --assume-role-policy-document "${SERVICE_ROLE_TRUST_POLICY}" | jq -r .Role.Arn` - echo ${SERVICE_ROLE_ARN} - - echo "Waiting for role to be created..." - aws iam wait role-exists \ - --role-name "${SERVICE_ROLE}" - - echo "Creating service role policy..." - SERVICE_ROLE_POLICY=$(cat <<'EOF' -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "timestreamIngestion", - "Effect": "Allow", - "Action": [ - "timestream:WriteRecords", - "timestream:Select", - "timestream:DescribeTable" - ] - }, - { - "Sid": "timestreamDescribeEndpoint", - "Effect": "Allow", - "Action": [ - "timestream:DescribeEndpoints" - ], - "Resource": "*" - } - ] -} -EOF -) - SERVICE_ROLE_POLICY=`echo "${SERVICE_ROLE_POLICY}" \ - | jq ".Statement[0].Resource=\"arn:aws:timestream:${REGION}:${ACCOUNT_ID}:database/${TIMESTREAM_DB_NAME}/*\""` - aws iam put-role-policy \ - --role-name "${SERVICE_ROLE}" \ - --policy-name ${SERVICE_ROLE}-policy \ - --policy-document "${SERVICE_ROLE_POLICY}" + SERVICE_ROLE_ARN=`${SCRIPT_DIR}/manage-service-role.sh \ + --service-role ${SERVICE_ROLE} \ + --service-principal ${SERVICE_PRINCIPAL} \ + --actions "timestream:WriteRecords,timestream:Select,timestream:DescribeTable" \ + --resources "arn:aws:timestream:${REGION}:${ACCOUNT_ID}:database/${TIMESTREAM_DB_NAME}/*" \ + --actions "timestream:DescribeEndpoints" \ + --resources "*"` +elif [ "${DATA_DESTINATION}" == "IOT_TOPIC" ]; then + TOPIC_ARN=arn:aws:iot:${REGION}:${ACCOUNT_ID}:topic/${IOT_TOPIC} + echo "IoT topic destination: ${TOPIC_ARN}" + SERVICE_ROLE_ARN=`${SCRIPT_DIR}/manage-service-role.sh \ + --service-role ${SERVICE_ROLE} \ + --service-principal ${SERVICE_PRINCIPAL} \ + --actions "iot:Publish" \ + --resources ${TOPIC_ARN}` else echo "Error: Unknown data destination ${DATA_DESTINATION}" exit -1 @@ -668,6 +630,9 @@ else elif [ "${DATA_DESTINATION}" == "TIMESTREAM" ]; then CAMPAIGN=`echo "${CAMPAIGN}" \ | jq ".dataDestinationConfigs=[{\"timestreamConfig\":{\"timestreamTableArn\":\"${TIMESTREAM_TABLE_ARN}\",\"executionRoleArn\":\"${SERVICE_ROLE_ARN}\"}}]"` + elif [ "${DATA_DESTINATION}" == "IOT_TOPIC" ]; then + CAMPAIGN=`echo "${CAMPAIGN}" \ + | jq ".dataDestinationConfigs=[{\"mqttTopicConfig\":{\"mqttTopicArn\":\"${TOPIC_ARN}\",\"executionRoleArn\":\"${SERVICE_ROLE_ARN}\"}}]"` else echo "Error: Unknown data destination ${DATA_DESTINATION}" exit -1 @@ -790,7 +755,8 @@ else done if [ ${#OUTPUT_FILES[@]} -eq 0 ]; then - echo "WARNING: No output files saved, was any data collected on any vehicles?" + echo "Error: No output files saved, was any data collected on any vehicles?" + exit -1 else echo "You can now view the collected data." echo "----------------------------------------------------------------" @@ -800,7 +766,50 @@ else echo `realpath ${FILE}` done fi + elif [ "${DATA_DESTINATION}" == "IOT_TOPIC" ]; then + echo "Publishing to ${IOT_TOPIC}" + + OUTPUT_FILES=() + + for VEHICLE in ${VEHICLES[@]}; do + TOPIC_NAME=${IOT_TOPIC} + TOPIC_JSON_FILE="${COLLECTED_DATA_DIR}${VEHICLE}-iot-topic-result.json" + SUBSCRIBE_TIME=30 + echo "Subscribing to IoT topic '${TOPIC_NAME}' for vehicle ${VEHICLE} for ${SUBSCRIBE_TIME} seconds.." + if python3 ${SCRIPT_DIR}/iot-topic-subscribe.py \ + --client-id ${VEHICLE}-subscriber \ + --region ${REGION} \ + --output-file ${TOPIC_JSON_FILE} \ + --vehicle-name ${VEHICLE} \ + --iot-topic ${IOT_TOPIC} \ + --run-time ${SUBSCRIBE_TIME}; then + echo "Saved IoT topic results to ${TOPIC_JSON_FILE}" + OUTPUT_FILE_HTML="${COLLECTED_DATA_DIR}${VEHICLE}.html" + OUTPUT_FILES+=(${OUTPUT_FILE_HTML}) + echo "Converting from IoT topic JSON to HTML..." + python3 ${SCRIPT_DIR}/iot-topic-to-html.py \ + --vehicle-name ${VEHICLE} \ + --files ${TOPIC_JSON_FILE} \ + --html-filename ${OUTPUT_FILE_HTML} \ + --include-signals "${INCLUDE_SIGNALS}" \ + --exclude-signals "${EXCLUDE_SIGNALS}" + else + echo "WARNING: Could not save IoT topic results for ${VEHICLE}, was any data collected?" + fi + done + if [ ${#OUTPUT_FILES[@]} -eq 0 ]; then + echo "Error: No output files saved, was any data collected on any vehicles?" + exit -1 + else + echo "You can now view the collected data." + echo "----------------------------------------------------------------" + echo "| Collected data for all campaigns for ${NAME} in HTML format: |" + echo "----------------------------------------------------------------" + for FILE in ${OUTPUT_FILES[@]}; do + echo `realpath ${FILE}` + done + fi else echo "Error: Unknown data destination ${DATA_DESTINATION}" exit -1 diff --git a/tools/cloud/install-deps.sh b/tools/cloud/install-deps.sh index fc548fb8..397b4039 100755 --- a/tools/cloud/install-deps.sh +++ b/tools/cloud/install-deps.sh @@ -6,8 +6,8 @@ set -eo pipefail # On Ubuntu install Python 3 and pip if command -v apt &> /dev/null; then - apt update -o DPkg::Lock::Timeout=1800 - apt install -y -o DPkg::Lock::Timeout=1800 python3 python3-pip + apt update + apt install -y python3 python3-pip fi # Install pip packages @@ -20,4 +20,8 @@ python3 -m pip install \ plotly==5.3.1 \ pandas==1.3.5 \ cantools==36.4.0 \ - pyarrow==12.0.1 + pyarrow==12.0.1 \ + boto3==1.18.60 \ + protobuf==3.20.2 \ + awsiotsdk==1.17.0 \ + packaging==20.3 diff --git a/tools/cloud/iot-topic-subscribe.py b/tools/cloud/iot-topic-subscribe.py new file mode 100755 index 00000000..792bdef2 --- /dev/null +++ b/tools/cloud/iot-topic-subscribe.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import json +import time +from concurrent.futures import Future + +import boto3 +from awscrt import auth, mqtt5 +from awsiot import mqtt5_client_builder +from botocore.config import Config + +received_data = None + + +def on_publish_received(message): + global received_data + packet = message.publish_packet + print(f"Received message on topic: {packet.topic}") + try: + # Parse the message payload and extract the relevant data + data = json.loads(packet.payload) + if received_data is None: + print(f"Received data: {json.dumps(data, indent=2)}") + else: + received_data.append(data) + except Exception as e: + print(f"Error parsing message: {e}") + + +def main(): + parser = argparse.ArgumentParser(description="Receives data from the IoT topic") + parser.add_argument("--endpoint-url", metavar="URL", help="IoT Core endpoint URL", default=None) + parser.add_argument("--region", metavar="REGION", help="IoT Core region", default="us-east-1") + parser.add_argument("--client-id", metavar="ID", help="IoT Core region", default="CUSTOMER_APP") + parser.add_argument("--run-time", metavar="SEC", help="Run time, zero is infinite", default="0") + parser.add_argument("--output-file", metavar="FILE", help="Output JSON file", default=None) + parser.add_argument("--iot-topic", metavar="IOT_TOPIC", help="MQTT topic", required=True) + parser.add_argument("--vehicle-name", metavar="NAME", help="Vehicle name", required=True) + args = parser.parse_args() + + if args.output_file: + global received_data + received_data = [] + + session = boto3.Session() + iot_client = session.client( + "iot", endpoint_url=args.endpoint_url, config=Config(region_name=args.region) + ) + iot_endpoint = iot_client.describe_endpoint(endpointType="iot:Data-ATS")["endpointAddress"] + credentials_provider = auth.AwsCredentialsProvider.new_default_chain() + + stop_future = Future() + + def on_lifecycle_stopped(data: mqtt5.LifecycleStoppedData): + print(f"MQTT client stopped {data=}") + stop_future.set_result(None) + + mqtt_connection = mqtt5_client_builder.websockets_with_default_aws_signing( + endpoint=iot_endpoint, + region=args.region, + on_publish_received=on_publish_received, + on_lifecycle_stopped=on_lifecycle_stopped, + credentials_provider=credentials_provider, + client_id=args.client_id, + clean_session=False, + keep_alive_secs=30, + ) + mqtt_connection.start() + mqtt_topic = f"{args.iot_topic}" + subscribe_future = mqtt_connection.subscribe( + subscribe_packet=mqtt5.SubscribePacket( + subscriptions=[mqtt5.Subscription(topic_filter=mqtt_topic, qos=mqtt5.QoS.AT_LEAST_ONCE)] + ) + ) + subscribe_res = subscribe_future.result(1000) + print(f"Established mqtt subscription to {mqtt_topic} with {subscribe_res.reason_codes}") + run_time = 0 + try: + while int(args.run_time) == 0 or run_time < int(args.run_time): + time.sleep(1) + run_time += 1 + except KeyboardInterrupt: + pass + + mqtt_connection.stop() + stop_future.result(timeout=10) + + if args.output_file: + with open(args.output_file, "w") as fp: + fp.write(json.dumps(received_data, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tools/cloud/iot-topic-to-html.py b/tools/cloud/iot-topic-to-html.py new file mode 100755 index 00000000..a0fdd6ff --- /dev/null +++ b/tools/cloud/iot-topic-to-html.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import json + +import pandas as pd +import plotly.graph_objects as go + + +def is_included(name, include_list, exclude_list): + if include_list: + for include in include_list: + if include and include in name: + break + else: + return False + for exclude in exclude_list: + if exclude and exclude in name: + return False + return True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Creates plots for collected IoT topic data") + parser.add_argument( + "--vehicle-name", + required=True, + help="Vehicle name", + ) + parser.add_argument( + "--files", + type=argparse.FileType("r"), + nargs="+", + required=True, + help="List files to process", + ) + parser.add_argument( + "--html-filename", + metavar="FILE", + required=True, + help="HTML output filename", + ) + parser.add_argument( + "--include-signals", + metavar="SIGNAL_LIST", + help="Comma separated list of signals to include", + ) + parser.add_argument( + "--exclude-signals", + metavar="SIGNAL_LIST", + help="Comma separated list of signals to exclude", + ) + + args = parser.parse_args() + + df = pd.DataFrame() + include_list = ( + [i.strip() for i in args.include_signals.split(",")] if args.include_signals else [] + ) + exclude_list = ( + [i.strip() for i in args.exclude_signals.split(",")] if args.exclude_signals else [] + ) + + for file in args.files: + try: + with open(file.name) as fp: + data = json.load(fp) + + for row in data: + if row["vehicleName"] != args.vehicle_name: + continue + + for measure_name, measure_value in row["signals"].items(): + if not is_included(measure_name, include_list, exclude_list): + continue + for signal in measure_value: + timestamp = signal["time"] + df.at[timestamp, measure_name] = signal["value"] + except Exception as e: + raise Exception(str(e) + f" in {file.name}") + + if df.empty: + raise Exception("No data found") + df["time"] = pd.to_datetime(df.index, unit="ms") + fig = go.Figure() + for column in df: + if column != "time": + fig.add_trace(go.Scatter(x=df["time"], y=df[column], mode="markers", name=column)) + + with open(args.html_filename, "w") as fp: + fp.write(fig.to_html()) diff --git a/tools/cloud/lks-subscribe.py b/tools/cloud/lks-subscribe.py new file mode 100755 index 00000000..1241eb46 --- /dev/null +++ b/tools/cloud/lks-subscribe.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import json +import random +import time +from concurrent.futures import Future + +import boto3 +from awscrt import auth, mqtt5 +from awsiot import mqtt5_client_builder +from botocore.config import Config +from google.protobuf.json_format import MessageToJson + +received_data = [] + + +def on_publish_received(message): + import last_known_state_message_pb2 + + global received_data + packet = message.publish_packet + print(f"Received message on topic: {packet.topic}") + try: + msg = last_known_state_message_pb2.LastKnownState() + msg.ParseFromString(packet.payload) + json_msg = MessageToJson(msg) + print(f"Received message: {json_msg}") + received_data += [json.loads(json_msg)] + except Exception as e: + print(f"Error parsing message: {e}") + + +def main(): + parser = argparse.ArgumentParser( + description="Receives Last Known State data on the customer MQTT topic" + ) + parser.add_argument("--endpoint-url", metavar="URL", help="IoT Core endpoint URL", default=None) + parser.add_argument("--region", metavar="REGION", help="IoT Core region", default="us-east-1") + parser.add_argument( + "--client-id", + metavar="ID", + help="Client ID for the MQTT connection. If omitted, a random one will be generated.", + default=f"lks-subscribe-script-{random.randint(0, 2**32 - 1):08x}", + ) + parser.add_argument("--run-time", metavar="SEC", help="Run time, zero is infinite", default="0") + parser.add_argument("--output-file", metavar="FILE", help="Output JSON file", default=None) + parser.add_argument( + "--iotfleetwise-topic-prefix", + metavar="PREFIX", + help="MQTT topic prefix", + default="$aws/iotfleetwise/", + ) + parser.add_argument("--vehicle-name", metavar="NAME", help="Vehicle name", required=True) + parser.add_argument( + "--template-name", metavar="NAME", help="State template name", required=True + ) + args = parser.parse_args() + + session = boto3.Session() + iot_client = session.client( + "iot", endpoint_url=args.endpoint_url, config=Config(region_name=args.region) + ) + iot_endpoint = iot_client.describe_endpoint(endpointType="iot:Data-ATS")["endpointAddress"] + credentials_provider = auth.AwsCredentialsProvider.new_default_chain() + + stop_future = Future() + + def on_lifecycle_connection_success(data: mqtt5.LifecycleConnectSuccessData): + print(f"MQTT connection succeeded {data=}") + + def on_lifecycle_connection_failure(data: mqtt5.LifecycleConnectFailureData): + print(f"MQTT connection failed {data=}") + + def on_lifecycle_disconnection(data: mqtt5.LifecycleDisconnectData): + print(f"MQTT disconnected {data=}") + + def on_lifecycle_stopped(data: mqtt5.LifecycleStoppedData): + print(f"MQTT client stopped {data=}") + stop_future.set_result(None) + + mqtt_connection = mqtt5_client_builder.websockets_with_default_aws_signing( + endpoint=iot_endpoint, + region=args.region, + on_publish_received=on_publish_received, + on_lifecycle_stopped=on_lifecycle_stopped, + on_lifecycle_connection_success=on_lifecycle_connection_success, + on_lifecycle_connection_failure=on_lifecycle_connection_failure, + on_lifecycle_disconnection=on_lifecycle_disconnection, + credentials_provider=credentials_provider, + client_id=args.client_id, + clean_session=False, + keep_alive_secs=30, + ) + mqtt_connection.start() + mqtt_topic = ( + f"{args.iotfleetwise_topic_prefix}vehicles/{args.vehicle_name}/" + f"last_known_state/{args.template_name}/data" + ) + subscribe_future = mqtt_connection.subscribe( + subscribe_packet=mqtt5.SubscribePacket( + subscriptions=[mqtt5.Subscription(topic_filter=mqtt_topic, qos=mqtt5.QoS.AT_LEAST_ONCE)] + ) + ) + subscribe_res = subscribe_future.result(1000) + print(f"Established mqtt subscription to {mqtt_topic} with {subscribe_res.reason_codes}") + run_time = 0 + try: + while int(args.run_time) == 0 or run_time < int(args.run_time): + time.sleep(1) + run_time += 1 + except KeyboardInterrupt: + pass + + mqtt_connection.stop() + stop_future.result(timeout=10) + + if args.output_file: + with open(args.output_file, "w") as fp: + fp.write(json.dumps(received_data, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tools/cloud/manage-service-role.sh b/tools/cloud/manage-service-role.sh new file mode 100755 index 00000000..5e62f219 --- /dev/null +++ b/tools/cloud/manage-service-role.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -feuo pipefail + +SERVICE_ROLE="" +SERVICE_PRINCIPAL="" +ACTIONS=() +RESOURCES=() +CLEAN_UP="false" + +parse_args() { + while [ "$#" -gt 0 ]; do + case $1 in + --service-role) + SERVICE_ROLE=$2 + shift + ;; + --service-principal) + SERVICE_PRINCIPAL=$2 + shift + ;; + --actions) + ACTIONS+=("$2") + shift + ;; + --resources) + RESOURCES+=("$2") + shift + ;; + --clean-up) + CLEAN_UP="true" + ;; + --help) + echo "Usage: $0 [OPTION]" >&2 + echo " --service-role Service role name" >&2 + echo " --service-principal Service principal" >&2 + echo " --actions CSV of actions to allow. Specify multiple times for multiple statements." >&2 + echo " --resources CSV of resources to allow. Specify multiple times for multiple statements." >&2 + echo " --clean-up Clean up the service role" >&2 + exit 0 + ;; + esac + shift + done + + if [ "${SERVICE_ROLE}" == "" ]; then + echo "Error: No service role provided" >&2 + exit -1 + fi + if ! ${CLEAN_UP} && [ "${SERVICE_PRINCIPAL}" == "" ]; then + echo "Error: No service principal provided" >&2 + exit -1 + fi + if ! ${CLEAN_UP} && [ ${#ACTIONS[@]} -eq 0 ]; then + echo "Error: No actions provided" >&2 + exit -1 + fi + if ! ${CLEAN_UP} && [ ${#ACTIONS[@]} -ne ${#RESOURCES[@]} ]; then + echo "Error: Number of actions doesn't match number of resources" >&2 + exit -1 + fi +} + +parse_args "$@" + +if $CLEAN_UP; then + echo "Checking if role ${SERVICE_ROLE} exists..." >&2 + if ! GET_ROLE_OUTPUT=`aws iam get-role --role-name ${SERVICE_ROLE} 2>&1`; then + if ! echo ${GET_ROLE_OUTPUT} | grep -q "NoSuchEntity"; then + echo ${GET_ROLE_OUTPUT} >&2 + exit -1 + fi + exit 0 + fi + + INLINE_POLICIES=$( + aws iam list-role-policies \ + --role-name ${SERVICE_ROLE} --query 'PolicyNames[]' --output text + ) + for POLICY_NAME in $INLINE_POLICIES; do + echo "Deleting inline policy: $POLICY_NAME from role: $SERVICE_ROLE" + aws iam delete-role-policy \ + --role-name ${SERVICE_ROLE} --policy-name ${POLICY_NAME} + done + + MAX_ATTEMPTS=60 + for ((i=0; ; i++)); do + echo "Deleting service role ${SERVICE_ROLE}..." >&2 + if DELETE_ROLE_OUTPUT=`aws iam delete-role --role-name ${SERVICE_ROLE} 2>&1`; then + break + elif ((i >= MAX_ATTEMPTS)); then + echo "Error: timeout deleting role: ${DELETE_ROLE_OUTPUT}" >&2 + exit -1 + else + sleep 1 + fi + done + exit 0 +fi + +echo "Checking if role ${SERVICE_ROLE} already exists..." >&2 +if GET_ROLE_OUTPUT=`aws iam get-role --role-name ${SERVICE_ROLE} 2>&1`; then + echo ${GET_ROLE_OUTPUT} | jq -r .Role.Arn + exit 0 +fi +if ! echo ${GET_ROLE_OUTPUT} | grep -q "NoSuchEntity"; then + echo ${GET_ROLE_OUTPUT} >&2 + exit -1 +fi + +SERVICE_ROLE_TRUST_POLICY=$(cat << EOF +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "${SERVICE_PRINCIPAL}" + ] + }, + "Action": "sts:AssumeRole" + } + ] +} +EOF +) +echo "Creating role ${SERVICE_ROLE}..." >&2 +SERVICE_ROLE_ARN=`aws iam create-role \ + --role-name "${SERVICE_ROLE}" \ + --assume-role-policy-document "${SERVICE_ROLE_TRUST_POLICY}" | jq -r .Role.Arn` +echo "Waiting for role to be created..." >&2 +aws iam wait role-exists --role-name "${SERVICE_ROLE}" +SERVICE_ROLE_POLICY='{"Version": "2012-10-17", "Statement": []}' +i=0 +for ACTION_LIST in "${ACTIONS[@]}"; do + SERVICE_ROLE_POLICY=`echo "${SERVICE_ROLE_POLICY}" | jq '.Statement+=[{"Effect": "Allow", "Action": [], "Resource": []}]'` + for ACTION in `echo ${ACTION_LIST} | tr ',' ' '`; do + SERVICE_ROLE_POLICY=`echo "${SERVICE_ROLE_POLICY}" | jq ".Statement[${i}].Action+=[\"${ACTION}\"]"` + done + ((++i)) +done +i=0 +for RESOURCE_LIST in "${RESOURCES[@]}"; do + for RESOURCE in `echo ${RESOURCE_LIST} | tr ',' ' '`; do + SERVICE_ROLE_POLICY=`echo "${SERVICE_ROLE_POLICY}" | jq ".Statement[${i}].Resource+=[\"${RESOURCE}\"]"` + done + ((++i)) +done +echo "Putting role policy..." >&2 +aws iam put-role-policy \ + --role-name "${SERVICE_ROLE}" \ + --policy-name ${SERVICE_ROLE}-policy \ + --policy-document "${SERVICE_ROLE_POLICY}" + +echo ${SERVICE_ROLE_ARN} diff --git a/tools/cloud/network-interface-custom-can-actuators.json b/tools/cloud/network-interface-custom-can-actuators.json new file mode 100644 index 00000000..af7d5231 --- /dev/null +++ b/tools/cloud/network-interface-custom-can-actuators.json @@ -0,0 +1,9 @@ +[ + { + "interfaceId": "CAN_ACTUATORS", + "type": "CUSTOM_DECODING_INTERFACE", + "customDecodingInterface": { + "name": "NamedSignalInterface" + } + } +] diff --git a/tools/cloud/network-interface-custom-location.json b/tools/cloud/network-interface-custom-location.json new file mode 100644 index 00000000..cc811066 --- /dev/null +++ b/tools/cloud/network-interface-custom-location.json @@ -0,0 +1,9 @@ +[ + { + "interfaceId": "LOCATION", + "type": "CUSTOM_DECODING_INTERFACE", + "customDecodingInterface": { + "name": "NamedSignalInterface" + } + } +] diff --git a/tools/cloud/network-interface-custom-named-signal.json b/tools/cloud/network-interface-custom-named-signal.json new file mode 100644 index 00000000..1e7723ca --- /dev/null +++ b/tools/cloud/network-interface-custom-named-signal.json @@ -0,0 +1,9 @@ +[ + { + "interfaceId": "NAMED_SIGNAL", + "type": "CUSTOM_DECODING_INTERFACE", + "customDecodingInterface": { + "name": "NamedSignalInterface" + } + } +] diff --git a/tools/cloud/network-interface-custom-someip.json b/tools/cloud/network-interface-custom-someip.json new file mode 100644 index 00000000..7bb75f5a --- /dev/null +++ b/tools/cloud/network-interface-custom-someip.json @@ -0,0 +1,9 @@ +[ + { + "interfaceId": "SOMEIP", + "type": "CUSTOM_DECODING_INTERFACE", + "customDecodingInterface": { + "name": "NamedSignalInterface" + } + } +] diff --git a/tools/cloud/network-interface-custom-uds-dtc.json b/tools/cloud/network-interface-custom-uds-dtc.json new file mode 100644 index 00000000..867b8eb3 --- /dev/null +++ b/tools/cloud/network-interface-custom-uds-dtc.json @@ -0,0 +1,9 @@ +[ + { + "interfaceId": "UDS_DTC", + "type": "CUSTOM_DECODING_INTERFACE", + "customDecodingInterface": { + "name": "NamedSignalInterface" + } + } +] diff --git a/tools/cloud/nuke-fw.sh b/tools/cloud/nuke-fw.sh index a43d8029..68e7692b 100755 --- a/tools/cloud/nuke-fw.sh +++ b/tools/cloud/nuke-fw.sh @@ -116,6 +116,17 @@ for ((i=0;;i++)); do aws iotfleetwise delete-model-manifest --region ${REGION} ${ENDPOINT_URL_OPTION} --name ${MODEL_MANIFEST_NAME} done +echo "Getting state templates..." +STATE_TEMPLATES=`aws iotfleetwise list-state-templates --region ${REGION} ${ENDPOINT_URL_OPTION}` +for ((i=0;;i++)); do + STATE_TEMPLATE=`echo "${STATE_TEMPLATES}" | jq -r .summaries[$i].name` + if [ "${STATE_TEMPLATE}" == "null" ]; then + break + fi + echo "Deleting state template ${STATE_TEMPLATE}..." + aws iotfleetwise delete-state-template --region ${REGION} ${ENDPOINT_URL_OPTION} --identifier ${STATE_TEMPLATE} +done + echo "Getting signal catalogs..." SIGNAL_CATALOGS=`aws iotfleetwise list-signal-catalogs --region ${REGION} ${ENDPOINT_URL_OPTION}` for ((i=0;;i++)); do diff --git a/tools/cloud/request-forward.sh b/tools/cloud/request-forward.sh new file mode 100755 index 00000000..5f410f44 --- /dev/null +++ b/tools/cloud/request-forward.sh @@ -0,0 +1,333 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(dirname "$(realpath "$0")")" +ACCOUNT_ID=`aws sts get-caller-identity --query "Account" --output text` +DISAMBIGUATOR="" +ENDPOINT_URL_OPTION="" +REGION="us-east-1" +VEHICLE_NAME="" +BUCKET_NAME="" +S3_UPLOAD=false +S3_SUFFIX="" +CAMPAIGN_NAME="" +END_TIME="" +END_TIME_ARGUMENT="" +FLEET_SIZE=1 +INCLUDE_SIGNALS="" +EXCLUDE_SIGNALS="" +TIMESTREAM_DB_NAME="" +JOB_STATUS_CHECK_RETRIES=360 # About 30mins + +if [ -f demo.env ]; then + source demo.env +fi + +# The demo script uses an iotfleetwise endpoint, this script uses an iot endpoint. +# So we discard any ENDPOINT_URL from demo.env. +unset ENDPOINT_URL +ENDPOINT_URL="" + +# If multiple campaigns were used in demo.sh, read just the first one +read CAMPAIGN_NAME __ <<< $(echo $CAMPAIGN_NAMES | tr "," " ") + +parse_args() { + while [ "$#" -gt 0 ]; do + case $1 in + --vehicle-name) + VEHICLE_NAME=$2 + shift + ;; + --fleet-size) + FLEET_SIZE=$2 + shift + ;; + --endpoint-url) + ENDPOINT_URL=$2 + shift + ;; + --enable-s3-upload) + S3_UPLOAD=true + ;; + --s3-suffix) + S3_SUFFIX=$2 + shift + ;; + --include-signals) + INCLUDE_SIGNALS="$2" + shift + ;; + --exclude-signals) + EXCLUDE_SIGNALS="$2" + shift + ;; + --timestream-db-name) + TIMESTREAM_DB_NAME="$2" + shift + ;; + --timestream-table-name) + TIMESTREAM_TABLE_NAME="$2" + shift + ;; + --disambiguator) + DISAMBIGUATOR=$2 + shift + ;; + --bucket-name) + BUCKET_NAME=$2 + shift + ;; + --region) + REGION=$2 + shift + ;; + --campaign-name) + CAMPAIGN_NAME=$2 + shift + ;; + --end-time) + END_TIME=$2 + shift + ;; + --help) + echo "Usage: $0 [OPTION]" + echo " --vehicle-name The vehicle name used in the fleet" + echo " --fleet-size Size of fleet, default: ${FLEET_SIZE}. When greater than 1," + echo " the instance number will be appended to each" + echo " Vehicle name after a '-', e.g. fwdemo-42" + echo " --disambiguator The unique string used by the demo.sh script to avoid resource name conflicts." + echo " Used to retrieve data after job execution." + echo " --enable-s3-upload Use demo data uploaded to S3 rather than Amazon Timestream" + echo " --s3-suffix The suffix of the bucket arn where the demo data is stored in S3" + echo " --include-signals Comma separated list of signals to include in HTML plot" + echo " --exclude-signals Comma separated list of signals to exclude from HTML plot" + echo " --timestream-db-name The name of the timestream database where the demo data is stored" + echo " --timestream-table-name The name of the timestream table where the demo data is stored" + echo " --bucket-name The name of the bucket created by the demo script where data should be forwarded to." + echo " Used to retrieve data after job execution." + echo " --campaign-name The campaign name for which data forward will be requested." + echo " If none is provided, defaults to the first campaign from demo.sh" + echo " --end-time