Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac58e34
python/iot3/mobility: cleanly stop GNSS provider on termination
ymorin-orange Aug 20, 2026
8155ea0
python/iot3/mobility: don't use test.mosquitto.org unless necessary
ymorin-orange Aug 20, 2026
6ef2b50
python/iot3/mobility: remove duplicate CAM test
ymorin-orange Aug 20, 2026
ecd546d
python/iot3/mobility: altitude confidence can't be good without altitude
ymorin-orange Aug 20, 2026
5aba682
python/iot3/mobility: fix station-uuid in tests
ymorin-orange Aug 20, 2026
a9add97
python/iot3/mobility: fix station-ID vs. station-UUID
ymorin-orange Aug 19, 2026
a2c9d51
python/iot3/mobility: ETSI zero-timestamp is valid
ymorin-orange Aug 18, 2026
cb36409
python/iot3/mobility: add tests for UNIX <-> ETSI time conversion
ymorin-orange Aug 18, 2026
560a29d
python/iot3/mobility: ETSI-specific tests belong to the ETSI test script
ymorin-orange Aug 18, 2026
377462a
python/iot3/mobility: expose GNSS devices
ymorin-orange Aug 18, 2026
3a45649
python/iot3/mobility: expose GNSS fix type in GNSSReport()
ymorin-orange Aug 18, 2026
6e5c32a
python/iot3/mobility: don't use legacy type names
ymorin-orange Aug 14, 2026
083d060
python/iot3/mobility: add width/length to CAM
ymorin-orange Aug 17, 2026
2e0d3ca
python/its-vehicle: check instance-id before using it
ymorin-orange Aug 19, 2026
d79c451
python/its-vehicle: simplify parsing the configuration
ymorin-orange Aug 19, 2026
cd5ed76
python/its-vehicle: use the main broker client-ID for the mirror broker
ymorin-orange Aug 19, 2026
940fad2
python/its-vehicle: add configurable TrafficParticipantType
ymorin-orange Aug 14, 2026
9773069
python/its-vehicle: replace instance-id with station-uuid
ymorin-orange Aug 19, 2026
d1bb0dd
python/its-vehicle: add length/width
ymorin-orange Aug 17, 2026
47b4e17
python/its-iqm: be explicit we open the config file for reading
ymorin-orange Aug 19, 2026
3a56cb3
python/its-iqm: replace instance-id with station-uuid
ymorin-orange Aug 19, 2026
a99f892
python/its-info: replace instance-id with station-uuid
ymorin-orange Aug 19, 2026
d47dcca
python/its-status: replace general.id with general.station-uuid
ymorin-orange Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,12 @@ in the python venv._
(docker)馃悕 ./python/iot3/tests/test-iot3-core-mqtt
(docker)馃悕 ./python/iot3/tests/test-iot3-core-otel
(docker)馃悕 ./python/iot3/tests/test-iot3-mobility
(docker)馃悕 ./python/iot3/tests/test-iot3-mobility-etsi
(docker)馃悕 ./python/iot3/tests/test-iot3-mobility-gnss
(docker)馃悕 ./python/iot3/tests/test-iot3-mobility-message
```
_**Note:**_ if `test.mosquitto.org` is unreachable, skip the test
`./python/iot3/tests/test-iot3-core-mqtt`.
5. ...

Expected results
Expand Down
8 changes: 5 additions & 3 deletions python/iot3/src/iot3/mobility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def my_alert_callback(
sample_config = dict(core.sample_config)
sample_config.update(
{
"uuid": "1234",
"station_type": etsi.Message.StationType.unknown,
"uuid": "ora_car_1234",
"station_type": etsi.Message.TrafficParticipantType.unknown,
"namespace": "default",
"report_depth": 22,
"roi_depth": 15,
Expand All @@ -68,7 +68,9 @@ def bootstrap(
role: str,
service_name: str,
bootstrap_config: dict,
station_type: Optional[etsi.Message.StationType] = etsi.Message.StationType.unknown,
station_type: Optional[
etsi.Message.TrafficParticipantType
] = etsi.Message.TrafficParticipantType.unknown,
# FIXME: Shouldn't namespace come from the bootstrap, in fact?
namespace: Optional[str] = "default",
) -> dict:
Expand Down
68 changes: 64 additions & 4 deletions python/iot3/src/iot3/mobility/cam.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ def __init__(
*,
uuid: str,
station_type: Optional[
etsi.Message.StationType
] = etsi.Message.StationType.unknown,
etsi.Message.TrafficParticipantType
] = etsi.Message.TrafficParticipantType.unknown,
gnss_report: GNSSReport,
length: float | None = None,
width: float | None = None,
):
"""Create a basic Cooperative Awareness Message

Expand Down Expand Up @@ -77,10 +79,22 @@ def __init__(
},
"drive_direction": 2,
"vehicle_length": {
"value": 1023,
"value": etsi.ETSI.si2etsi(
length,
etsi.ETSI.DECI_METER,
1023,
{"min": 1, "max": 1021},
1022,
),
"confidence": 4,
},
"vehicle_width": 62,
"vehicle_width": etsi.ETSI.si2etsi(
width,
etsi.ETSI.DECI_METER,
62,
{"min": 1, "max": 60},
61,
),
"longitudinal_acceleration": {
"value": etsi.ETSI.si2etsi(
gnss_report.acceleration,
Expand Down Expand Up @@ -164,6 +178,52 @@ def altitude(self, altitude):
800001,
)

@property
def length(self):
return etsi.ETCI.etsi2si(
self._message["message"]["high_frequency_container"][
"basic_vehicle_container_high_frequency"
]["vehicle_length"]["value"],
etsi.ETSI.DECI_METER,
1023,
1022,
)

@length.setter
def length(self, length):
self._message["message"]["high_frequency_container"][
"basic_vehicle_container_high_frequency"
]["vehicle_length"]["value"] = etsi.ETSI.si2etsi(
length,
etsi.ETSI.DECI_METER,
1023,
{"min": 1, "max": 1021},
1022,
)

@property
def width(self):
return etsi.ETCI.etsi2si(
self._message["message"]["high_frequency_container"][
"basic_vehicle_container_high_frequency"
]["width"],
etsi.ETSI.DECI_METER,
62,
61,
)

@width.setter
def width(self, width):
self._message["message"]["high_frequency_container"][
"basic_vehicle_container_high_frequency"
]["width"] = etsi.ETSI.si2etsi(
width,
etsi.ETSI.DECI_METER,
62,
{"min": 1, "max": 60},
61,
)


# Shorthand
CAM = CooperativeAwarenessMessage
8 changes: 4 additions & 4 deletions python/iot3/src/iot3/mobility/cpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ def __init__(
*,
uuid: str,
station_type: Optional[
etsi.Message.StationType
] = etsi.Message.StationType.unknown,
etsi.Message.TrafficParticipantType
] = etsi.Message.TrafficParticipantType.unknown,
gnss_report: GNSSReport,
segmentation_info: Optional[SegmentationInfo] = None,
perceived_objects: Optional[Iterable[PerceivedObject]] = [],
Expand Down Expand Up @@ -162,10 +162,10 @@ def __init__(
},
)

if station_type == etsi.Message.StationType.unknown:
if station_type == etsi.Message.TrafficParticipantType.unknown:
# Unknown station type, no station data.
pass
elif station_type == etsi.Message.StationType.roadSideUnit:
elif station_type == etsi.Message.TrafficParticipantType.roadSideUnit:
# We for now don't have enough to handle the MAPEM to define an RSU;
# the RSU container is just a list of stuff, so just provide an empty
# list for now.
Expand Down
46 changes: 39 additions & 7 deletions python/iot3/src/iot3/mobility/etsi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import hashlib
import its_quadkeys
import json
import warnings
from typing import Optional
from . import leapseconds
from .gnss import GNSSReport
Expand Down Expand Up @@ -196,7 +197,7 @@ def unix2etsi_time(
tzinfo=None
)
).timestamp()
return ETSI.si2etsi(tai_time - ETSI.EPOCH, ETSI.MILLI_SECOND, 0)
return ETSI.si2etsi(tai_time - ETSI.EPOCH, ETSI.MILLI_SECOND)

@staticmethod
def etsi2unix_time(
Expand All @@ -219,7 +220,7 @@ def etsi2unix_time(
>>> datetime.datetime.utcfromtimestamp(etsi2unix_time(94694401000))
datetime.datetime(2007, 1, 1, 0, 0)
"""
tai_time = ETSI.etsi2si(etsi_time, ETSI.MILLI_SECOND, 0) + ETSI.EPOCH
tai_time = ETSI.etsi2si(etsi_time, ETSI.MILLI_SECOND) + ETSI.EPOCH
return leapseconds.tai_to_utc(
datetime.datetime.fromtimestamp(tai_time, datetime.timezone.utc).replace(
tzinfo=None
Expand Down Expand Up @@ -273,10 +274,41 @@ def __init__(self, *args, **kwargs):

@staticmethod
def station_id(uuid: str) -> int:
return int(
hashlib.sha256(uuid.encode()).hexdigest()[:6],
16,
)
"""Extract the station ID from a station UUID

Station UUID is a three-part, underscore-separated identifier:
* company name, usualy a trigram, e.g. 'ora' (for Orange)
* participant role, e.g. 'car', trailer'...
* station ID, e.g. '12345678' (an 32-bit unsigned integer in base 10)
which would give the station_UUID: 'ora_car_12345678'.

Note: for legacy backward compatibility, if the station ID part
is not reecognised as a base-10 number, then a base-16 conversion
is attempted; if that worked, a warning is emitted. Support for
legacy hexadecimal station-ID will be removed in the future.
"""
parts = uuid.split("_")
if len(parts) < 3:
raise ValueError(f"{uuid}: invalid UUID (expected company_role_id)")

try:
return int(parts[2])
except ValueError as v_error:
base10_error = v_error

try:
station_id = int(parts[2], base=16)
except ValueError:
raise base10_error from None
else:
# stacklevel 1 is here, 2 is our caller (usually in iot3 itself),
# 3 is the caller of our caller (usually the actual application).
warnings.warn(
f"use of hexadecimal ({parts[2]}) in station-UUID is deprecated, convert to the base-10 value ({station_id})",
stacklevel=3,
)

return station_id

@property
def timestamp(self):
Expand Down Expand Up @@ -407,7 +439,7 @@ def altitude_confidence(
gnss_report: GNSSReport,
) -> int:
"""Return the altitude confidence value."""
if gnss_report.altitude_error is None:
if gnss_report.altitude is None or gnss_report.altitude_error is None:
return 15
steps = [1, 2, 5]
for confidence in range(14):
Expand Down
Loading
Loading