From f23e263ef6b5f4c6eea6c34b7d337166179b58d3 Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 30 Jun 2025 14:02:01 +0200 Subject: [PATCH 01/12] erase stop from command when in stop and next stop is current one --- docs/mission_module.md | 15 +++++++++++++++ .../mission_module/devices/AutonomyDevice.cpp | 13 +++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/mission_module.md b/docs/mission_module.md index d94cd00..2a788a2 100644 --- a/docs/mission_module.md +++ b/docs/mission_module.md @@ -19,6 +19,21 @@ The Autonomy keeps in memory the NAME of the next stop (the `nextStop` in the ac The Autonomy device sends the car status to the Mission Module. The status contains a field `State` with the value corresponding to the state of the device (`DRIVE`, `IN_STOP`, `IDLE`, `OBSTACLE`, `ERROR`). +When the connection between module gateway and external server is dropped, stops are being removed from commands when certain conditions are met. When a stop is finished while offline, it is added to a list of stops in the error message, which will be sent on reconnection. + +```mermaid +flowchart TD + command_gen(Command generation) + command_gen --> stop_size{{Any remaining stops in command?}} + stop_size -->|yes| in_stop{{IN_STOP car state in current status?}} + in_stop -->|yes| remove_check{{DRIVE car state in previous satus OR next station in command equal to next station in current status}} + remove_check -->|yes| remove_stop(Remove next stop from command) + error_aggr(Error aggregation) + error_aggr --> in_stop_2{{IN_STOP car state in current status?}} + in_stop_2 -->|yes| same_stop{{Last finished stop in error message different than current stop?}} + same_stop -->|yes| add_stop(Add current stop to finished stops in error message) +``` + # Messages ## Structure diff --git a/source/bringauto/modules/mission_module/devices/AutonomyDevice.cpp b/source/bringauto/modules/mission_module/devices/AutonomyDevice.cpp index 56b8859..8d4645c 100644 --- a/source/bringauto/modules/mission_module/devices/AutonomyDevice.cpp +++ b/source/bringauto/modules/mission_module/devices/AutonomyDevice.cpp @@ -38,13 +38,14 @@ int AutonomyDevice::generate_command(struct buffer *generated_command, const str auto newAutonomyStatus = protobuf::ProtobufHelper::parseAutonomyStatus(new_status); auto currentAutonomyCommand = protobuf::ProtobufHelper::parseAutonomyCommand(current_command); - if (currentAutonomyStatus.state() == MissionModule::AutonomyStatus_State_DRIVE && - newAutonomyStatus.state() == MissionModule::AutonomyStatus_State_IN_STOP) { - auto* stations = currentAutonomyCommand.mutable_stops(); - if (stations->size() > 0) { - stations->erase(stations->begin()); - } + auto* stations = currentAutonomyCommand.mutable_stops(); + if (stations->size() > 0 && + newAutonomyStatus.state() == MissionModule::AutonomyStatus_State_IN_STOP && + (currentAutonomyStatus.state() == MissionModule::AutonomyStatus_State_DRIVE || + newAutonomyStatus.nextstop().name() == stations->begin()->name())) { + stations->erase(stations->begin()); } + return protobuf::ProtobufHelper::serializeProtobufMessageToBuffer(generated_command, currentAutonomyCommand); } From 902caac040de56a57e64ea2d1db81ccae808e3ae Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Wed, 2 Jul 2025 11:39:23 +0200 Subject: [PATCH 02/12] added test for getting stuck in a stop --- .gitignore | 1 + README.md | 4 + tests/README.md | 26 ++ tests/libs/ExternalProtocol_pb2.py | 49 +++ tests/libs/InternalProtocol_pb2.py | 42 +++ tests/libs/MissionModule_pb2.py | 424 +++++++++++++++++++++++ tests/libs/sniff_mqtt.py | 219 ++++++++++++ tests/libs/testing_utilities.py | 69 ++++ tests/requirements.txt | 7 + tests/test_disconnect_on_stop_arrival.py | 80 +++++ 10 files changed, 921 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/libs/ExternalProtocol_pb2.py create mode 100644 tests/libs/InternalProtocol_pb2.py create mode 100644 tests/libs/MissionModule_pb2.py create mode 100644 tests/libs/sniff_mqtt.py create mode 100644 tests/libs/testing_utilities.py create mode 100755 tests/requirements.txt create mode 100644 tests/test_disconnect_on_stop_arrival.py diff --git a/.gitignore b/.gitignore index 52e89f5..ea9338f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea .vscode +.venv _*/ cmake-build-debug/ \ No newline at end of file diff --git a/README.md b/README.md index 1f9bca1..3fec930 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,7 @@ External Server Module Configuration is required as: - `api_key`: generated in Fleet Protocol HTTP API (script/new_admin.py) - `company_name`, `car_name`: used to identify the car in Fleet Protocol HTTP API - `max_requests_threshold_count`, `max_requests_threshold_period_ms`, `delay_after_threshold_reached_ms`, `retry_requests_delay_ms`: explained in [HTTP client README](./lib/fleet-v2-http-client/README.md) + +## Testing + +All tests are contained within the `tests` folder. [Testing README](./tests/README.md) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..925c962 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,26 @@ +# Mission Module Tests + +So far only manual tests are implemented. + +## Manual tests + +### Setup + +Set up an [etna](https://github.com/bringauto/etna) repository anywhere. + +```bash +cd tests +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +### MQTT disconnect on stop arrival + +```bash +python3 ./test_disconnect_on_stop_arrival.py --yaml +``` + +This test case runs the default etna scenario and disconnects the mqtt broker as soon as the car reaches the first stop. After roughly 2m 30s the car should reach the next stop automatically without mqtt connection. The test then reconnects the broker and checks for a status error message, which should contain the next stop in its finished stops list. If it doesn't exist, the test fails. + +Path to the etna docker-compose file needs to be provided as an argument. diff --git a/tests/libs/ExternalProtocol_pb2.py b/tests/libs/ExternalProtocol_pb2.py new file mode 100644 index 0000000..d38c392 --- /dev/null +++ b/tests/libs/ExternalProtocol_pb2.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ExternalProtocol.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import libs.InternalProtocol_pb2 as InternalProtocol__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x45xternalProtocol.proto\x12\x10\x45xternalProtocol\x1a\x16InternalProtocol.proto\"\xc7\x01\n\x0e\x45xternalServer\x12<\n\x0f\x63onnectResponse\x18\x01 \x01(\x0b\x32!.ExternalProtocol.ConnectResponseH\x00\x12:\n\x0estatusResponse\x18\x02 \x01(\x0b\x32 .ExternalProtocol.StatusResponseH\x00\x12,\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x19.ExternalProtocol.CommandH\x00\x42\r\n\x0bMessageType\"\xb7\x01\n\x0e\x45xternalClient\x12,\n\x07\x63onnect\x18\x01 \x01(\x0b\x32\x19.ExternalProtocol.ConnectH\x00\x12*\n\x06status\x18\x02 \x01(\x0b\x32\x18.ExternalProtocol.StatusH\x00\x12<\n\x0f\x63ommandResponse\x18\x03 \x01(\x0b\x32!.ExternalProtocol.CommandResponseH\x00\x42\r\n\x0bMessageType\"m\n\x07\x43onnect\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x0f\n\x07\x63ompany\x18\x02 \x01(\t\x12\x13\n\x0bvehicleName\x18\x03 \x01(\t\x12)\n\x07\x64\x65vices\x18\x04 \x03(\x0b\x32\x18.InternalProtocol.Device\"~\n\x0f\x43onnectResponse\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x34\n\x04type\x18\x02 \x01(\x0e\x32&.ExternalProtocol.ConnectResponse.Type\"\"\n\x04Type\x12\x06\n\x02OK\x10\x00\x12\x12\n\x0e\x41LREADY_LOGGED\x10\x01\"\x97\x02\n\x06Status\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x39\n\x0b\x64\x65viceState\x18\x02 \x01(\x0e\x32$.ExternalProtocol.Status.DeviceState\x12\x16\n\x0emessageCounter\x18\x03 \x01(\r\x12\x34\n\x0c\x64\x65viceStatus\x18\x04 \x01(\x0b\x32\x1e.InternalProtocol.DeviceStatus\x12\x19\n\x0c\x65rrorMessage\x18\x05 \x01(\x0cH\x00\x88\x01\x01\"E\n\x0b\x44\x65viceState\x12\x0e\n\nCONNECTING\x10\x00\x12\x0b\n\x07RUNNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x12\x0e\n\nDISCONNECT\x10\x03\x42\x0f\n\r_errorMessage\"\x80\x01\n\x0eStatusResponse\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x33\n\x04type\x18\x02 \x01(\x0e\x32%.ExternalProtocol.StatusResponse.Type\x12\x16\n\x0emessageCounter\x18\x03 \x01(\r\"\x0e\n\x04Type\x12\x06\n\x02OK\x10\x00\"l\n\x07\x43ommand\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x16\n\x0emessageCounter\x18\x02 \x01(\r\x12\x36\n\rdeviceCommand\x18\x03 \x01(\x0b\x32\x1f.InternalProtocol.DeviceCommand\"\xcb\x01\n\x0f\x43ommandResponse\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x34\n\x04type\x18\x02 \x01(\x0e\x32&.ExternalProtocol.CommandResponse.Type\x12\x16\n\x0emessageCounter\x18\x03 \x01(\r\"W\n\x04Type\x12\x06\n\x02OK\x10\x00\x12\x18\n\x14\x44\x45VICE_NOT_CONNECTED\x10\x01\x12\x18\n\x14\x44\x45VICE_NOT_SUPPORTED\x10\x02\x12\x13\n\x0fINVALID_COMMAND\x10\x03\x42>Z!../internal/pkg/ba_proto;ba_proto\xaa\x02\x18Google.Protobuf.ba_protob\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ExternalProtocol_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'Z!../internal/pkg/ba_proto;ba_proto\252\002\030Google.Protobuf.ba_proto' + _EXTERNALSERVER._serialized_start=69 + _EXTERNALSERVER._serialized_end=268 + _EXTERNALCLIENT._serialized_start=271 + _EXTERNALCLIENT._serialized_end=454 + _CONNECT._serialized_start=456 + _CONNECT._serialized_end=565 + _CONNECTRESPONSE._serialized_start=567 + _CONNECTRESPONSE._serialized_end=693 + _CONNECTRESPONSE_TYPE._serialized_start=659 + _CONNECTRESPONSE_TYPE._serialized_end=693 + _STATUS._serialized_start=696 + _STATUS._serialized_end=975 + _STATUS_DEVICESTATE._serialized_start=889 + _STATUS_DEVICESTATE._serialized_end=958 + _STATUSRESPONSE._serialized_start=978 + _STATUSRESPONSE._serialized_end=1106 + _STATUSRESPONSE_TYPE._serialized_start=659 + _STATUSRESPONSE_TYPE._serialized_end=673 + _COMMAND._serialized_start=1108 + _COMMAND._serialized_end=1216 + _COMMANDRESPONSE._serialized_start=1219 + _COMMANDRESPONSE._serialized_end=1422 + _COMMANDRESPONSE_TYPE._serialized_start=1335 + _COMMANDRESPONSE_TYPE._serialized_end=1422 +# @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/tests/libs/InternalProtocol_pb2.py b/tests/libs/InternalProtocol_pb2.py new file mode 100644 index 0000000..3014c30 --- /dev/null +++ b/tests/libs/InternalProtocol_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: InternalProtocol.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16InternalProtocol.proto\x12\x10InternalProtocol\"\x91\x01\n\x0eInternalClient\x12\x38\n\rdeviceConnect\x18\x01 \x01(\x0b\x32\x1f.InternalProtocol.DeviceConnectH\x00\x12\x36\n\x0c\x64\x65viceStatus\x18\x02 \x01(\x0b\x32\x1e.InternalProtocol.DeviceStatusH\x00\x42\r\n\x0bMessageType\"\xa3\x01\n\x0eInternalServer\x12H\n\x15\x64\x65viceConnectResponse\x18\x01 \x01(\x0b\x32\'.InternalProtocol.DeviceConnectResponseH\x00\x12\x38\n\rdeviceCommand\x18\x02 \x01(\x0b\x32\x1f.InternalProtocol.DeviceCommandH\x00\x42\r\n\x0bMessageType\"9\n\rDeviceConnect\x12(\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x18.InternalProtocol.Device\"\x98\x02\n\x15\x44\x65viceConnectResponse\x12J\n\x0cresponseType\x18\x01 \x01(\x0e\x32\x34.InternalProtocol.DeviceConnectResponse.ResponseType\x12(\n\x06\x64\x65vice\x18\x02 \x01(\x0b\x32\x18.InternalProtocol.Device\"\x88\x01\n\x0cResponseType\x12\x06\n\x02OK\x10\x00\x12\x15\n\x11\x41LREADY_CONNECTED\x10\x01\x12\x18\n\x14MODULE_NOT_SUPPORTED\x10\x02\x12\x18\n\x14\x44\x45VICE_NOT_SUPPORTED\x10\x03\x12%\n!HIGHER_PRIORITY_ALREADY_CONNECTED\x10\x04\"L\n\x0c\x44\x65viceStatus\x12(\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x18.InternalProtocol.Device\x12\x12\n\nstatusData\x18\x02 \x01(\x0c\"N\n\rDeviceCommand\x12(\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x18.InternalProtocol.Device\x12\x13\n\x0b\x63ommandData\x18\x02 \x01(\x0c\"\xe5\x01\n\x06\x44\x65vice\x12/\n\x06module\x18\x01 \x01(\x0e\x32\x1f.InternalProtocol.Device.Module\x12\x12\n\ndeviceType\x18\x02 \x01(\r\x12\x12\n\ndeviceRole\x18\x03 \x01(\t\x12\x12\n\ndeviceName\x18\x04 \x01(\t\x12\x10\n\x08priority\x18\x05 \x01(\r\"\\\n\x06Module\x12\x13\n\x0fRESERVED_MODULE\x10\x00\x12\x12\n\x0eMISSION_MODULE\x10\x01\x12\r\n\tIO_MODULE\x10\x02\x12\x13\n\x0e\x45XAMPLE_MODULE\x10\xe8\x07\"\x05\x08\x03\x10\xe7\x07\x42>Z!../internal/pkg/ba_proto;ba_proto\xaa\x02\x18Google.Protobuf.ba_protob\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'InternalProtocol_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'Z!../internal/pkg/ba_proto;ba_proto\252\002\030Google.Protobuf.ba_proto' + _INTERNALCLIENT._serialized_start=45 + _INTERNALCLIENT._serialized_end=190 + _INTERNALSERVER._serialized_start=193 + _INTERNALSERVER._serialized_end=356 + _DEVICECONNECT._serialized_start=358 + _DEVICECONNECT._serialized_end=415 + _DEVICECONNECTRESPONSE._serialized_start=418 + _DEVICECONNECTRESPONSE._serialized_end=698 + _DEVICECONNECTRESPONSE_RESPONSETYPE._serialized_start=562 + _DEVICECONNECTRESPONSE_RESPONSETYPE._serialized_end=698 + _DEVICESTATUS._serialized_start=700 + _DEVICESTATUS._serialized_end=776 + _DEVICECOMMAND._serialized_start=778 + _DEVICECOMMAND._serialized_end=856 + _DEVICE._serialized_start=859 + _DEVICE._serialized_end=1088 + _DEVICE_MODULE._serialized_start=996 + _DEVICE_MODULE._serialized_end=1088 +# @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/tests/libs/MissionModule_pb2.py b/tests/libs/MissionModule_pb2.py new file mode 100644 index 0000000..8962fad --- /dev/null +++ b/tests/libs/MissionModule_pb2.py @@ -0,0 +1,424 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: MissionModule.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='MissionModule.proto', + package='MissionModule', + syntax='proto3', + serialized_options=b'Z!../internal/pkg/ba_proto;ba_proto\252\002\030Google.Protobuf.ba_proto', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x13MissionModule.proto\x12\rMissionModule\"\xd5\x02\n\x0e\x41utonomyStatus\x12:\n\ttelemetry\x18\x01 \x01(\x0b\x32\'.MissionModule.AutonomyStatus.Telemetry\x12\x32\n\x05state\x18\x02 \x01(\x0e\x32#.MissionModule.AutonomyStatus.State\x12-\n\x08nextStop\x18\x03 \x01(\x0b\x32\x16.MissionModule.StationH\x00\x88\x01\x01\x1aS\n\tTelemetry\x12\r\n\x05speed\x18\x01 \x01(\x01\x12\x0c\n\x04\x66uel\x18\x02 \x01(\x01\x12)\n\x08position\x18\x03 \x01(\x0b\x32\x17.MissionModule.Position\"B\n\x05State\x12\x08\n\x04IDLE\x10\x00\x12\t\n\x05\x44RIVE\x10\x01\x12\x0b\n\x07IN_STOP\x10\x02\x12\x0c\n\x08OBSTACLE\x10\x03\x12\t\n\x05\x45RROR\x10\x04\x42\x0b\n\t_nextStop\"\xac\x01\n\x0f\x41utonomyCommand\x12%\n\x05stops\x18\x01 \x03(\x0b\x32\x16.MissionModule.Station\x12\r\n\x05route\x18\x02 \x01(\t\x12\x35\n\x06\x61\x63tion\x18\x03 \x01(\x0e\x32%.MissionModule.AutonomyCommand.Action\",\n\x06\x41\x63tion\x12\r\n\tNO_ACTION\x10\x00\x12\x08\n\x04STOP\x10\x01\x12\t\n\x05START\x10\x02\">\n\rAutonomyError\x12-\n\rfinishedStops\x18\x01 \x03(\x0b\x32\x16.MissionModule.Station\"B\n\x07Station\x12\x0c\n\x04name\x18\x01 \x01(\t\x12)\n\x08position\x18\x02 \x01(\x0b\x32\x17.MissionModule.Position\"A\n\x08Position\x12\x10\n\x08latitude\x18\x01 \x01(\x01\x12\x11\n\tlongitude\x18\x02 \x01(\x01\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x01\x42>Z!../internal/pkg/ba_proto;ba_proto\xaa\x02\x18Google.Protobuf.ba_protob\x06proto3' +) + + + +_AUTONOMYSTATUS_STATE = _descriptor.EnumDescriptor( + name='State', + full_name='MissionModule.AutonomyStatus.State', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='IDLE', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DRIVE', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IN_STOP', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OBSTACLE', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ERROR', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=301, + serialized_end=367, +) +_sym_db.RegisterEnumDescriptor(_AUTONOMYSTATUS_STATE) + +_AUTONOMYCOMMAND_ACTION = _descriptor.EnumDescriptor( + name='Action', + full_name='MissionModule.AutonomyCommand.Action', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='NO_ACTION', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STOP', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='START', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=511, + serialized_end=555, +) +_sym_db.RegisterEnumDescriptor(_AUTONOMYCOMMAND_ACTION) + + +_AUTONOMYSTATUS_TELEMETRY = _descriptor.Descriptor( + name='Telemetry', + full_name='MissionModule.AutonomyStatus.Telemetry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='speed', full_name='MissionModule.AutonomyStatus.Telemetry.speed', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='fuel', full_name='MissionModule.AutonomyStatus.Telemetry.fuel', index=1, + number=2, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='position', full_name='MissionModule.AutonomyStatus.Telemetry.position', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=216, + serialized_end=299, +) + +_AUTONOMYSTATUS = _descriptor.Descriptor( + name='AutonomyStatus', + full_name='MissionModule.AutonomyStatus', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='telemetry', full_name='MissionModule.AutonomyStatus.telemetry', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='state', full_name='MissionModule.AutonomyStatus.state', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='nextStop', full_name='MissionModule.AutonomyStatus.nextStop', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_AUTONOMYSTATUS_TELEMETRY, ], + enum_types=[ + _AUTONOMYSTATUS_STATE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='_nextStop', full_name='MissionModule.AutonomyStatus._nextStop', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=39, + serialized_end=380, +) + + +_AUTONOMYCOMMAND = _descriptor.Descriptor( + name='AutonomyCommand', + full_name='MissionModule.AutonomyCommand', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='stops', full_name='MissionModule.AutonomyCommand.stops', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='route', full_name='MissionModule.AutonomyCommand.route', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='action', full_name='MissionModule.AutonomyCommand.action', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _AUTONOMYCOMMAND_ACTION, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=383, + serialized_end=555, +) + + +_AUTONOMYERROR = _descriptor.Descriptor( + name='AutonomyError', + full_name='MissionModule.AutonomyError', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='finishedStops', full_name='MissionModule.AutonomyError.finishedStops', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=557, + serialized_end=619, +) + + +_STATION = _descriptor.Descriptor( + name='Station', + full_name='MissionModule.Station', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='MissionModule.Station.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='position', full_name='MissionModule.Station.position', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=621, + serialized_end=687, +) + + +_POSITION = _descriptor.Descriptor( + name='Position', + full_name='MissionModule.Position', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='latitude', full_name='MissionModule.Position.latitude', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='longitude', full_name='MissionModule.Position.longitude', index=1, + number=2, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='altitude', full_name='MissionModule.Position.altitude', index=2, + number=3, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=689, + serialized_end=754, +) + +_AUTONOMYSTATUS_TELEMETRY.fields_by_name['position'].message_type = _POSITION +_AUTONOMYSTATUS_TELEMETRY.containing_type = _AUTONOMYSTATUS +_AUTONOMYSTATUS.fields_by_name['telemetry'].message_type = _AUTONOMYSTATUS_TELEMETRY +_AUTONOMYSTATUS.fields_by_name['state'].enum_type = _AUTONOMYSTATUS_STATE +_AUTONOMYSTATUS.fields_by_name['nextStop'].message_type = _STATION +_AUTONOMYSTATUS_STATE.containing_type = _AUTONOMYSTATUS +_AUTONOMYSTATUS.oneofs_by_name['_nextStop'].fields.append( + _AUTONOMYSTATUS.fields_by_name['nextStop']) +_AUTONOMYSTATUS.fields_by_name['nextStop'].containing_oneof = _AUTONOMYSTATUS.oneofs_by_name['_nextStop'] +_AUTONOMYCOMMAND.fields_by_name['stops'].message_type = _STATION +_AUTONOMYCOMMAND.fields_by_name['action'].enum_type = _AUTONOMYCOMMAND_ACTION +_AUTONOMYCOMMAND_ACTION.containing_type = _AUTONOMYCOMMAND +_AUTONOMYERROR.fields_by_name['finishedStops'].message_type = _STATION +_STATION.fields_by_name['position'].message_type = _POSITION +DESCRIPTOR.message_types_by_name['AutonomyStatus'] = _AUTONOMYSTATUS +DESCRIPTOR.message_types_by_name['AutonomyCommand'] = _AUTONOMYCOMMAND +DESCRIPTOR.message_types_by_name['AutonomyError'] = _AUTONOMYERROR +DESCRIPTOR.message_types_by_name['Station'] = _STATION +DESCRIPTOR.message_types_by_name['Position'] = _POSITION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +AutonomyStatus = _reflection.GeneratedProtocolMessageType('AutonomyStatus', (_message.Message,), { + + 'Telemetry' : _reflection.GeneratedProtocolMessageType('Telemetry', (_message.Message,), { + 'DESCRIPTOR' : _AUTONOMYSTATUS_TELEMETRY, + '__module__' : 'MissionModule_pb2' + # @@protoc_insertion_point(class_scope:MissionModule.AutonomyStatus.Telemetry) + }) + , + 'DESCRIPTOR' : _AUTONOMYSTATUS, + '__module__' : 'MissionModule_pb2' + # @@protoc_insertion_point(class_scope:MissionModule.AutonomyStatus) + }) +_sym_db.RegisterMessage(AutonomyStatus) +_sym_db.RegisterMessage(AutonomyStatus.Telemetry) + +AutonomyCommand = _reflection.GeneratedProtocolMessageType('AutonomyCommand', (_message.Message,), { + 'DESCRIPTOR' : _AUTONOMYCOMMAND, + '__module__' : 'MissionModule_pb2' + # @@protoc_insertion_point(class_scope:MissionModule.AutonomyCommand) + }) +_sym_db.RegisterMessage(AutonomyCommand) + +AutonomyError = _reflection.GeneratedProtocolMessageType('AutonomyError', (_message.Message,), { + 'DESCRIPTOR' : _AUTONOMYERROR, + '__module__' : 'MissionModule_pb2' + # @@protoc_insertion_point(class_scope:MissionModule.AutonomyError) + }) +_sym_db.RegisterMessage(AutonomyError) + +Station = _reflection.GeneratedProtocolMessageType('Station', (_message.Message,), { + 'DESCRIPTOR' : _STATION, + '__module__' : 'MissionModule_pb2' + # @@protoc_insertion_point(class_scope:MissionModule.Station) + }) +_sym_db.RegisterMessage(Station) + +Position = _reflection.GeneratedProtocolMessageType('Position', (_message.Message,), { + 'DESCRIPTOR' : _POSITION, + '__module__' : 'MissionModule_pb2' + # @@protoc_insertion_point(class_scope:MissionModule.Position) + }) +_sym_db.RegisterMessage(Position) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/tests/libs/sniff_mqtt.py b/tests/libs/sniff_mqtt.py new file mode 100644 index 0000000..a3fc1d0 --- /dev/null +++ b/tests/libs/sniff_mqtt.py @@ -0,0 +1,219 @@ +import time, threading + +import libs.ExternalProtocol_pb2 +import libs.InternalProtocol_pb2 +import libs.MissionModule_pb2 + +from paho.mqtt.client import ( + Client, MQTTMessage, MQTTv311 +) +from paho.mqtt.enums import CallbackAPIVersion + + + +MISSION_MODULE_NUMBER = 1 +MAX_MESSAGES = 100 + + +def mission_module_device() -> libs.InternalProtocol_pb2.Device: + device = libs.InternalProtocol_pb2.Device() + device.module = MISSION_MODULE_NUMBER + device.deviceType = 1 + device.deviceRole = 'autonomy' + device.deviceName = 'Autonomy' + return device + + +class MqttMonitoring: + def __init__(self, verbose: bool = False, store_statuses: bool = False, store_commands: bool = False, logging: bool = False): + self._create_client() + self._verbose = verbose + self._logging = logging + self._store_statuses = store_statuses + self._store_commands = store_commands + self._statuses = [] + self._commands = [] + self._status_lock = threading.Lock() + self._status_condition = threading.Condition(self._status_lock) + self._command_lock = threading.Lock() + self._command_condition = threading.Condition(self._command_lock) + self._mission_module_command_counter = 0 + self._mission_module_session_id = '' + + + def _create_client(self): + self._client =Client( + client_id='Monitoring Test', + protocol=MQTTv311, + transport='tcp', + callback_api_version=CallbackAPIVersion.VERSION2, + reconnect_on_failure=True, + ) + self._client.on_connect = self._on_connect + self._client.on_message = self._on_message + + + def connect(self, host: str = '127.0.0.1', port: int = 1883, keepalive: int = 5) -> None: + """Connects to the MQTT broker with retry logic.""" + connected = False + while not connected: + try: + self._create_client() + self._client.connect(host=host, port=port, keepalive=keepalive) + connected = True + except ConnectionRefusedError: + time.sleep(2) + + + def start(self) -> None: + """Starts the MQTT client loop to listen for messages.""" + self._client.loop_forever(timeout=60) + + + def stop(self) -> None: + """Stops the MQTT client loop and disconnects from the broker.""" + self._client.loop_stop() + self._client.disconnect() + + + def _on_connect(self, client, userdata, flags, rc, properties) -> None: + self._client.subscribe("#", qos=1) + + + def _on_message(self, client, userdata, message: MQTTMessage) -> None: + if self._verbose: self._print(f'topic: {message.topic}') + message_type = message.topic.split('/')[-1] + if message_type == 'module_gateway': + self._print('\nMessage from module gateway:') + self._decode_module_gateway_message(message.payload) + elif message_type == 'external_server': + self._print('\nMessage from external server:') + self._decode_external_server_message(message.payload) + else: + self._print(f'\nUnknown message type: {message_type}') + + + def _decode_module_gateway_message(self, payload: bytes): + message = libs.ExternalProtocol_pb2.ExternalClient() + message.ParseFromString(payload) + if message.HasField('status'): + self._handle_status(getattr(message, 'status').deviceStatus) + elif message.HasField('connect'): + self._print('\tConnect message sent') + elif message.HasField('commandResponse'): + self._print('\tCommand response sent') + else: + self._print(f'\tMessage with unhandled type') + if self._verbose: self._print(message) + + + def _decode_external_server_message(self, payload: bytes): + message = libs.ExternalProtocol_pb2.ExternalServer() + message.ParseFromString(payload) + if message.HasField('command'): + self._handle_command(getattr(message, 'command')) + elif message.HasField('statusResponse'): + self._print('\tStatus response sent') + elif message.HasField('connectResponse'): + self._print('\tConnect response sent') + else: + self._print(f'\tMessage with unhandled type') + if self._verbose: self._print(message) + + + def _handle_status(self, status): + if status.device.module == MISSION_MODULE_NUMBER: + self._handle_mission_module_status(status.statusData) + else: + self._print(f'\tStatus from unsupported module: {status.device.module}') + + + def _handle_mission_module_status(self, status): + mission_status = libs.MissionModule_pb2.AutonomyStatus() + mission_status.ParseFromString(status) + if self._store_statuses: + with self._status_condition: + self._statuses.append(mission_status) + if len(self._statuses) > MAX_MESSAGES: + self._statuses.pop(0) + self._status_condition.notify_all() + self._print(f'\tMission Module Status:\n--------------------\n{mission_status}\n--------------------') + + + def get_mission_module_statuses(self): + """Returns a copy of the stored mission module statuses and clears the storage.""" + with self._status_condition: + while not self._statuses: + self._status_condition.wait() + ret = self._statuses.copy() + self._statuses.clear() + return ret + + + def _handle_command(self, command): + if command.deviceCommand.device.module == MISSION_MODULE_NUMBER: + self._mission_module_command_counter = command.messageCounter + self._mission_module_session_id = command.sessionId + self._handle_mission_module_command(command.deviceCommand.commandData) + else: + self._print(f'\tCommand for unsupported module: {command.deviceCommand.device.module}') + + + def _handle_mission_module_command(self, command): + mission_command = libs.MissionModule_pb2.AutonomyCommand() + mission_command.ParseFromString(command) + if self._store_commands: + with self._command_condition: + self._commands.append(mission_command) + if len(self._commands) > MAX_MESSAGES: + self._commands.pop(0) + self._command_condition.notify_all() + self._print(f'\tMission Module Command:\n--------------------\n{mission_command}\n--------------------') + + + def get_mission_module_commands(self): + """Returns a copy of the stored mission module commands and clears the storage.""" + with self._command_condition: + while not self._commands: + self._command_condition.wait() + ret = self._commands.copy() + self._commands.clear() + return ret + + + def _print(self, message: str): + """Prints a message if logging is enabled.""" + if self._logging: + print(message) + + + def send_mission_module_command(self, payload: libs.MissionModule_pb2.AutonomyCommand) -> None: + """Sends a mission module command to the MQTT broker.""" + if not isinstance(payload, libs.MissionModule_pb2.AutonomyCommand): + raise TypeError('Command must be of type AutonomyCommand') + + command = libs.ExternalProtocol_pb2.Command() + command.sessionId = self._mission_module_session_id + command.messageCounter = self._mission_module_command_counter + 1 + + device_command = libs.InternalProtocol_pb2.DeviceCommand() + device_command.device.CopyFrom(mission_module_device()) + device_command.commandData = payload.SerializeToString() + command.deviceCommand.CopyFrom(device_command) + + msg = libs.ExternalProtocol_pb2.ExternalServer() + msg.command.CopyFrom(command) + + self._client.publish( + topic=f'bringauto/virtual_vehicle/external_server', + payload=msg.SerializeToString(), + qos=1 + ) + + + +if __name__ == '__main__': + print('Waiting for MQTT broker connection...') + mqtt_monitoring = MqttMonitoring(logging=True) + mqtt_monitoring.connect() + mqtt_monitoring.start() diff --git a/tests/libs/testing_utilities.py b/tests/libs/testing_utilities.py new file mode 100644 index 0000000..50a5835 --- /dev/null +++ b/tests/libs/testing_utilities.py @@ -0,0 +1,69 @@ +import subprocess +import fleet_http_client_python # type: ignore + + + +PROFILE_ALL = 'all' +PROFILE_MQTT = 'vernemq-testing' +NAME_MQTT = 'scripts-vernemq-1' + +STATE_IDLE = 0 +STATE_DRIVE = 1 +STATE_IN_STOP = 2 + + +def get_baseline_docker_command(yaml_file: str) -> list[str]: + return ['docker', 'compose', '--file', yaml_file ] + + +def run_docker_compose_all(yaml_file: str): + subprocess.run(get_baseline_docker_command(yaml_file) + ['--profile', PROFILE_ALL, 'up', '-d'], check=True) + + +def stop_docker_compose_all(yaml_file: str): + subprocess.run(get_baseline_docker_command(yaml_file) + ['--profile', PROFILE_ALL, 'down'], check=True) + + +def run_docker_compose_profiles(profiles: list[str], yaml_file: str): + command = get_baseline_docker_command(yaml_file) + for profile in profiles: + command += ['--profile', profile] + command += ['up', '-d'] + subprocess.run(command, check=True) + + +def stop_docker_compose_profiles(profiles: list[str], yaml_file: str): + command = get_baseline_docker_command(yaml_file) + for profile in profiles: + command += ['--profile', profile] + command += ['down'] + subprocess.run(command, check=True) + + +def kill_docker_component(name: str): + subprocess.run(['docker', 'stop', name], check=True) + + +def check_car_state(statuses: list, state: int) -> bool: + for status in statuses: + if status.state == state: + return True + return False + + +def check_car_state_and_next_stop(statuses: list, state: int, next_stop: str) -> bool: + for status in statuses: + if status.state == state and status.nextStop.name == next_stop: + return True + return False + + +def get_status_errors() -> list[fleet_http_client_python.Message]: + configuration = fleet_http_client_python.Configuration(host="http://localhost:8080/v2/protocol", + api_key={"AdminAuth": "ProtocolStaticAccessKey"}) + api_client = fleet_http_client_python.ApiClient(configuration) + device_api = fleet_http_client_python.DeviceApi(api_client) + statuses = device_api.list_statuses(company_name='bringauto', + car_name='virtual_vehicle', + since=0) + return [status for status in statuses if status.payload.message_type == 'STATUS_ERROR' and status.device_id.module_id == 1] diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100755 index 0000000..3a6634a --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,7 @@ +paho-mqtt >= 1.5.1 +protobuf == 3.20 +requests >= 2.26.0 +pydantic >= 2.7.1 +python-dateutil >= 2.9.0.post0 +fleet_management_http_client_python @ git+https://github.com/bringauto/fleet-management-http-client-python.git@v3.1.5 +fleet_http_client_python @ git+https://github.com/bringauto/fleet-http-client-python.git@v2.8.5 diff --git a/tests/test_disconnect_on_stop_arrival.py b/tests/test_disconnect_on_stop_arrival.py new file mode 100644 index 0000000..dd98d8d --- /dev/null +++ b/tests/test_disconnect_on_stop_arrival.py @@ -0,0 +1,80 @@ +import argparse +import threading +import time + +import libs.MissionModule_pb2 +import libs.sniff_mqtt +import libs.testing_utilities as _utils + + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Test disconnecting from MQTT broker on stop arrival.') + parser.add_argument('--yaml', type=str, help='Path to the YAML file for Docker Compose.') + args = parser.parse_args() + + if not args.yaml: + print('No YAML file provided. Please use --yaml to specify the path to the Docker Compose YAML file.') + exit(1) + + print('Starting test: Disconnect on stop arrival') + _utils.run_docker_compose_all(args.yaml) + print('Waiting for MQTT broker to start...') + time.sleep(5) + mqtt_monitoring = libs.sniff_mqtt.MqttMonitoring(store_statuses=True, store_commands=True, logging=True, verbose=True) + mqtt_monitoring.connect() + sniff_thread = threading.Thread(target=mqtt_monitoring.start, daemon=True) + sniff_thread.start() + + try: + print('Waiting for car to arrive in stop...') + while True: + statuses = mqtt_monitoring.get_mission_module_statuses() + #print(statuses) + if _utils.check_car_state_and_next_stop(statuses, _utils.STATE_IN_STOP, 'Svatopluka Čecha A'): + print('Arrived in stop') + + command = libs.MissionModule_pb2.AutonomyCommand() + command.action = libs.MissionModule_pb2.AutonomyCommand.Action.START + command.route = 'Moravské náměstí 2' + + stops: list[libs.MissionModule_pb2.Station()] = [] + stops.append(libs.MissionModule_pb2.Station(name='Svatopluka Čecha A', + position=libs.MissionModule_pb2.Position(latitude=49.221645, + longitude=16.59081))) + stops.append(libs.MissionModule_pb2.Station(name='Těšínská', + position=libs.MissionModule_pb2.Position(latitude=49.22316, + longitude=16.58995))) + command.stops.extend(stops) + + mqtt_monitoring.send_mission_module_command(payload=command) + + _utils.kill_docker_component(_utils.NAME_MQTT) + print('Waiting for 2m 45s for car to arrive at the next stop...') + time.sleep(165) + _utils.run_docker_compose_profiles([_utils.PROFILE_MQTT], args.yaml) + time.sleep(5) + break + + statuses = _utils.get_status_errors() + #print(statuses) + if len(statuses) != 1: + print('Received unexpected number of status errors:', len(statuses)) + else: + finished_stops = statuses[0].payload.data.to_dict()['finishedStops'] + if len(finished_stops) != 1: + print('Unexpected number of finished stops:', len(finished_stops)) + elif finished_stops[0]['name'] != 'Těšínská': + print('Unexpected finished stop name:', finished_stops[0].name) + else: + print('Test passed: Car arrived at the correct stop after disconnecting from the MQTT broker.') + + + except KeyboardInterrupt: + print('KeyboardInterrupt received, stopping...') + except Exception as e: + print(f'Error: {e}') + + mqtt_monitoring.stop() + sniff_thread.join() + _utils.stop_docker_compose_all(args.yaml) From 20a65c69be5c2e1caac6c018dc3aefadffe6102b Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Wed, 2 Jul 2025 15:23:12 +0200 Subject: [PATCH 03/12] update test to not rely on random integration layer behavior --- CMakeLists.txt | 2 +- tests/README.md | 5 +++ tests/libs/testing_utilities.py | 6 ++- tests/test_disconnect_on_stop_arrival.py | 52 ++++++++++++++++-------- 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dbccd66..64c6843 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ INCLUDE(CheckPIESupported) CHECK_PIE_SUPPORTED() SET(CMAKE_POSITION_INDEPENDENT_CODE ON) -SET(MISSION_MODULE_VERSION 1.2.12) +SET(MISSION_MODULE_VERSION 1.2.13) OPTION(BRINGAUTO_INSTALL "Configure install" OFF) OPTION(BRINGAUTO_PACKAGE "Configure package creation" OFF) diff --git a/tests/README.md b/tests/README.md index 925c962..a302ba6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -24,3 +24,8 @@ python3 ./test_disconnect_on_stop_arrival.py --yaml ` : path to etna docker-compose file. +- `--logging` : enables logging of communication of mission module +- `--verbose` : enables logging of all mqtt communication diff --git a/tests/libs/testing_utilities.py b/tests/libs/testing_utilities.py index 50a5835..2b6b11c 100644 --- a/tests/libs/testing_utilities.py +++ b/tests/libs/testing_utilities.py @@ -4,8 +4,7 @@ PROFILE_ALL = 'all' -PROFILE_MQTT = 'vernemq-testing' -NAME_MQTT = 'scripts-vernemq-1' +PROFILE_MQTT = 'mqtt' STATE_IDLE = 0 STATE_DRIVE = 1 @@ -45,6 +44,7 @@ def kill_docker_component(name: str): def check_car_state(statuses: list, state: int) -> bool: + """Check if any of the statuses has the given state.""" for status in statuses: if status.state == state: return True @@ -52,6 +52,7 @@ def check_car_state(statuses: list, state: int) -> bool: def check_car_state_and_next_stop(statuses: list, state: int, next_stop: str) -> bool: + """Check if any of the statuses has the given state and next stop.""" for status in statuses: if status.state == state and status.nextStop.name == next_stop: return True @@ -59,6 +60,7 @@ def check_car_state_and_next_stop(statuses: list, state: int, next_stop: str) -> def get_status_errors() -> list[fleet_http_client_python.Message]: + """Get all status errors from the fleet HTTP API.""" configuration = fleet_http_client_python.Configuration(host="http://localhost:8080/v2/protocol", api_key={"AdminAuth": "ProtocolStaticAccessKey"}) api_client = fleet_http_client_python.ApiClient(configuration) diff --git a/tests/test_disconnect_on_stop_arrival.py b/tests/test_disconnect_on_stop_arrival.py index dd98d8d..46d14f9 100644 --- a/tests/test_disconnect_on_stop_arrival.py +++ b/tests/test_disconnect_on_stop_arrival.py @@ -8,20 +8,45 @@ +def create_command() -> libs.MissionModule_pb2.AutonomyCommand: + """Creates a command that is expected in the default etna scenario since it reaches the first stop.""" + command = libs.MissionModule_pb2.AutonomyCommand() + command.action = libs.MissionModule_pb2.AutonomyCommand.Action.START + command.route = 'Moravské náměstí 2' + + stops: list[libs.MissionModule_pb2.Station()] = [] + stops.append(libs.MissionModule_pb2.Station(name='Svatopluka Čecha A', + position=libs.MissionModule_pb2.Position(latitude=49.221645, + longitude=16.59081))) + stops.append(libs.MissionModule_pb2.Station(name='Těšínská', + position=libs.MissionModule_pb2.Position(latitude=49.22316, + longitude=16.58995))) + command.stops.extend(stops) + return command + + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Test disconnecting from MQTT broker on stop arrival.') parser.add_argument('--yaml', type=str, help='Path to the YAML file for Docker Compose.') + parser.add_argument('--logging', action='store_true', help='Enable mqtt sniffer logging for the test run.') + parser.add_argument('--verbose', action='store_true', help='Enable verbose output for mqtt sniffer logs.') args = parser.parse_args() if not args.yaml: print('No YAML file provided. Please use --yaml to specify the path to the Docker Compose YAML file.') exit(1) + # Construct the vernemq docker component name based on the YAML file path. + mqtt_component_name = str(args.yaml).split('/')[-2] + '-vernemq-1' + print('Starting test: Disconnect on stop arrival') _utils.run_docker_compose_all(args.yaml) print('Waiting for MQTT broker to start...') time.sleep(5) - mqtt_monitoring = libs.sniff_mqtt.MqttMonitoring(store_statuses=True, store_commands=True, logging=True, verbose=True) + mqtt_monitoring = libs.sniff_mqtt.MqttMonitoring(store_statuses=True, + store_commands=True, + logging=args.logging, + verbose=args.verbose) mqtt_monitoring.connect() sniff_thread = threading.Thread(target=mqtt_monitoring.start, daemon=True) sniff_thread.start() @@ -31,31 +56,22 @@ while True: statuses = mqtt_monitoring.get_mission_module_statuses() #print(statuses) + # Wait until the car reaches the Svatopluka Čecha A stop. if _utils.check_car_state_and_next_stop(statuses, _utils.STATE_IN_STOP, 'Svatopluka Čecha A'): print('Arrived in stop') - - command = libs.MissionModule_pb2.AutonomyCommand() - command.action = libs.MissionModule_pb2.AutonomyCommand.Action.START - command.route = 'Moravské náměstí 2' + # Send a command that would normally cause the car to be stuck in the stop. Disconnect the MQTT broker at the same time. + mqtt_monitoring.send_mission_module_command(payload=create_command()) + _utils.kill_docker_component(mqtt_component_name) - stops: list[libs.MissionModule_pb2.Station()] = [] - stops.append(libs.MissionModule_pb2.Station(name='Svatopluka Čecha A', - position=libs.MissionModule_pb2.Position(latitude=49.221645, - longitude=16.59081))) - stops.append(libs.MissionModule_pb2.Station(name='Těšínská', - position=libs.MissionModule_pb2.Position(latitude=49.22316, - longitude=16.58995))) - command.stops.extend(stops) - - mqtt_monitoring.send_mission_module_command(payload=command) - - _utils.kill_docker_component(_utils.NAME_MQTT) print('Waiting for 2m 45s for car to arrive at the next stop...') time.sleep(165) _utils.run_docker_compose_profiles([_utils.PROFILE_MQTT], args.yaml) - time.sleep(5) + + print('Waiting for 30 seconds to ensure the car has time to process the reconnection...') + time.sleep(30) break + # Check if upon reconnection the car already arrived at the Těšínská stop. statuses = _utils.get_status_errors() #print(statuses) if len(statuses) != 1: From 42913fc9bcfdc2f70bbc5afecbf4fca6abe71ed7 Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 7 Jul 2025 10:31:19 +0200 Subject: [PATCH 04/12] coderabbit suggestions --- tests/libs/sniff_mqtt.py | 6 +++--- tests/libs/testing_utilities.py | 2 +- tests/requirements.txt | 8 ++++---- tests/test_disconnect_on_stop_arrival.py | 15 ++++++++++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/tests/libs/sniff_mqtt.py b/tests/libs/sniff_mqtt.py index a3fc1d0..ab246cc 100644 --- a/tests/libs/sniff_mqtt.py +++ b/tests/libs/sniff_mqtt.py @@ -103,7 +103,7 @@ def _decode_module_gateway_message(self, payload: bytes): elif message.HasField('commandResponse'): self._print('\tCommand response sent') else: - self._print(f'\tMessage with unhandled type') + self._print('\tMessage with unhandled type') if self._verbose: self._print(message) @@ -117,7 +117,7 @@ def _decode_external_server_message(self, payload: bytes): elif message.HasField('connectResponse'): self._print('\tConnect response sent') else: - self._print(f'\tMessage with unhandled type') + self._print('\tMessage with unhandled type') if self._verbose: self._print(message) @@ -205,7 +205,7 @@ def send_mission_module_command(self, payload: libs.MissionModule_pb2.AutonomyCo msg.command.CopyFrom(command) self._client.publish( - topic=f'bringauto/virtual_vehicle/external_server', + topic='bringauto/virtual_vehicle/external_server', payload=msg.SerializeToString(), qos=1 ) diff --git a/tests/libs/testing_utilities.py b/tests/libs/testing_utilities.py index 2b6b11c..fb14e46 100644 --- a/tests/libs/testing_utilities.py +++ b/tests/libs/testing_utilities.py @@ -39,7 +39,7 @@ def stop_docker_compose_profiles(profiles: list[str], yaml_file: str): subprocess.run(command, check=True) -def kill_docker_component(name: str): +def stop_docker_component(name: str): subprocess.run(['docker', 'stop', name], check=True) diff --git a/tests/requirements.txt b/tests/requirements.txt index 3a6634a..3757add 100755 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,7 +1,7 @@ -paho-mqtt >= 1.5.1 +paho-mqtt == 2.1.0 protobuf == 3.20 -requests >= 2.26.0 -pydantic >= 2.7.1 -python-dateutil >= 2.9.0.post0 +requests == 2.26.0 +pydantic == 2.7.1 +python-dateutil == 2.9.0.post0 fleet_management_http_client_python @ git+https://github.com/bringauto/fleet-management-http-client-python.git@v3.1.5 fleet_http_client_python @ git+https://github.com/bringauto/fleet-http-client-python.git@v2.8.5 diff --git a/tests/test_disconnect_on_stop_arrival.py b/tests/test_disconnect_on_stop_arrival.py index 46d14f9..5a5cd78 100644 --- a/tests/test_disconnect_on_stop_arrival.py +++ b/tests/test_disconnect_on_stop_arrival.py @@ -36,6 +36,8 @@ def create_command() -> libs.MissionModule_pb2.AutonomyCommand: print('No YAML file provided. Please use --yaml to specify the path to the Docker Compose YAML file.') exit(1) + test_passed = False + # Construct the vernemq docker component name based on the YAML file path. mqtt_component_name = str(args.yaml).split('/')[-2] + '-vernemq-1' @@ -61,7 +63,7 @@ def create_command() -> libs.MissionModule_pb2.AutonomyCommand: print('Arrived in stop') # Send a command that would normally cause the car to be stuck in the stop. Disconnect the MQTT broker at the same time. mqtt_monitoring.send_mission_module_command(payload=create_command()) - _utils.kill_docker_component(mqtt_component_name) + _utils.stop_docker_component(mqtt_component_name) print('Waiting for 2m 45s for car to arrive at the next stop...') time.sleep(165) @@ -81,9 +83,10 @@ def create_command() -> libs.MissionModule_pb2.AutonomyCommand: if len(finished_stops) != 1: print('Unexpected number of finished stops:', len(finished_stops)) elif finished_stops[0]['name'] != 'Těšínská': - print('Unexpected finished stop name:', finished_stops[0].name) + print('Unexpected finished stop name:', finished_stops[0]['name']) else: - print('Test passed: Car arrived at the correct stop after disconnecting from the MQTT broker.') + print('Car arrived at the correct stop after disconnecting from the MQTT broker.') + test_passed = True except KeyboardInterrupt: @@ -94,3 +97,9 @@ def create_command() -> libs.MissionModule_pb2.AutonomyCommand: mqtt_monitoring.stop() sniff_thread.join() _utils.stop_docker_compose_all(args.yaml) + + if test_passed: + print('\n\033[92mTest passed.\033[0m') + else: + print('\n\033[31mTest failed.\033[0m') + exit(1) From 75f329e93d162c89893a3cb3e6222ff2b0416cae Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 7 Jul 2025 10:36:00 +0200 Subject: [PATCH 05/12] fix type hint --- tests/test_disconnect_on_stop_arrival.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_disconnect_on_stop_arrival.py b/tests/test_disconnect_on_stop_arrival.py index 5a5cd78..51341ab 100644 --- a/tests/test_disconnect_on_stop_arrival.py +++ b/tests/test_disconnect_on_stop_arrival.py @@ -14,7 +14,7 @@ def create_command() -> libs.MissionModule_pb2.AutonomyCommand: command.action = libs.MissionModule_pb2.AutonomyCommand.Action.START command.route = 'Moravské náměstí 2' - stops: list[libs.MissionModule_pb2.Station()] = [] + stops: list[libs.MissionModule_pb2.Station] = [] stops.append(libs.MissionModule_pb2.Station(name='Svatopluka Čecha A', position=libs.MissionModule_pb2.Position(latitude=49.221645, longitude=16.59081))) From 53ed0e2f2f8b0c705728c8213e763e1864eee122 Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 14 Jul 2025 13:36:23 +0200 Subject: [PATCH 06/12] removed python protobuf files from tests directory --- lib/protobuf-mission-module/.gitignore | 2 + .../MissionModule_pb2.py | 0 lib/protobuf-mission-module/pyproject.toml | 7 +++ tests/libs/ExternalProtocol_pb2.py | 49 ------------------- tests/libs/InternalProtocol_pb2.py | 42 ---------------- tests/libs/sniff_mqtt.py | 28 +++++------ tests/requirements.txt | 2 + tests/test_disconnect_on_stop_arrival.py | 23 ++++----- 8 files changed, 37 insertions(+), 116 deletions(-) create mode 100644 lib/protobuf-mission-module/.gitignore rename {tests/libs => lib/protobuf-mission-module/mission_module_protobuf_files}/MissionModule_pb2.py (100%) create mode 100644 lib/protobuf-mission-module/pyproject.toml delete mode 100644 tests/libs/ExternalProtocol_pb2.py delete mode 100644 tests/libs/InternalProtocol_pb2.py diff --git a/lib/protobuf-mission-module/.gitignore b/lib/protobuf-mission-module/.gitignore new file mode 100644 index 0000000..703e159 --- /dev/null +++ b/lib/protobuf-mission-module/.gitignore @@ -0,0 +1,2 @@ +build +*.egg-info \ No newline at end of file diff --git a/tests/libs/MissionModule_pb2.py b/lib/protobuf-mission-module/mission_module_protobuf_files/MissionModule_pb2.py similarity index 100% rename from tests/libs/MissionModule_pb2.py rename to lib/protobuf-mission-module/mission_module_protobuf_files/MissionModule_pb2.py diff --git a/lib/protobuf-mission-module/pyproject.toml b/lib/protobuf-mission-module/pyproject.toml new file mode 100644 index 0000000..665b384 --- /dev/null +++ b/lib/protobuf-mission-module/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "mission_module_protobuf_files" +version = "1.2.13" + + +[tool.setuptools.packages.find] +include = ["mission_module_protobuf_files"] \ No newline at end of file diff --git a/tests/libs/ExternalProtocol_pb2.py b/tests/libs/ExternalProtocol_pb2.py deleted file mode 100644 index d38c392..0000000 --- a/tests/libs/ExternalProtocol_pb2.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: ExternalProtocol.proto -"""Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -import libs.InternalProtocol_pb2 as InternalProtocol__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x45xternalProtocol.proto\x12\x10\x45xternalProtocol\x1a\x16InternalProtocol.proto\"\xc7\x01\n\x0e\x45xternalServer\x12<\n\x0f\x63onnectResponse\x18\x01 \x01(\x0b\x32!.ExternalProtocol.ConnectResponseH\x00\x12:\n\x0estatusResponse\x18\x02 \x01(\x0b\x32 .ExternalProtocol.StatusResponseH\x00\x12,\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x19.ExternalProtocol.CommandH\x00\x42\r\n\x0bMessageType\"\xb7\x01\n\x0e\x45xternalClient\x12,\n\x07\x63onnect\x18\x01 \x01(\x0b\x32\x19.ExternalProtocol.ConnectH\x00\x12*\n\x06status\x18\x02 \x01(\x0b\x32\x18.ExternalProtocol.StatusH\x00\x12<\n\x0f\x63ommandResponse\x18\x03 \x01(\x0b\x32!.ExternalProtocol.CommandResponseH\x00\x42\r\n\x0bMessageType\"m\n\x07\x43onnect\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x0f\n\x07\x63ompany\x18\x02 \x01(\t\x12\x13\n\x0bvehicleName\x18\x03 \x01(\t\x12)\n\x07\x64\x65vices\x18\x04 \x03(\x0b\x32\x18.InternalProtocol.Device\"~\n\x0f\x43onnectResponse\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x34\n\x04type\x18\x02 \x01(\x0e\x32&.ExternalProtocol.ConnectResponse.Type\"\"\n\x04Type\x12\x06\n\x02OK\x10\x00\x12\x12\n\x0e\x41LREADY_LOGGED\x10\x01\"\x97\x02\n\x06Status\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x39\n\x0b\x64\x65viceState\x18\x02 \x01(\x0e\x32$.ExternalProtocol.Status.DeviceState\x12\x16\n\x0emessageCounter\x18\x03 \x01(\r\x12\x34\n\x0c\x64\x65viceStatus\x18\x04 \x01(\x0b\x32\x1e.InternalProtocol.DeviceStatus\x12\x19\n\x0c\x65rrorMessage\x18\x05 \x01(\x0cH\x00\x88\x01\x01\"E\n\x0b\x44\x65viceState\x12\x0e\n\nCONNECTING\x10\x00\x12\x0b\n\x07RUNNING\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x12\x0e\n\nDISCONNECT\x10\x03\x42\x0f\n\r_errorMessage\"\x80\x01\n\x0eStatusResponse\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x33\n\x04type\x18\x02 \x01(\x0e\x32%.ExternalProtocol.StatusResponse.Type\x12\x16\n\x0emessageCounter\x18\x03 \x01(\r\"\x0e\n\x04Type\x12\x06\n\x02OK\x10\x00\"l\n\x07\x43ommand\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x16\n\x0emessageCounter\x18\x02 \x01(\r\x12\x36\n\rdeviceCommand\x18\x03 \x01(\x0b\x32\x1f.InternalProtocol.DeviceCommand\"\xcb\x01\n\x0f\x43ommandResponse\x12\x11\n\tsessionId\x18\x01 \x01(\t\x12\x34\n\x04type\x18\x02 \x01(\x0e\x32&.ExternalProtocol.CommandResponse.Type\x12\x16\n\x0emessageCounter\x18\x03 \x01(\r\"W\n\x04Type\x12\x06\n\x02OK\x10\x00\x12\x18\n\x14\x44\x45VICE_NOT_CONNECTED\x10\x01\x12\x18\n\x14\x44\x45VICE_NOT_SUPPORTED\x10\x02\x12\x13\n\x0fINVALID_COMMAND\x10\x03\x42>Z!../internal/pkg/ba_proto;ba_proto\xaa\x02\x18Google.Protobuf.ba_protob\x06proto3') - -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ExternalProtocol_pb2', globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'Z!../internal/pkg/ba_proto;ba_proto\252\002\030Google.Protobuf.ba_proto' - _EXTERNALSERVER._serialized_start=69 - _EXTERNALSERVER._serialized_end=268 - _EXTERNALCLIENT._serialized_start=271 - _EXTERNALCLIENT._serialized_end=454 - _CONNECT._serialized_start=456 - _CONNECT._serialized_end=565 - _CONNECTRESPONSE._serialized_start=567 - _CONNECTRESPONSE._serialized_end=693 - _CONNECTRESPONSE_TYPE._serialized_start=659 - _CONNECTRESPONSE_TYPE._serialized_end=693 - _STATUS._serialized_start=696 - _STATUS._serialized_end=975 - _STATUS_DEVICESTATE._serialized_start=889 - _STATUS_DEVICESTATE._serialized_end=958 - _STATUSRESPONSE._serialized_start=978 - _STATUSRESPONSE._serialized_end=1106 - _STATUSRESPONSE_TYPE._serialized_start=659 - _STATUSRESPONSE_TYPE._serialized_end=673 - _COMMAND._serialized_start=1108 - _COMMAND._serialized_end=1216 - _COMMANDRESPONSE._serialized_start=1219 - _COMMANDRESPONSE._serialized_end=1422 - _COMMANDRESPONSE_TYPE._serialized_start=1335 - _COMMANDRESPONSE_TYPE._serialized_end=1422 -# @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/tests/libs/InternalProtocol_pb2.py b/tests/libs/InternalProtocol_pb2.py deleted file mode 100644 index 3014c30..0000000 --- a/tests/libs/InternalProtocol_pb2.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: InternalProtocol.proto -"""Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16InternalProtocol.proto\x12\x10InternalProtocol\"\x91\x01\n\x0eInternalClient\x12\x38\n\rdeviceConnect\x18\x01 \x01(\x0b\x32\x1f.InternalProtocol.DeviceConnectH\x00\x12\x36\n\x0c\x64\x65viceStatus\x18\x02 \x01(\x0b\x32\x1e.InternalProtocol.DeviceStatusH\x00\x42\r\n\x0bMessageType\"\xa3\x01\n\x0eInternalServer\x12H\n\x15\x64\x65viceConnectResponse\x18\x01 \x01(\x0b\x32\'.InternalProtocol.DeviceConnectResponseH\x00\x12\x38\n\rdeviceCommand\x18\x02 \x01(\x0b\x32\x1f.InternalProtocol.DeviceCommandH\x00\x42\r\n\x0bMessageType\"9\n\rDeviceConnect\x12(\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x18.InternalProtocol.Device\"\x98\x02\n\x15\x44\x65viceConnectResponse\x12J\n\x0cresponseType\x18\x01 \x01(\x0e\x32\x34.InternalProtocol.DeviceConnectResponse.ResponseType\x12(\n\x06\x64\x65vice\x18\x02 \x01(\x0b\x32\x18.InternalProtocol.Device\"\x88\x01\n\x0cResponseType\x12\x06\n\x02OK\x10\x00\x12\x15\n\x11\x41LREADY_CONNECTED\x10\x01\x12\x18\n\x14MODULE_NOT_SUPPORTED\x10\x02\x12\x18\n\x14\x44\x45VICE_NOT_SUPPORTED\x10\x03\x12%\n!HIGHER_PRIORITY_ALREADY_CONNECTED\x10\x04\"L\n\x0c\x44\x65viceStatus\x12(\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x18.InternalProtocol.Device\x12\x12\n\nstatusData\x18\x02 \x01(\x0c\"N\n\rDeviceCommand\x12(\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x18.InternalProtocol.Device\x12\x13\n\x0b\x63ommandData\x18\x02 \x01(\x0c\"\xe5\x01\n\x06\x44\x65vice\x12/\n\x06module\x18\x01 \x01(\x0e\x32\x1f.InternalProtocol.Device.Module\x12\x12\n\ndeviceType\x18\x02 \x01(\r\x12\x12\n\ndeviceRole\x18\x03 \x01(\t\x12\x12\n\ndeviceName\x18\x04 \x01(\t\x12\x10\n\x08priority\x18\x05 \x01(\r\"\\\n\x06Module\x12\x13\n\x0fRESERVED_MODULE\x10\x00\x12\x12\n\x0eMISSION_MODULE\x10\x01\x12\r\n\tIO_MODULE\x10\x02\x12\x13\n\x0e\x45XAMPLE_MODULE\x10\xe8\x07\"\x05\x08\x03\x10\xe7\x07\x42>Z!../internal/pkg/ba_proto;ba_proto\xaa\x02\x18Google.Protobuf.ba_protob\x06proto3') - -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'InternalProtocol_pb2', globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'Z!../internal/pkg/ba_proto;ba_proto\252\002\030Google.Protobuf.ba_proto' - _INTERNALCLIENT._serialized_start=45 - _INTERNALCLIENT._serialized_end=190 - _INTERNALSERVER._serialized_start=193 - _INTERNALSERVER._serialized_end=356 - _DEVICECONNECT._serialized_start=358 - _DEVICECONNECT._serialized_end=415 - _DEVICECONNECTRESPONSE._serialized_start=418 - _DEVICECONNECTRESPONSE._serialized_end=698 - _DEVICECONNECTRESPONSE_RESPONSETYPE._serialized_start=562 - _DEVICECONNECTRESPONSE_RESPONSETYPE._serialized_end=698 - _DEVICESTATUS._serialized_start=700 - _DEVICESTATUS._serialized_end=776 - _DEVICECOMMAND._serialized_start=778 - _DEVICECOMMAND._serialized_end=856 - _DEVICE._serialized_start=859 - _DEVICE._serialized_end=1088 - _DEVICE_MODULE._serialized_start=996 - _DEVICE_MODULE._serialized_end=1088 -# @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/tests/libs/sniff_mqtt.py b/tests/libs/sniff_mqtt.py index ab246cc..6d79af6 100644 --- a/tests/libs/sniff_mqtt.py +++ b/tests/libs/sniff_mqtt.py @@ -1,8 +1,8 @@ import time, threading -import libs.ExternalProtocol_pb2 -import libs.InternalProtocol_pb2 -import libs.MissionModule_pb2 +import fleet_protocol_protobuf_files.ExternalProtocol_pb2 as ExternalProtocol_pb2 +import fleet_protocol_protobuf_files.InternalProtocol_pb2 as InternalProtocol_pb2 +import mission_module_protobuf_files.MissionModule_pb2 as MissionModule_pb2 from paho.mqtt.client import ( Client, MQTTMessage, MQTTv311 @@ -15,8 +15,8 @@ MAX_MESSAGES = 100 -def mission_module_device() -> libs.InternalProtocol_pb2.Device: - device = libs.InternalProtocol_pb2.Device() +def mission_module_device() -> InternalProtocol_pb2.Device: + device = InternalProtocol_pb2.Device() device.module = MISSION_MODULE_NUMBER device.deviceType = 1 device.deviceRole = 'autonomy' @@ -94,7 +94,7 @@ def _on_message(self, client, userdata, message: MQTTMessage) -> None: def _decode_module_gateway_message(self, payload: bytes): - message = libs.ExternalProtocol_pb2.ExternalClient() + message = ExternalProtocol_pb2.ExternalClient() message.ParseFromString(payload) if message.HasField('status'): self._handle_status(getattr(message, 'status').deviceStatus) @@ -108,7 +108,7 @@ def _decode_module_gateway_message(self, payload: bytes): def _decode_external_server_message(self, payload: bytes): - message = libs.ExternalProtocol_pb2.ExternalServer() + message = ExternalProtocol_pb2.ExternalServer() message.ParseFromString(payload) if message.HasField('command'): self._handle_command(getattr(message, 'command')) @@ -129,7 +129,7 @@ def _handle_status(self, status): def _handle_mission_module_status(self, status): - mission_status = libs.MissionModule_pb2.AutonomyStatus() + mission_status = MissionModule_pb2.AutonomyStatus() mission_status.ParseFromString(status) if self._store_statuses: with self._status_condition: @@ -160,7 +160,7 @@ def _handle_command(self, command): def _handle_mission_module_command(self, command): - mission_command = libs.MissionModule_pb2.AutonomyCommand() + mission_command = MissionModule_pb2.AutonomyCommand() mission_command.ParseFromString(command) if self._store_commands: with self._command_condition: @@ -187,21 +187,21 @@ def _print(self, message: str): print(message) - def send_mission_module_command(self, payload: libs.MissionModule_pb2.AutonomyCommand) -> None: + def send_mission_module_command(self, payload: MissionModule_pb2.AutonomyCommand) -> None: """Sends a mission module command to the MQTT broker.""" - if not isinstance(payload, libs.MissionModule_pb2.AutonomyCommand): + if not isinstance(payload, MissionModule_pb2.AutonomyCommand): raise TypeError('Command must be of type AutonomyCommand') - command = libs.ExternalProtocol_pb2.Command() + command = ExternalProtocol_pb2.Command() command.sessionId = self._mission_module_session_id command.messageCounter = self._mission_module_command_counter + 1 - device_command = libs.InternalProtocol_pb2.DeviceCommand() + device_command = InternalProtocol_pb2.DeviceCommand() device_command.device.CopyFrom(mission_module_device()) device_command.commandData = payload.SerializeToString() command.deviceCommand.CopyFrom(device_command) - msg = libs.ExternalProtocol_pb2.ExternalServer() + msg = ExternalProtocol_pb2.ExternalServer() msg.command.CopyFrom(command) self._client.publish( diff --git a/tests/requirements.txt b/tests/requirements.txt index 3757add..68d3fc3 100755 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -5,3 +5,5 @@ pydantic == 2.7.1 python-dateutil == 2.9.0.post0 fleet_management_http_client_python @ git+https://github.com/bringauto/fleet-management-http-client-python.git@v3.1.5 fleet_http_client_python @ git+https://github.com/bringauto/fleet-http-client-python.git@v2.8.5 +fleet_protocol_protobuf_files @ git+https://github.com/bringauto/fleet-protocol.git@main#subdirectory=protobuf/compiled/python +file:../lib/protobuf-mission-module \ No newline at end of file diff --git a/tests/test_disconnect_on_stop_arrival.py b/tests/test_disconnect_on_stop_arrival.py index 51341ab..40b5b7b 100644 --- a/tests/test_disconnect_on_stop_arrival.py +++ b/tests/test_disconnect_on_stop_arrival.py @@ -2,25 +2,26 @@ import threading import time -import libs.MissionModule_pb2 +import mission_module_protobuf_files.MissionModule_pb2 as MissionModule_pb2 + import libs.sniff_mqtt import libs.testing_utilities as _utils -def create_command() -> libs.MissionModule_pb2.AutonomyCommand: +def create_command() -> MissionModule_pb2.AutonomyCommand: """Creates a command that is expected in the default etna scenario since it reaches the first stop.""" - command = libs.MissionModule_pb2.AutonomyCommand() - command.action = libs.MissionModule_pb2.AutonomyCommand.Action.START + command = MissionModule_pb2.AutonomyCommand() + command.action = MissionModule_pb2.AutonomyCommand.Action.START command.route = 'Moravské náměstí 2' - stops: list[libs.MissionModule_pb2.Station] = [] - stops.append(libs.MissionModule_pb2.Station(name='Svatopluka Čecha A', - position=libs.MissionModule_pb2.Position(latitude=49.221645, - longitude=16.59081))) - stops.append(libs.MissionModule_pb2.Station(name='Těšínská', - position=libs.MissionModule_pb2.Position(latitude=49.22316, - longitude=16.58995))) + stops: list[MissionModule_pb2.Station] = [] + stops.append(MissionModule_pb2.Station(name='Svatopluka Čecha A', + position=MissionModule_pb2.Position(latitude=49.221645, + longitude=16.59081))) + stops.append(MissionModule_pb2.Station(name='Těšínská', + position=MissionModule_pb2.Position(latitude=49.22316, + longitude=16.58995))) command.stops.extend(stops) return command From 03e26e17fd83cd7fb835f5ce9f51eaee0535286e Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 14 Jul 2025 13:43:41 +0200 Subject: [PATCH 07/12] typo fix --- tests/libs/sniff_mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/libs/sniff_mqtt.py b/tests/libs/sniff_mqtt.py index 6d79af6..fee354e 100644 --- a/tests/libs/sniff_mqtt.py +++ b/tests/libs/sniff_mqtt.py @@ -42,7 +42,7 @@ def __init__(self, verbose: bool = False, store_statuses: bool = False, store_co def _create_client(self): - self._client =Client( + self._client = Client( client_id='Monitoring Test', protocol=MQTTv311, transport='tcp', From ff66b94e6feede1b14f7d9674ea17ac8f1690526 Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 21 Jul 2025 14:30:30 +0200 Subject: [PATCH 08/12] stop etna components on test start --- tests/README.md | 2 +- tests/test_disconnect_on_stop_arrival.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index a302ba6..ff3d9d5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,7 +6,7 @@ So far only manual tests are implemented. ### Setup -Set up an [etna](https://github.com/bringauto/etna) repository anywhere. +Set up an [etna](https://github.com/bringauto/etna) repository anywhere with the latest master branch. ```bash cd tests diff --git a/tests/test_disconnect_on_stop_arrival.py b/tests/test_disconnect_on_stop_arrival.py index 40b5b7b..1e5d335 100644 --- a/tests/test_disconnect_on_stop_arrival.py +++ b/tests/test_disconnect_on_stop_arrival.py @@ -43,6 +43,8 @@ def create_command() -> MissionModule_pb2.AutonomyCommand: mqtt_component_name = str(args.yaml).split('/')[-2] + '-vernemq-1' print('Starting test: Disconnect on stop arrival') + _utils.stop_docker_compose_all(args.yaml) + time.sleep(5) # Wait for the services to stop completely _utils.run_docker_compose_all(args.yaml) print('Waiting for MQTT broker to start...') time.sleep(5) From 5e44a34a483e9fe60a0352094aeaaaafbfceb2af Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 21 Jul 2025 14:48:05 +0200 Subject: [PATCH 09/12] clearer setup steps for testing --- tests/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index ff3d9d5..121b5d3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,7 +6,13 @@ So far only manual tests are implemented. ### Setup -Set up an [etna](https://github.com/bringauto/etna) repository anywhere with the latest master branch. +Set up an [etna](https://github.com/bringauto/etna) repository, ideally with the latest master branch. + +Steps to use local mission module changes in etna: +- Set up a [Module Gateway](https://github.com/bringauto/module-gateway) repository, ideally with the latest master branch. +- Push the local mission module changes to a new branch +- Change the mission module vertion in the module gateway Dockerfile to the new branch name +- Run the create_docker_compose_for_testing.py python script in etna to use a local build of module gateway ```bash cd tests From b3725981787b6ee5a8422e38829bc0a474916c89 Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 21 Jul 2025 14:51:55 +0200 Subject: [PATCH 10/12] fix typo --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 121b5d3..dcadcbb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,7 +11,7 @@ Set up an [etna](https://github.com/bringauto/etna) repository, ideally with the Steps to use local mission module changes in etna: - Set up a [Module Gateway](https://github.com/bringauto/module-gateway) repository, ideally with the latest master branch. - Push the local mission module changes to a new branch -- Change the mission module vertion in the module gateway Dockerfile to the new branch name +- Change the mission module version in the module gateway Dockerfile to the new branch name - Run the create_docker_compose_for_testing.py python script in etna to use a local build of module gateway ```bash From a49c827b8a03333f4f99ab8266f130f78b3af97a Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 21 Jul 2025 15:00:38 +0200 Subject: [PATCH 11/12] specify component versions in tests --- tests/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index dcadcbb..8c37e75 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,10 +6,10 @@ So far only manual tests are implemented. ### Setup -Set up an [etna](https://github.com/bringauto/etna) repository, ideally with the latest master branch. +Set up an [etna](https://github.com/bringauto/etna) repository, tests were created for etna tag v2.5.0. Steps to use local mission module changes in etna: -- Set up a [Module Gateway](https://github.com/bringauto/module-gateway) repository, ideally with the latest master branch. +- Set up a [Module Gateway](https://github.com/bringauto/module-gateway) repository, tests were created for Module Gateway tag v1.3.4. - Push the local mission module changes to a new branch - Change the mission module version in the module gateway Dockerfile to the new branch name - Run the create_docker_compose_for_testing.py python script in etna to use a local build of module gateway From 215c3a243a72e889f87a3b84fa526dcb5c10f6dc Mon Sep 17 00:00:00 2001 From: MarioIvancik Date: Mon, 21 Jul 2025 15:02:35 +0200 Subject: [PATCH 12/12] fix versions in readme --- tests/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index 8c37e75..f0a322e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,10 +6,10 @@ So far only manual tests are implemented. ### Setup -Set up an [etna](https://github.com/bringauto/etna) repository, tests were created for etna tag v2.5.0. +Set up an [etna](https://github.com/bringauto/etna) repository, tests were created for etna tag v2.5.1. Steps to use local mission module changes in etna: -- Set up a [Module Gateway](https://github.com/bringauto/module-gateway) repository, tests were created for Module Gateway tag v1.3.4. +- Set up a [Module Gateway](https://github.com/bringauto/module-gateway) repository, tests were created for Module Gateway tag v1.3.5. - Push the local mission module changes to a new branch - Change the mission module version in the module gateway Dockerfile to the new branch name - Run the create_docker_compose_for_testing.py python script in etna to use a local build of module gateway