Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1378dc4
github: fix pythopn MR template
ymorin-orange Aug 7, 2026
df2c65c
python: fix a few typoes
ymorin-orange Aug 7, 2026
15b157e
python/iot3/mobility: forget the sock_fd on disconnect()
ymorin-orange Aug 4, 2026
fcc1f8f
python/iot3/mobility: trigger disconnect when stopping gpsd client
ymorin-orange Aug 5, 2026
ad7be24
python/iot3/mobility: fix parsing gpsd's attitude messages
ymorin-orange Aug 4, 2026
ef20231
python/iot3/mobility: rename attribute with data for current epoch
ymorin-orange Aug 4, 2026
e688e7d
python/iot3/mobility: better heuristic to build a GNSS measurement
ymorin-orange Aug 4, 2026
8aadc77
python/iot3/mobility: simplify GNSSReport's post-init
ymorin-orange Aug 4, 2026
c3ac934
python/iot3/mobility: add error ellipse to GNSSReposrt object
ymorin-orange Aug 4, 2026
8d70a15
python/iot3/mobility: store the GST message from gpsd
ymorin-orange Aug 4, 2026
d71bb01
python/iot3/mobility: create GNSSReport with error ellipse from GST
ymorin-orange Aug 4, 2026
2f191a3
python/iot3/mobility: allow caller to specify age of valid GNSSReport()
ymorin-orange Aug 4, 2026
24a35ed
python/iot3/mobility: add method to get position confidence ellipse
ymorin-orange Aug 4, 2026
c757e3d
python/iot3/mobility: use new position confidence ellipse method
ymorin-orange Aug 4, 2026
a707109
python/iot3/mobility: compliance with python 3.12
tigroo Jul 31, 2026
1fd9134
python/iot3/mobility: add method to get altitude confidence
ymorin-orange Aug 4, 2026
ce28f01
python/iot3/mobility: use new altitude confidence method
ymorin-orange Aug 4, 2026
782b223
python/iot3/mobility: add method to get full reference position
ymorin-orange Aug 4, 2026
4a6b7bb
python/iot3/mobility: simplify CPM with new reference position method
ymorin-orange Aug 4, 2026
338bc7f
python/iot3/mobility: implement CAM 2.4.0
tigroo Aug 4, 2026
6e4c0e7
python: bump iot3 SDK requirement for apps
ymorin-orange Aug 7, 2026
db767c9
python/its-vehicle: drop never-used timestamp-based gpsd heuristic
ymorin-orange Aug 5, 2026
859c86d
python/its-vehicle: swith to iot3.mobility.gnss
ymorin-orange Aug 5, 2026
e73e9f1
python/its-vehicle: swith to iot3.mobility.cam
ymorin-orange Aug 5, 2026
ada832e
python/its-vehicle: accept lat/lon to get RoI
ymorin-orange Aug 5, 2026
ca128a2
python/its-vehicle: do not create a quadkey just to get the RoI
ymorin-orange Aug 5, 2026
327c8b0
python/iot3/tests: use non-standard port for gpsfake
ymorin-orange Aug 7, 2026
e171964
python/iot3/tests: tweaks and fixes in comments
ymorin-orange Aug 7, 2026
d0d7bd0
python/iot3/tests; check faster when connecting to gpsd
ymorin-orange Aug 7, 2026
3ae4a6a
python/iot3/tests; add tests for GST and error ellipse in GNSReport
ymorin-orange Aug 7, 2026
0cb1108
python/iot3/tests: extend CAM tests for CAM 2.4.0 and error ellipse
ymorin-orange Aug 7, 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
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ in the python venv._
4. in another terminal, start a fake _gpsd_ process
(no need for an actual GNSS device):
```sh
$ TMPDIR=/tmp gpsfake -q -P 2947 -u -n -c 0.1 tests/data/NMEA.log
$ TMPDIR=/tmp gpsfake -q -P 2948 -u -n -c 0.1 tests/data/NMEA.log
```
5. in another terminal, start a container with Python 3.11
and the necessary packages:
Expand All @@ -69,7 +69,7 @@ in the python venv._
--user $(id -u):$(id -u) \
--mount type=bind,source=$(pwd),destination=$(pwd) \
--workdir $(pwd) \
python:3.11.17-slim-trixie \
python:3.11.15-slim-trixie \

@tigroo tigroo Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious to use an older version...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.11.17 was never released. The last 3.11 was 3.11.15. But at the time I wrote the MR template, the latest was 3.11.14. And if you look at your numpad, the '4' and the '7' are next to each other, so my guess is that I fat-fingered the 7 for the 4... :-/

/bin/bash -il

$ docker container exec -u 0:0 iot3 apt update
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ they are using the following versions:
| Schema | Rust | Python | Java | Swift |
|:-----------------:|:-----------------------------------------------------------------------------------:|:---------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------:|
| **Bootstrap** | | | | |
| **CAM** | [2.4.0](schema/cam/cam_schema_2-4-0.json) [1.1.3](schema/cam/cam_schema_1-1-3.json) | [1.1.3](schema/cam/cam_schema_1-1-3.json) | [2.4.0](schema/cam/cam_schema_2-4-0.json) [1.1.3](schema/cam/cam_schema_1-1-3.json) | [1.1.3](schema/cam/cam_schema_1-1-3.json) |
| **CAM** | [2.4.0](schema/cam/cam_schema_2-4-0.json) [1.1.3](schema/cam/cam_schema_1-1-3.json) | [2.4.0](schema/cam/cam_schema_2-4-0.json) | [2.4.0](schema/cam/cam_schema_2-4-0.json) [1.1.3](schema/cam/cam_schema_1-1-3.json) | [1.1.3](schema/cam/cam_schema_1-1-3.json) |
| **CPM** | [2.1.1](schema/cpm/cpm_schema_2-1-1.json) | [2.1.1](schema/cpm/cpm_schema_2-1-1.json) | [2.1.1](schema/cpm/cpm_schema_2-1-1.json) [1.2.1](schema/cpm/cpm_schema_1-2-1.json) | |
| **DENM** | [2.2.0](schema/denm/denm_schema_2-2-0.json) | [1.1.3](schema/denm/denm_schema_1-1-3.json) | [2.3.0](schema/denm/denm_schema_2-3-0.json) [2.2.0](schema/denm/denm_schema_2-2-0.json) [1.1.3](schema/denm/denm_schema_1-1-3.json) | [1.1.3](schema/denm/denm_schema_1-1-3.json) |
| **Information** | [2.1.0](schema/information/information_schema_2-1-0.json) | [1.2.0](schema/information/information_schema_1-2-0.json) | | |
Expand Down
107 changes: 45 additions & 62 deletions python/iot3/src/iot3/mobility/cam.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ def __init__(

self._message = dict(
{
"type": "cam",
"origin": "self",
"version": "1.1.3",
"message_type": "cam",
"message_format": "json/raw",
"source_uuid": uuid,
"timestamp": (
etsi.ETSI.si2etsi(
Expand All @@ -47,6 +46,7 @@ def __init__(
0,
)
),
"version": "2.4.0",
"message": {
"protocol_version": 1,
"station_id": self.station_id(uuid),
Expand All @@ -55,68 +55,51 @@ def __init__(
),
"basic_container": {
"station_type": station_type,
"reference_position": {
"latitude": etsi.ETSI.si2etsi(
gnss_report.latitude,
etsi.ETSI.DECI_MICRO_DEGREE,
900000001,
),
"longitude": etsi.ETSI.si2etsi(
gnss_report.longitude,
etsi.ETSI.DECI_MICRO_DEGREE,
1800000001,
),
"altitude": etsi.ETSI.si2etsi(
gnss_report.altitude,
etsi.ETSI.CENTI_METER,
800001,
),
},
"confidence": {
"position_confidence_ellipse": {
# We treat the 2D error as a circle, so semi-major
# and semi-minor are eqal, and thus the orientation
# of the elipse does not matter.
"semi_major_confidence": etsi.ETSI.si2etsi(
gnss_report.horizontal_error,
etsi.ETSI.CENTI_METER,
4095,
{"min": 0, "max": 4093},
4094,
),
"semi_minor_confidence": etsi.ETSI.si2etsi(
gnss_report.horizontal_error,
etsi.ETSI.CENTI_METER,
4095,
{"min": 0, "max": 4093},
4094,
),
# Any orientation is valid for a circle, just use 0.
"semi_major_orientation": etsi.ETSI.si2etsi(
0,
"reference_position": self.reference_position(gnss_report),
},
"high_frequency_container": {
"basic_vehicle_container_high_frequency": {
"heading": {
"value": etsi.ETSI.si2etsi(
gnss_report.track,
etsi.ETSI.DECI_DEGREE,
3601,
),
"confidence": 127,
},
"speed": {
"value": etsi.ETSI.si2etsi(
gnss_report.speed,
etsi.ETSI.CENTI_METER_PER_SECOND,
16383,
),
"confidence": 127,
},
"drive_direction": 2,
"vehicle_length": {
"value": 1023,
"confidence": 4,
},
"vehicle_width": 62,
"longitudinal_acceleration": {
"value": etsi.ETSI.si2etsi(
gnss_report.acceleration,
etsi.ETSI.DECI_METER_PER_SECOND_SECOND,
161,
),
"confidence": 102,
},
"curvature": {
"value": 1023,
"confidence": 7,
},
"curvature_calculation_mode": 2,
"yaw_rate": {
"value": 32767,
"confidence": 8,
},
},
},
"high_frequency_container": {
"heading": etsi.ETSI.si2etsi(
gnss_report.track,
etsi.ETSI.DECI_DEGREE,
3601,
),
"speed": etsi.ETSI.si2etsi(
gnss_report.speed,
etsi.ETSI.CENTI_METER_PER_SECOND,
16383,
),
"longitudinal_acceleration": etsi.ETSI.si2etsi(
gnss_report.acceleration,
etsi.ETSI.DECI_METER_PER_SECOND_SECOND,
161,
),
},
},
},
)
Expand Down Expand Up @@ -166,15 +149,15 @@ def altitude(self):
return etsi.ETSI.etsi2si(
self._message["message"]["basic_container"]["reference_position"][
"altitude"
],
]["value"],
etsi.ETSI.CENTI_METER,
800001,
)

@altitude.setter
def altitude(self, altitude):
self._message["message"]["basic_container"]["reference_position"][
"altitude"
self._message["message"]["basic_container"]["reference_position"]["altitude"][
"value"
] = etsi.ETSI.si2etsi(
altitude,
etsi.ETSI.CENTI_METER,
Expand Down
52 changes: 1 addition & 51 deletions python/iot3/src/iot3/mobility/cpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,57 +155,7 @@ def __init__(
"management_container": {
"station_type": station_type,
"reference_time": etsi.ETSI.unix2etsi_time(self._timestamp),
"reference_position": {
"latitude": etsi.ETSI.si2etsi(
gnss_report.latitude,
etsi.ETSI.DECI_MICRO_DEGREE,
900000001,
),
"longitude": etsi.ETSI.si2etsi(
gnss_report.longitude,
etsi.ETSI.DECI_MICRO_DEGREE,
1800000001,
),
"altitude": {
"value": etsi.ETSI.si2etsi(
gnss_report.altitude,
etsi.ETSI.CENTI_METER,
800001,
),
# Encoding the altitude error is a non-linear search in
# an array... Let's consider it unavailable for now.
"confidence": etsi.ETSI.si2etsi(
None,
etsi.ETSI.CENTI_METER,
15,
),
},
"position_confidence_ellipse": {
# We treat the 2D error as a circle, so semi-major
# and semi-minor are eqal, and thus the orientation
# of the elipse does not matter.
"semi_major": etsi.ETSI.si2etsi(
gnss_report.horizontal_error,
etsi.ETSI.CENTI_METER,
4095,
{"min": 0, "max": 4093},
4094,
),
"semi_minor": etsi.ETSI.si2etsi(
gnss_report.horizontal_error,
etsi.ETSI.CENTI_METER,
4095,
{"min": 0, "max": 4093},
4094,
),
# Any orientation is valid for a circle, just use 0.
"semi_major_orientation": etsi.ETSI.si2etsi(
0,
etsi.ETSI.DECI_DEGREE,
3601,
),
},
},
"reference_position": self.reference_position(gnss_report),
},
"perceived_object_container": [],
},
Expand Down
82 changes: 80 additions & 2 deletions python/iot3/src/iot3/mobility/etsi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import json
from typing import Optional
from . import leapseconds
from .gnss import GNSSReport


class ETSI(abc.ABC):
Expand Down Expand Up @@ -191,7 +192,9 @@ def unix2etsi_time(
94694401000
"""
tai_time = leapseconds.utc_to_tai(
datetime.datetime.utcfromtimestamp(unix_time)
datetime.datetime.fromtimestamp(unix_time, datetime.timezone.utc).replace(
tzinfo=None
)
).timestamp()
return ETSI.si2etsi(tai_time - ETSI.EPOCH, ETSI.MILLI_SECOND, 0)

Expand All @@ -218,7 +221,9 @@ def etsi2unix_time(
"""
tai_time = ETSI.etsi2si(etsi_time, ETSI.MILLI_SECOND, 0) + ETSI.EPOCH
return leapseconds.tai_to_utc(
datetime.datetime.utcfromtimestamp(tai_time)
datetime.datetime.fromtimestamp(tai_time, datetime.timezone.utc).replace(
tzinfo=None
)
).timestamp()

@staticmethod
Expand Down Expand Up @@ -337,3 +342,76 @@ def topic(
def to_json(self) -> str:
# Return the densest-possible JSON sentence
return json.dumps(self._message, separators=(",", ":"))

@staticmethod
def reference_position(
gnss_report: GNSSReport,
) -> dict:
return {
"latitude": ETSI.si2etsi(
gnss_report.latitude,
ETSI.DECI_MICRO_DEGREE,
900000001,
),
"longitude": ETSI.si2etsi(
gnss_report.longitude,
ETSI.DECI_MICRO_DEGREE,
1800000001,
),
"altitude": {
"value": ETSI.si2etsi(
gnss_report.altitude,
ETSI.CENTI_METER,
800001,
),
"confidence": Message.altitude_confidence(gnss_report),
},
"position_confidence_ellipse": (
Message.position_confidence_ellipse(gnss_report)
),
}

@staticmethod
def position_confidence_ellipse(
gnss_report: GNSSReport,
) -> dict:
return {
"semi_major": ETSI.si2etsi(
gnss_report.ellipse_semi_major,
ETSI.CENTI_METER,
4095,
{"min": 1, "max": 4093},
4094,
),
"semi_minor": ETSI.si2etsi(
gnss_report.ellipse_semi_minor,
ETSI.CENTI_METER,
4095,
{"min": 1, "max": 4093},
4094,
),
"semi_major_orientation": ETSI.si2etsi(
(
None
if gnss_report.ellipse_orient is None
else gnss_report.ellipse_orient % 360.0
),
ETSI.DECI_DEGREE,
3601,
{"min": 0, "max": 3599},
),
}

@staticmethod
def altitude_confidence(
gnss_report: GNSSReport,
) -> int:
"""Return the altitude confidence value."""
if gnss_report.altitude_error is None:
return 15
steps = [1, 2, 5]
for confidence in range(14):
threshold = steps[confidence % 3] * (10 ** (int(confidence / 3) - 2))
if gnss_report.altitude_error <= threshold:
return confidence
return 14
Loading
Loading