Keep the colleague's microbu-esp32c5 tree in this repository
obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but that tree was gitignored, so a clone of this repository could not build the firmware it ships. It is now committed here as ordinary files in its own folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP bridge's signature verification (--trust) used on the bench. Nothing is fetched from or pushed to the colleague's repository; this repository and its remotes carry everything. The folder's own .gitignore keeps build output, downloaded components and private key material out, as it did there; the committed file set is identical to that repository's tracked files. The ESP32-C5 is still flashed from obu-firmware/, which only takes vanetza-idf from microbu-esp32c5/, so the two stay separate folders. FLASHING.md says how to take a newer version of the colleague's tree (copy it over the folder, rebuild, test, commit).
This commit is contained in:
@@ -0,0 +1,743 @@
|
||||
{
|
||||
"arguments": {
|
||||
"port": "COM5",
|
||||
"ble": null,
|
||||
"connect_timeout": 20.0,
|
||||
"bundle": "chain.vcr",
|
||||
"erase_credentials": false,
|
||||
"unsecured": false,
|
||||
"radio": "txrx",
|
||||
"channel": 180,
|
||||
"power": 10,
|
||||
"no_beacons": false,
|
||||
"lat": 53.565247,
|
||||
"lon": 10.008239,
|
||||
"speed": 2.0,
|
||||
"radius": 20.0,
|
||||
"poti_rate": 5.0,
|
||||
"period": 5,
|
||||
"station_id": null,
|
||||
"ssp": "01",
|
||||
"no_vam": false,
|
||||
"id_change_every": null,
|
||||
"duration": 15.0,
|
||||
"mirror": false,
|
||||
"divert": false,
|
||||
"pcap": null,
|
||||
"report": "dcc_gate_check.json",
|
||||
"asn1": null,
|
||||
"timeout": 6.0,
|
||||
"verbose": true,
|
||||
"quiet_log": false
|
||||
},
|
||||
"station": {
|
||||
"gn_address": "0800be0de448e3bc",
|
||||
"identifier": "c06fbc0de448e3bc",
|
||||
"tickets": 1
|
||||
},
|
||||
"vams_generated": 242,
|
||||
"results_accepted": 0,
|
||||
"results_refused": 0,
|
||||
"results_pending": 0,
|
||||
"timing": {
|
||||
"stream_seconds": 15.052010699990205,
|
||||
"generation_rate_hz": 16.077586232393354,
|
||||
"generation_interval_ms": {
|
||||
"samples": 241,
|
||||
"mean": 62.20173734458573,
|
||||
"p95": 63.81820002570748,
|
||||
"max": 73.63060000352561
|
||||
},
|
||||
"vam_assembly_ms": {
|
||||
"samples": 242,
|
||||
"mean": 0.1734210780055901,
|
||||
"p95": 0.33850001636892557,
|
||||
"max": 0.5439000087790191
|
||||
},
|
||||
"request_to_result_ms": null
|
||||
},
|
||||
"frames_captured": 0,
|
||||
"final_status": {
|
||||
"uptime_ms": 121265,
|
||||
"configured": 1,
|
||||
"gn_address": "0800be0de448e3bc",
|
||||
"identifier": "c06fbc0de448e3bc",
|
||||
"change_pending": 0,
|
||||
"tickets": 1,
|
||||
"signed_messages": 242,
|
||||
"refused_no_ticket": 0,
|
||||
"refused_change_pending": 0,
|
||||
"refused_permission": 0,
|
||||
"sign_failed": 0,
|
||||
"verified": 0,
|
||||
"rejected": 0,
|
||||
"requests_accepted": 242,
|
||||
"requests_refused": 0,
|
||||
"indications": 0,
|
||||
"radio_submitted": 242,
|
||||
"radio_failed": 0,
|
||||
"radio_received": 0,
|
||||
"radio_dropped": 0,
|
||||
"link_rx_frames": 313,
|
||||
"link_crc_errors": 0,
|
||||
"link_malformed": 0,
|
||||
"poti_updates": 62,
|
||||
"its_time_ms": 717107692039
|
||||
},
|
||||
"events": [
|
||||
"22:34:30 serial connected: COM5",
|
||||
"22:34:30 PoTi trajectory: centre=53.5652470,10.0082390 radius=20.0 m speed=2.0 m/s",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:30 STATUS its_time=0 signed=0 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=0/0 ind=0 radio tx/fail/rx/drop=0/0/0/0 id=0000000000000000 pending=0 tickets=0",
|
||||
"22:34:31 station configured: gn_address=080062d0e89fe341 identifier=ba1862d0e89fe341 credentials=True tickets=2",
|
||||
"22:34:31 credentials provisioned: 1 roots, 1 authorities, 1 tickets",
|
||||
"22:34:31 station rebuilt: gn_address=0800be0de448e3bc identifier=c06fbc0de448e3bc",
|
||||
"22:34:31 SF-IDCHANGE-SUBSCRIBE: subscription 1",
|
||||
"22:34:31 VAM 1 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:31 STATUS its_time=717107676782 signed=2 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=2/0 ind=0 radio tx/fail/rx/drop=2/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:31 VAM 2 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:31 VAM 3 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:31 VAM 4 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:31 VAM 5 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 6 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 7 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 8 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 9 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 10 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 11 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 12 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 13 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 14 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 15 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 16 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 17 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 STATUS its_time=717107677782 signed=18 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=18/0 ind=0 radio tx/fail/rx/drop=18/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:32 VAM 18 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 19 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 20 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:32 VAM 21 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 22 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 23 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 24 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 25 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 26 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 27 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 28 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 29 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 30 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 31 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 32 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 33 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 STATUS its_time=717107678782 signed=34 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=34/0 ind=0 radio tx/fail/rx/drop=34/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:33 VAM 34 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 35 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 36 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:33 VAM 37 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 38 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 39 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 40 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 41 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 42 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 43 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 44 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 45 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 46 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 47 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 48 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 49 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 STATUS its_time=717107679782 signed=50 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=50/0 ind=0 radio tx/fail/rx/drop=50/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:34 VAM 50 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 51 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 52 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:34 VAM 53 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 54 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 55 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 56 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 57 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 58 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 59 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 60 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 61 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 62 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 63 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 64 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 65 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 STATUS its_time=717107680782 signed=66 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=66/0 ind=0 radio tx/fail/rx/drop=66/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:35 VAM 66 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 67 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 68 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:35 VAM 69 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 70 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 71 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 72 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 73 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 74 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 75 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 76 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 77 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 78 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 79 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 80 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 81 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 STATUS its_time=717107681782 signed=82 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=82/0 ind=0 radio tx/fail/rx/drop=82/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:36 VAM 82 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 83 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 84 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:36 VAM 85 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 86 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 87 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 88 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 89 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 90 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 91 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 92 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 93 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 94 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 95 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 96 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 97 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 STATUS its_time=717107682782 signed=98 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=98/0 ind=0 radio tx/fail/rx/drop=98/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:37 VAM 98 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 99 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 100 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:37 VAM 101 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 102 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 103 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 104 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 105 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 106 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 107 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 108 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 109 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 110 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 111 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 112 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 113 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 STATUS its_time=717107683782 signed=114 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=114/0 ind=0 radio tx/fail/rx/drop=114/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:38 VAM 114 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 115 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 116 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:38 VAM 117 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 118 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 119 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 120 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 121 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 122 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 123 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 124 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 125 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 126 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 127 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 128 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 129 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 130 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 STATUS its_time=717107684816 signed=131 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=131/0 ind=0 radio tx/fail/rx/drop=131/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:39 VAM 131 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 132 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:39 VAM 133 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 134 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 135 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 136 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 137 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 138 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 139 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 140 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 141 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 142 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 143 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 144 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 145 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 146 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 STATUS its_time=717107685816 signed=147 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=147/0 ind=0 radio tx/fail/rx/drop=147/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:40 VAM 147 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 148 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:40 VAM 149 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 150 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 151 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 152 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 153 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 154 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 155 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 156 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 157 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 158 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 159 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 160 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 161 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 162 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 STATUS its_time=717107686816 signed=163 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=163/0 ind=0 radio tx/fail/rx/drop=163/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:41 VAM 163 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 164 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:41 VAM 165 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 166 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 167 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 168 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 169 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 170 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 171 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 172 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 173 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 174 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 175 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 176 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 177 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 178 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 STATUS its_time=717107687816 signed=179 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=179/0 ind=0 radio tx/fail/rx/drop=179/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:42 VAM 179 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 180 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:42 VAM 181 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 182 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 183 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 184 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 185 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 186 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 187 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 188 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 189 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 190 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 191 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 192 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 193 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 194 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 STATUS its_time=717107688816 signed=195 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=195/0 ind=0 radio tx/fail/rx/drop=195/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:43 VAM 195 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 196 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:43 VAM 197 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 198 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 199 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 200 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 201 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 202 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 203 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 204 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 205 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 206 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 207 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 208 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 209 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 210 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 211 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 STATUS its_time=717107689846 signed=212 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=212/0 ind=0 radio tx/fail/rx/drop=212/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:44 VAM 212 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:44 VAM 213 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 214 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 215 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 216 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 217 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 218 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 219 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 220 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 221 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 222 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 223 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 224 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 225 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 226 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 227 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 STATUS its_time=717107690846 signed=228 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=228/0 ind=0 radio tx/fail/rx/drop=228/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:45 VAM 228 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 229 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:45 VAM 230 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 231 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 232 (36 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 233 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 234 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 235 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 236 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 237 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 238 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 239 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 240 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 241 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 VAM 242 (34 B, StationId 3391780636) -> BTP-DATA.request accepted",
|
||||
"22:34:46 STATUS its_time=717107691846 signed=242 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=242/0 ind=0 radio tx/fail/rx/drop=242/0/0/0 id=c06fbc0de448e3bc pending=0 tickets=1",
|
||||
"22:34:47 final STATUS: signed=242 refused(no ticket 0, pending 0, permission 0) failed=0 req ok/refused=242/0 ind=0 radio tx/fail/rx/drop=242/0/0/0"
|
||||
],
|
||||
"micrOBU_log": [
|
||||
"I (964) phy: libbtbb version: 92325d6, Mar 10 2026, 10:22:25",
|
||||
"I (974) cits_ble: NimBLE host task running",
|
||||
"I (974) NimBLE: GAP procedure initiated: stop advertising.",
|
||||
"I (974) NimBLE: GAP procedure initiated: stop advertising.",
|
||||
"I (974) NimBLE: GAP procedure initiated: advertise; ",
|
||||
"I (974) NimBLE: disc_mode=2",
|
||||
"I (974) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0",
|
||||
"I (974) NimBLE: ",
|
||||
"I (974) cits_ble: BLE advertising started as 'micrOBU-0196'",
|
||||
"I (974) cits_ble: simple_ble initialized (fixed passkey 123456)",
|
||||
"I (974) microbu: micrOBU firmware on vanetza-idf, link protocol version 1, test channel enabled",
|
||||
"I (974) board: LED GPIO 27 (mode: normal_tx (normally OFF))",
|
||||
"I (974) microbu: station task ready; the phone/PC configures the station over BLE or USB Serial-JTAG",
|
||||
"I (105454) pp: pp rom version: 78a72e9d5",
|
||||
"I (105454) net80211: net80211 rom version: 78a72e9d5",
|
||||
"I (105464) wifi:",
|
||||
"wifi driver task: 4082f18c, prio:23, stack:6656, core=0",
|
||||
"I (105464) wifi:",
|
||||
"wifi firmware version: 00ad238",
|
||||
"I (105464) wifi:",
|
||||
"wifi certification version: v7.0",
|
||||
"I (105464) wifi:",
|
||||
"config NVS flash: disabled",
|
||||
"I (105464) wifi:",
|
||||
"config nano formatting: disabled",
|
||||
"I (105464) wifi:",
|
||||
"mac_version:HAL_MAC_ESP32AX_752MP_ECO2,ut_version:N, band mode:0x3",
|
||||
"I (105464) wifi:",
|
||||
"Init data frame dynamic rx buffer num: 16",
|
||||
"I (105464) wifi:",
|
||||
"Init static rx mgmt buffer num: 5",
|
||||
"I (105464) wifi:",
|
||||
"Init management short buffer num: 16",
|
||||
"I (105464) wifi:",
|
||||
"Init dynamic tx buffer num: 16",
|
||||
"I (105464) wifi:",
|
||||
"Init static tx FG buffer num: 2",
|
||||
"I (105464) wifi:",
|
||||
"Init static rx buffer size: 1700 (rxctrl:64, csi:512)",
|
||||
"I (105464) wifi:",
|
||||
"Init static rx buffer num: 6",
|
||||
"I (105464) wifi:",
|
||||
"Init dynamic rx buffer num: 16",
|
||||
"I (105464) wifi_init: rx ba win: 6",
|
||||
"I (105464) wifi_init: accept mbox: 6",
|
||||
"I (105464) wifi_init: tcpip mbox: 32",
|
||||
"I (105464) wifi_init: udp mbox: 6",
|
||||
"I (105464) wifi_init: tcp mbox: 6",
|
||||
"I (105464) wifi_init: tcp tx win: 5760",
|
||||
"I (105464) wifi_init: tcp rx win: 5760",
|
||||
"I (105464) wifi_init: tcp mss: 1440",
|
||||
"I (105464) wifi_init: WiFi IRAM OP enabled",
|
||||
"I (105464) wifi_init: WiFi RX IRAM OP enabled",
|
||||
"I (105464) wifi_init: WiFi SLP IRAM OP enabled",
|
||||
"I (105464) wifi:",
|
||||
"11ax coex: WDEVAX_PTI0(0x55777555), WDEVAX_PTI1(0x00003377).",
|
||||
"I (105464) wifi:",
|
||||
"mode : sta (10:bd:a3:cc:01:94)",
|
||||
"I (105464) wifi:",
|
||||
"enable tsf",
|
||||
"I (105464) wifi:",
|
||||
"wifi set band mode, new band mode 2, old band mode 3",
|
||||
"I (105474) wifi:",
|
||||
"enable tsf",
|
||||
"I (105474) wifi:",
|
||||
"Set ps type: 0, coexist: 1",
|
||||
"I (105474) wifi:",
|
||||
"Coexist!!! Wi-Fi station would only keep waked when available",
|
||||
"I (105474) wifi:",
|
||||
"ic_enable_sniffer",
|
||||
"I (105474) c5_radio: ITS-G5 802.11p radio started on channel 180 (5900 MHz), TX/RX",
|
||||
"I (105474) station: radio on channel 180, transmit and receive",
|
||||
"I (105594) station: credentials from NVS: 1 roots, 2 authorities, 2 tickets (result 0)",
|
||||
"I (105604) link: station configured: security 1, radio 2, result 0",
|
||||
"I (105964) station: credentials from NVS: 1 roots, 1 authorities, 1 tickets (result 0)",
|
||||
"I (105964) station: credentials provisioned: 1 roots, 1 authorities, 1 tickets",
|
||||
"I (106084) station: credentials from NVS: 1 roots, 1 authorities, 1 tickets (result 0)",
|
||||
"I (106084) link: station configured: security 1, radio 2, result 0",
|
||||
"I (106204) station: credentials from NVS: 1 roots, 1 authorities, 1 tickets (result 0)",
|
||||
"I (106204) link: SF_IDCHANGE_SUBSCRIBE handle=1 result=0",
|
||||
"I (106264) station: heap before sign: free=37632 min=33820 dma=21688 stack_hwm=6864",
|
||||
"I (106324) station: heap before sign: free=38124 min=27472 dma=22180 stack_hwm=6816",
|
||||
"I (106384) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106444) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106504) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106564) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106624) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106694) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106754) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106814) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106874) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (106944) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107004) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107064) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107124) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107194) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107254) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107314) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107374) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107434) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107504) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107564) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107634) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107694) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107754) station: heap before sign: free=38132 min=27472 dma=22188 stack_hwm=6816",
|
||||
"I (107814) station: heap before sign: free=37628 min=27472 dma=21684 stack_hwm=6816",
|
||||
"I (107874) station: heap before sign: free=38132 min=27460 dma=22188 stack_hwm=6816",
|
||||
"I (107944) station: heap before sign: free=38132 min=27460 dma=22188 stack_hwm=6816",
|
||||
"I (108014) station: heap before sign: free=38136 min=27460 dma=22192 stack_hwm=6816",
|
||||
"I (108064) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108124) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108184) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108254) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108314) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108374) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108444) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108504) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108564) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108624) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108684) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108744) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108814) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108874) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108934) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (108994) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (109064) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (109124) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (109184) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (109244) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (109304) station: heap before sign: free=38124 min=27460 dma=22180 stack_hwm=6816",
|
||||
"I (109374) station: heap before sign: free=37616 min=27460 dma=21672 stack_hwm=6816",
|
||||
"I (109434) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109494) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109554) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109614) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109684) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109744) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109804) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109864) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109934) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (109994) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110054) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110114) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110174) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110244) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110304) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110364) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110424) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110494) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110554) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110614) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110674) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110744) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110804) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110864) station: heap before sign: free=38120 min=27444 dma=22176 stack_hwm=6816",
|
||||
"I (110924) station: heap before sign: free=37616 min=27444 dma=21672 stack_hwm=6816",
|
||||
"I (110984) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111054) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111114) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111174) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111234) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111304) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111364) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111424) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111484) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111554) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111614) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111674) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111734) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111804) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111864) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111924) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6816",
|
||||
"I (111984) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112044) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112104) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112174) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112234) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112294) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112354) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112424) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (112484) station: heap before sign: free=37608 min=27444 dma=21664 stack_hwm=6720",
|
||||
"I (112544) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112604) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112674) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112734) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112794) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112864) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112924) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (112984) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113044) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113104) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113174) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113234) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113294) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113354) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113414) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113484) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113544) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113604) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113664) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113724) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (113794) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113854) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113914) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (113984) station: heap before sign: free=38112 min=27444 dma=22168 stack_hwm=6720",
|
||||
"I (114044) station: heap before sign: free=37608 min=27444 dma=21664 stack_hwm=6720",
|
||||
"I (114104) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114164) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114224) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114294) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114354) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114414) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114474) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114544) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114604) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114664) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114724) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114784) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114844) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114904) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (114974) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115034) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115094) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115154) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115214) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115274) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115344) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115404) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115464) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115524) station: heap before sign: free=38124 min=27444 dma=22180 stack_hwm=6720",
|
||||
"I (115584) station: heap before sign: free=37612 min=27444 dma=21668 stack_hwm=6720",
|
||||
"I (115654) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (115714) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (115774) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (115834) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (115904) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (115964) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116024) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116094) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116154) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116214) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116274) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116344) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116404) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116464) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116524) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116594) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116654) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116714) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116774) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116834) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116904) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (116964) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (117024) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (117084) station: heap before sign: free=38116 min=27432 dma=22172 stack_hwm=6720",
|
||||
"I (117144) station: heap before sign: free=37612 min=27432 dma=21668 stack_hwm=6720",
|
||||
"I (117214) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117274) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117334) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117394) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117464) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117524) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117584) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117644) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117704) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117774) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117834) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117894) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (117954) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118024) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118084) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118144) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118204) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118264) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118334) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118394) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118454) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118514) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118574) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118644) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118704) station: heap before sign: free=37620 min=27432 dma=21676 stack_hwm=6720",
|
||||
"I (118764) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118824) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118894) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (118954) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119014) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119074) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119144) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119204) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119264) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119324) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119384) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119444) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119514) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119574) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119634) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119694) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119764) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119824) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119884) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (119944) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120004) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120064) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120134) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120194) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120254) station: heap before sign: free=37620 min=27432 dma=21676 stack_hwm=6720",
|
||||
"I (120314) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120374) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120434) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120504) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120564) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120624) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120694) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120754) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120814) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120874) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120934) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (120994) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (121064) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (121124) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (121184) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720",
|
||||
"I (121254) station: heap before sign: free=38124 min=27432 dma=22180 stack_hwm=6720"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
"""Phone side of the micrOBU station-internal link (implementation/station-link/README.md).
|
||||
|
||||
``messages`` is the transport-independent message layer, ``serial_transport`` the framing
|
||||
over native USB Serial/JTAG, ``ble_transport`` the laptop/phone-equivalent BLE GATT client,
|
||||
and ``client`` the shared request/reply API with unsolicited messages delivered to callbacks.
|
||||
"""
|
||||
from .messages import * # noqa: F401,F403
|
||||
from .transport import TransportAdapter # noqa: F401
|
||||
from .serial_transport import FrameType, SerialTransport, encode_frame, FrameDecoder # noqa: F401
|
||||
from .ble_transport import BleTransport # noqa: F401
|
||||
from .client import LinkClient, LinkError, TestChannel # noqa: F401
|
||||
@@ -0,0 +1,199 @@
|
||||
"""BLE GATT transport for the station-link message layer.
|
||||
|
||||
The ESP32 exposes the Nordic-UART-shaped service (NUS) documented in the station-link
|
||||
README. Values contain binary station-link messages (up to 512 bytes). Bleak
|
||||
runs on a private asyncio thread while the LinkClient remains a synchronous request/reply API.
|
||||
Fixed passkey bonding is used (passkey: 123456).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import queue
|
||||
import threading
|
||||
from typing import Callable, Optional
|
||||
|
||||
from . import messages as m
|
||||
from .serial_transport import FrameType
|
||||
|
||||
# Structured ETSI C-ITS Station Service UUIDs
|
||||
SERVICE_UUID = '0000c175-ba5e-4c17-8000-00805f9b34fb'
|
||||
BTP_REQUEST_UUID = '0000c176-ba5e-4c17-8000-00805f9b34fb'
|
||||
BTP_INDICATION_UUID = '0000c177-ba5e-4c17-8000-00805f9b34fb'
|
||||
POTI_UUID = '0000c178-ba5e-4c17-8000-00805f9b34fb'
|
||||
STATUS_UUID = '0000c179-ba5e-4c17-8000-00805f9b34fb'
|
||||
ID_EVENT_UUID = '0000c17a-ba5e-4c17-8000-00805f9b34fb'
|
||||
CONFIG_UUID = '0000c17b-ba5e-4c17-8000-00805f9b34fb'
|
||||
RESULT_UUID = '0000c17c-ba5e-4c17-8000-00805f9b34fb'
|
||||
FIXED_PASSKEY = 123456
|
||||
|
||||
|
||||
class BleTransport:
|
||||
"""Synchronous LinkClient adapter backed by Bleak on a private event loop.
|
||||
|
||||
Transmits complete station-link messages directly without ATT fragmentation chunking
|
||||
or queue delays.
|
||||
"""
|
||||
|
||||
def __init__(self, target: Optional[str], on_frame: Callable[[int, bytes], None],
|
||||
connect_timeout: float = 20.0, operation_timeout: float = 5.0):
|
||||
self.target = target or None
|
||||
self.on_frame = on_frame
|
||||
self.connect_timeout = connect_timeout
|
||||
self.operation_timeout = operation_timeout
|
||||
self.attribute_value = m.MAXIMUM_MESSAGE
|
||||
self.device_name = ''
|
||||
self.device_address = ''
|
||||
self.error: Optional[BaseException] = None
|
||||
self.client = None
|
||||
self.loop = asyncio.new_event_loop()
|
||||
self.stop_event = None
|
||||
self.ready = threading.Event()
|
||||
self.delivery: 'queue.Queue[Optional[bytes]]' = queue.Queue()
|
||||
self.delivery_thread = threading.Thread(target=self._deliver, name='microbu-ble-delivery', daemon=True)
|
||||
self.delivery_thread.start()
|
||||
self.thread = threading.Thread(target=self._thread_main, name='microbu-ble-asyncio', daemon=True)
|
||||
self.thread.start()
|
||||
if not self.ready.wait(connect_timeout + 5):
|
||||
self.close()
|
||||
raise TimeoutError('BLE scan/connect did not finish')
|
||||
if self.error:
|
||||
error = self.error
|
||||
self.close()
|
||||
raise ConnectionError('BLE station-link connection failed: %s' % error) from error
|
||||
|
||||
def _thread_main(self):
|
||||
asyncio.set_event_loop(self.loop)
|
||||
try:
|
||||
self.loop.run_until_complete(self._session())
|
||||
except BaseException as error:
|
||||
self.error = error
|
||||
self.ready.set()
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
||||
async def _session(self):
|
||||
from bleak import BleakClient, BleakScanner
|
||||
|
||||
target = self.target.lower() if self.target else None
|
||||
|
||||
def matches(device, advertisement):
|
||||
names = (device.name or '', advertisement.local_name or '')
|
||||
services = [uuid.lower() for uuid in advertisement.service_uuids]
|
||||
if target:
|
||||
return target == device.address.lower() or any(target == name.lower() for name in names)
|
||||
return SERVICE_UUID in services or any(name.lower().startswith('microbu-') for name in names)
|
||||
|
||||
device = await BleakScanner.find_device_by_filter(matches, timeout=self.connect_timeout)
|
||||
if device is None:
|
||||
wanted = self.target or 'advertised micrOBU service'
|
||||
raise TimeoutError('no BLE device matching %s' % wanted)
|
||||
self.device_name = device.name or 'micrOBU'
|
||||
self.device_address = device.address
|
||||
|
||||
self.stop_event = asyncio.Event()
|
||||
self.client = BleakClient(device, timeout=self.connect_timeout, pair=True,
|
||||
disconnected_callback=self._disconnected)
|
||||
await self.client.connect()
|
||||
|
||||
# LE Secure Connections + passkey display takes ~1-3 s; wait for the link
|
||||
# to be encrypted before attempting CCCD writes on authenticated characteristics.
|
||||
for _ in range(30):
|
||||
await asyncio.sleep(0.1)
|
||||
try:
|
||||
# Attempt a read on STATUS (Read|Encrypt) as an encryption probe.
|
||||
# Once it succeeds (or gives ATT error other than InsufficientEncryption)
|
||||
# the link is encrypted and CCCD writes on notify characteristics will work.
|
||||
await self.client.read_gatt_char(STATUS_UUID)
|
||||
break
|
||||
except Exception as exc:
|
||||
exc_str = str(exc).lower()
|
||||
if 'insufficient' in exc_str or 'encrypt' in exc_str or 'auth' in exc_str:
|
||||
continue # still pairing
|
||||
break # any other error — proceed anyway
|
||||
|
||||
# Subscribe to peripheral notification characteristics that matter for normal operation
|
||||
# (BTP_INDICATION, ID_EVENT, RESULT). STATUS is polled or read on demand, but can be notified if supported.
|
||||
subscribed_uuids = set()
|
||||
for notif_uuid in (RESULT_UUID, BTP_INDICATION_UUID, ID_EVENT_UUID):
|
||||
for attempt in range(3):
|
||||
try:
|
||||
await self.client.start_notify(notif_uuid, self._notification)
|
||||
subscribed_uuids.add(notif_uuid)
|
||||
break
|
||||
except Exception as exc:
|
||||
if attempt < 2:
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
print(f'BLE: start_notify on {notif_uuid[:8]} failed: {exc}')
|
||||
|
||||
self.attribute_value = min(self.client.mtu_size - 3, m.MAXIMUM_MESSAGE)
|
||||
self.ready.set()
|
||||
await self.stop_event.wait()
|
||||
if self.client.is_connected:
|
||||
await self.client.disconnect()
|
||||
|
||||
def _disconnected(self, _client):
|
||||
if self.stop_event and not self.stop_event.is_set():
|
||||
self.error = ConnectionError('micrOBU disconnected')
|
||||
self.stop_event.set()
|
||||
|
||||
def _notification(self, characteristic, data: bytearray):
|
||||
# Direct immediate message delivery: each GATT notification is one complete link message
|
||||
self.delivery.put(bytes(data))
|
||||
|
||||
def _deliver(self):
|
||||
while True:
|
||||
message = self.delivery.get()
|
||||
if message is None:
|
||||
return
|
||||
try:
|
||||
self.on_frame(FrameType.LINK, message)
|
||||
except Exception as error:
|
||||
print('BLE message handler error:', error)
|
||||
|
||||
def write(self, frame_type: int, payload: bytes):
|
||||
if frame_type != FrameType.LINK:
|
||||
raise NotImplementedError('the USB test/log channels do not exist over BLE')
|
||||
if self.error:
|
||||
raise ConnectionError(str(self.error))
|
||||
if self.client is None or not self.client.is_connected or self.loop.is_closed():
|
||||
raise ConnectionError('BLE station link is not ready')
|
||||
|
||||
# Route write to the specific ETSI C-ITS GATT characteristic.
|
||||
# Check opcode in link header (payload[0])
|
||||
target_uuid = CONFIG_UUID
|
||||
if payload:
|
||||
op = payload[0]
|
||||
if op == m.Opcode.BTP_DATA_REQUEST:
|
||||
target_uuid = BTP_REQUEST_UUID
|
||||
elif op == m.Opcode.POTI_UPDATE:
|
||||
target_uuid = POTI_UUID
|
||||
elif op == m.Opcode.SF_IDCHANGE_EVENT_RESPONSE:
|
||||
target_uuid = ID_EVENT_UUID
|
||||
else:
|
||||
target_uuid = CONFIG_UUID
|
||||
|
||||
async def send():
|
||||
for attempt in range(4):
|
||||
try:
|
||||
await self.client.write_gatt_char(target_uuid, bytes(payload), response=True)
|
||||
return
|
||||
except Exception as error:
|
||||
if 'insufficient resource' not in str(error).lower() or attempt == 3:
|
||||
raise
|
||||
await asyncio.sleep(0.1 * (attempt + 1))
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(send(), self.loop)
|
||||
try:
|
||||
future.result(timeout=self.operation_timeout)
|
||||
except Exception as error:
|
||||
raise ConnectionError('BLE write failed: %s' % error) from error
|
||||
|
||||
def close(self):
|
||||
if self.stop_event and not self.loop.is_closed():
|
||||
self.loop.call_soon_threadsafe(self.stop_event.set)
|
||||
if hasattr(self, 'thread') and self.thread.is_alive() and threading.current_thread() is not self.thread:
|
||||
self.thread.join(timeout=self.operation_timeout)
|
||||
self.delivery.put(None)
|
||||
if self.delivery_thread.is_alive() and threading.current_thread() is not self.delivery_thread:
|
||||
self.delivery_thread.join(timeout=1)
|
||||
@@ -0,0 +1,245 @@
|
||||
"""Request/reply client of the station-internal link plus the USB test channel."""
|
||||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from . import messages as m
|
||||
from .serial_transport import FrameType, SerialTransport
|
||||
from .transport import TransportAdapter
|
||||
|
||||
|
||||
class LinkError(RuntimeError):
|
||||
def __init__(self, result: m.Result, request: str):
|
||||
super().__init__('%s refused: %s' % (request, result.name))
|
||||
self.result = result
|
||||
|
||||
|
||||
class LinkClient:
|
||||
"""The phone's view of the micrOBU: send requests, await their RESULT, receive the
|
||||
micrOBU's own messages (indications, identifier-change events, status, log lines)
|
||||
through callbacks. Thread-safe for one request at a time."""
|
||||
|
||||
def __init__(self, port: Optional[str] = None, timeout: float = 3.0,
|
||||
transport_factory: Optional[Callable[[Callable[[int, bytes], None]], TransportAdapter]] = None):
|
||||
self.timeout = timeout
|
||||
self.sequence = 0
|
||||
self.lock = threading.Lock()
|
||||
self.replies: 'queue.Queue[m.Message]' = queue.Queue()
|
||||
self.on_indication: Optional[Callable[[m.BtpDataIndication], None]] = None
|
||||
self.on_id_event: Optional[Callable[[m.IdChangeEvent, int], None]] = None
|
||||
self.on_mf_set: Optional[Callable[[m.MfSetRequest], None]] = None
|
||||
self.on_status: Optional[Callable[[m.Status], None]] = None
|
||||
self.on_log: Optional[Callable[[str], None]] = None
|
||||
self.last_status: Optional[m.Status] = None
|
||||
self.test_replies: 'queue.Queue[bytes]' = queue.Queue()
|
||||
if transport_factory:
|
||||
self.transport = transport_factory(self._on_frame)
|
||||
elif port:
|
||||
self.transport = SerialTransport(port, self._on_frame)
|
||||
else:
|
||||
raise ValueError('port or transport_factory is required')
|
||||
|
||||
def close(self):
|
||||
self.transport.close()
|
||||
|
||||
# ---- receive side ----
|
||||
def _on_frame(self, frame_type: int, payload: bytes):
|
||||
if frame_type == FrameType.LOG:
|
||||
if self.on_log:
|
||||
self.on_log(payload.decode('utf-8', 'replace'))
|
||||
return
|
||||
if frame_type == FrameType.TEST:
|
||||
self.test_replies.put(payload)
|
||||
return
|
||||
if frame_type != FrameType.LINK:
|
||||
return
|
||||
try:
|
||||
message = m.decode_message(payload)
|
||||
except m.DecodeError:
|
||||
return
|
||||
op = message.header.opcode
|
||||
if op == m.Opcode.RESULT or (op == m.Opcode.STATUS and message.header.sequence == self._awaited_status):
|
||||
self.replies.put(message)
|
||||
elif op == m.Opcode.BTP_DATA_INDICATION:
|
||||
if self.on_indication:
|
||||
self.on_indication(m.BtpDataIndication.decode(message.body))
|
||||
elif op == m.Opcode.SF_IDCHANGE_EVENT:
|
||||
if self.on_id_event:
|
||||
self.on_id_event(m.IdChangeEvent.decode(message.body), message.header.sequence)
|
||||
elif op == m.Opcode.MF_SET_REQUEST:
|
||||
if self.on_mf_set:
|
||||
self.on_mf_set(m.MfSetRequest.decode(message.body))
|
||||
elif op == m.Opcode.STATUS:
|
||||
self.last_status = m.Status.decode(message.body)
|
||||
if self.on_status:
|
||||
self.on_status(self.last_status)
|
||||
|
||||
_awaited_status = -1
|
||||
|
||||
# ---- send side ----
|
||||
def send(self, opcode: int, body: bytes = b'', sequence: Optional[int] = None) -> int:
|
||||
"""Send one message without waiting (indication responses, PoTi updates)."""
|
||||
if sequence is None:
|
||||
self.sequence = (self.sequence + 1) & 0xFFFF
|
||||
sequence = self.sequence
|
||||
self.transport.write(FrameType.LINK, m.encode_message(m.Message(m.Header(opcode, 0, sequence), body)))
|
||||
return sequence
|
||||
|
||||
def request(self, opcode: int, body: bytes = b'', timeout: Optional[float] = None) -> m.Result:
|
||||
"""Send a request and wait for its RESULT (raises LinkError unless accepted)."""
|
||||
with self.lock:
|
||||
while not self.replies.empty(): # stale replies of timed-out requests
|
||||
self.replies.get_nowait()
|
||||
sequence = self.send(opcode, body)
|
||||
deadline = time.monotonic() + (timeout or self.timeout)
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError('no RESULT for opcode 0x%02x (sequence %d)' % (opcode, sequence))
|
||||
try:
|
||||
reply = self.replies.get(timeout=remaining)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if reply.header.sequence != sequence or reply.header.opcode != m.Opcode.RESULT:
|
||||
continue
|
||||
result = m.Result.decode(reply.body)
|
||||
if result.code != m.Code.accepted:
|
||||
raise LinkError(result, m.Opcode(opcode).name)
|
||||
return result
|
||||
|
||||
def request_async(self, opcode: int, body: bytes = b'') -> int:
|
||||
"""Submit a request without serializing on its RESULT.
|
||||
|
||||
The peer still emits the ordinary per-request RESULT. Call
|
||||
receive_result() to consume it. This is used for the 100 ms VAM stream,
|
||||
where a BLE connection interval must not become the service period.
|
||||
"""
|
||||
with self.lock:
|
||||
return self.send(opcode, body)
|
||||
|
||||
def receive_result(self, timeout: Optional[float] = None) -> Tuple[int, m.Result]:
|
||||
reply = self.replies.get(timeout=self.timeout if timeout is None else timeout)
|
||||
if reply.header.opcode != m.Opcode.RESULT:
|
||||
raise ValueError('expected RESULT, got opcode 0x%02x' % reply.header.opcode)
|
||||
return reply.header.sequence, m.Result.decode(reply.body)
|
||||
|
||||
# ---- primitives ----
|
||||
def configure(self, config: m.StationConfigure) -> m.StationInfo:
|
||||
# Starting the ESP32-C5 ITS-G5 radio initializes the Wi-Fi/PHY driver
|
||||
# before the station can reply. Keep ordinary request timeouts short,
|
||||
# but allow this idempotent lifecycle operation to finish on hardware.
|
||||
return m.StationInfo.decode(self.request(m.Opcode.STATION_CONFIGURE, config.encode(), timeout=30).detail)
|
||||
|
||||
def poti(self, update: m.PotiUpdate):
|
||||
"""Silent unless refused; a refusal arrives as a RESULT that request() ignores."""
|
||||
self.send(m.Opcode.POTI_UPDATE, update.encode())
|
||||
|
||||
def btp_data_request(self, request: m.BtpDataRequest) -> m.Result:
|
||||
return self.request(m.Opcode.BTP_DATA_REQUEST, request.encode())
|
||||
|
||||
def provision(self, bundle: bytes) -> Tuple[int, int, int]:
|
||||
result = None
|
||||
for segment in m.provision_segments(bundle):
|
||||
result = self.request(m.Opcode.CREDENTIALS_PROVISION, segment.encode(), timeout=20)
|
||||
assert result is not None and len(result.detail) == 3, 'apply report expected'
|
||||
return result.detail[0], result.detail[1], result.detail[2]
|
||||
|
||||
def erase_credentials(self):
|
||||
self.request(m.Opcode.CREDENTIALS_ERASE, timeout=10)
|
||||
|
||||
def subscribe(self, subscriber_data: bytes = b'') -> int:
|
||||
result = self.request(m.Opcode.SF_IDCHANGE_SUBSCRIBE, bytes([len(subscriber_data)]) + subscriber_data)
|
||||
return int.from_bytes(result.detail, 'little')
|
||||
|
||||
def unsubscribe(self, subscription: int):
|
||||
self.request(m.Opcode.SF_IDCHANGE_UNSUBSCRIBE, subscription.to_bytes(8, 'little'))
|
||||
|
||||
def event_response(self, event: m.IdChangeEvent, event_sequence: int, return_code: bool):
|
||||
self.send(m.Opcode.SF_IDCHANGE_EVENT_RESPONSE, m.IdChangeEventResponse(event.subscription, return_code).encode(),
|
||||
sequence=event_sequence)
|
||||
|
||||
def trigger(self):
|
||||
self.request(m.Opcode.SF_IDCHANGE_TRIGGER)
|
||||
|
||||
def lock(self, seconds: int) -> int:
|
||||
return int.from_bytes(self.request(m.Opcode.SF_ID_LOCK, bytes([seconds])).detail, 'little')
|
||||
|
||||
def unlock(self, handle: int):
|
||||
self.request(m.Opcode.SF_ID_UNLOCK, handle.to_bytes(8, 'little'))
|
||||
|
||||
def status(self, timeout: float = 5.0) -> m.Status:
|
||||
with self.lock:
|
||||
while not self.replies.empty():
|
||||
self.replies.get_nowait()
|
||||
self.sequence = (self.sequence + 1) & 0xFFFF
|
||||
self._awaited_status = self.sequence
|
||||
try:
|
||||
self.transport.write(FrameType.LINK, m.encode_message(m.Message(m.Header(m.Opcode.STATUS_REQUEST, 0, self.sequence))))
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError('no STATUS')
|
||||
reply = self.replies.get(timeout=remaining)
|
||||
if reply.header.opcode == m.Opcode.STATUS and reply.header.sequence == self.sequence:
|
||||
self.last_status = m.Status.decode(reply.body)
|
||||
return self.last_status
|
||||
finally:
|
||||
self._awaited_status = -1
|
||||
|
||||
|
||||
class TestChannel:
|
||||
"""USB test channel (frame type 0x11): software lower tester and campaign hooks."""
|
||||
|
||||
MIRROR, INJECT, GN_REQUEST, DRAIN, RESET = 0x01, 0x02, 0x03, 0x04, 0x05
|
||||
|
||||
def __init__(self, client: LinkClient, timeout: float = 3.0):
|
||||
self.client = client
|
||||
self.timeout = timeout
|
||||
|
||||
def execute(self, request: bytes) -> Tuple[int, List[Tuple[int, bytes]]]:
|
||||
with self.client.lock:
|
||||
while not self.client.test_replies.empty():
|
||||
self.client.test_replies.get_nowait()
|
||||
self.client.transport.write(FrameType.TEST, request)
|
||||
reply = self.client.test_replies.get(timeout=self.timeout)
|
||||
result, count = reply[0], reply[1]
|
||||
records, at = [], 2
|
||||
for _ in range(count):
|
||||
kind = reply[at]
|
||||
length = int.from_bytes(reply[at + 1:at + 3], 'little')
|
||||
records.append((kind, reply[at + 3:at + 3 + length]))
|
||||
at += 3 + length
|
||||
if at != len(reply):
|
||||
raise ValueError('trailing test channel octets')
|
||||
return result, records
|
||||
|
||||
def mirror(self, mode: int):
|
||||
result, _ = self.execute(bytes([self.MIRROR, mode]))
|
||||
if result != 0:
|
||||
raise RuntimeError('mirror mode refused: %d' % result)
|
||||
|
||||
def inject(self, source: bytes, destination: bytes, gnpdu: bytes) -> int:
|
||||
result, _ = self.execute(bytes([self.INJECT]) + source + destination + gnpdu)
|
||||
return result
|
||||
|
||||
def gn_request(self, traffic_class: int, payload: bytes) -> int:
|
||||
result, _ = self.execute(bytes([self.GN_REQUEST, traffic_class]) + payload)
|
||||
return result
|
||||
|
||||
def drain(self) -> Tuple[bool, List[Tuple[int, bytes]]]:
|
||||
"""All queued records (several frames when needed); returns (overflow, records)."""
|
||||
records: List[Tuple[int, bytes]] = []
|
||||
overflow = False
|
||||
while True:
|
||||
result, batch = self.execute(bytes([self.DRAIN]))
|
||||
overflow = overflow or result == m.Code.resource_limit
|
||||
records.extend(batch)
|
||||
if not batch:
|
||||
return overflow, records
|
||||
|
||||
def reset(self):
|
||||
self.execute(bytes([self.RESET]))
|
||||
@@ -0,0 +1,466 @@
|
||||
"""Message layer version 1 of the micrOBU station-internal link.
|
||||
|
||||
Mirror of the firmware's ``link_protocol.hpp``; the two are checked against each other by
|
||||
``tests/test_station_link.py``. All integers little-endian; ETSI payloads opaque.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
MAXIMUM_MESSAGE = 512
|
||||
HEADER_SIZE = 4
|
||||
|
||||
|
||||
class Opcode(enum.IntEnum):
|
||||
STATION_CONFIGURE = 0x01
|
||||
POTI_UPDATE = 0x02
|
||||
BTP_DATA_REQUEST = 0x03
|
||||
CREDENTIALS_PROVISION = 0x04
|
||||
CREDENTIALS_ERASE = 0x05
|
||||
SF_IDCHANGE_SUBSCRIBE = 0x06
|
||||
SF_IDCHANGE_UNSUBSCRIBE = 0x07
|
||||
SF_IDCHANGE_EVENT_RESPONSE = 0x08
|
||||
SF_IDCHANGE_TRIGGER = 0x09
|
||||
SF_ID_LOCK = 0x0A
|
||||
SF_ID_UNLOCK = 0x0B
|
||||
STATUS_REQUEST = 0x0C
|
||||
RESULT = 0x80
|
||||
BTP_DATA_INDICATION = 0x81
|
||||
SF_IDCHANGE_EVENT = 0x82
|
||||
MF_SET_REQUEST = 0x83
|
||||
STATUS = 0x84
|
||||
|
||||
|
||||
class Code(enum.IntEnum):
|
||||
"""RESULT codes: vanetza_idf::Result first, then link codes."""
|
||||
accepted = 0
|
||||
invalid_argument = 1
|
||||
unsupported = 2
|
||||
wrong_entry_point = 3
|
||||
security_unavailable = 4
|
||||
resource_limit = 5
|
||||
rejected = 6
|
||||
time_regression = 7
|
||||
identity_change_pending = 8
|
||||
unknown_opcode = 0x10
|
||||
malformed = 0x11
|
||||
not_configured = 0x12
|
||||
busy = 0x13
|
||||
no_credentials = 0x14
|
||||
|
||||
|
||||
FLAG_FRAGMENT_FIRST = 0x01
|
||||
FLAG_FRAGMENT_MORE = 0x02
|
||||
|
||||
|
||||
class IdChangeCommand(enum.IntEnum):
|
||||
PREPARE = 0
|
||||
COMMIT = 1
|
||||
ABORT = 2
|
||||
DEREG = 3
|
||||
|
||||
|
||||
class TransportType(enum.IntEnum):
|
||||
SHB = 1
|
||||
GBC = 2
|
||||
TSB = 3
|
||||
GAC = 4
|
||||
GUC = 5
|
||||
|
||||
|
||||
VERIFICATION_REPORTS = ['unsecured', 'Success', 'False_Signature', 'Invalid_Certificate', 'Revoked_Certificate',
|
||||
'Inconsistent_Chain', 'Invalid_Timestamp', 'Duplicate_Message', 'Invalid_Mobility_Data',
|
||||
'Unsigned_Message', 'Signer_Certificate_Not_Found', 'Unsupported_Signer_Identifier_Type',
|
||||
'Incompatible_Protocol', 'Configuration_Problem']
|
||||
|
||||
|
||||
class DecodeError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class _Reader:
|
||||
def __init__(self, data: bytes, at: int = 0):
|
||||
self.data = data
|
||||
self.at = at
|
||||
|
||||
def _take(self, n: int) -> bytes:
|
||||
if self.at + n > len(self.data):
|
||||
raise DecodeError('truncated body')
|
||||
chunk = self.data[self.at:self.at + n]
|
||||
self.at += n
|
||||
return chunk
|
||||
|
||||
def u8(self) -> int: return self._take(1)[0]
|
||||
def u16(self) -> int: return struct.unpack('<H', self._take(2))[0]
|
||||
def u32(self) -> int: return struct.unpack('<I', self._take(4))[0]
|
||||
def u64(self) -> int: return struct.unpack('<Q', self._take(8))[0]
|
||||
def i32(self) -> int: return struct.unpack('<i', self._take(4))[0]
|
||||
def bytes(self, n: int) -> bytes: return self._take(n)
|
||||
def rest(self) -> bytes:
|
||||
chunk = self.data[self.at:]
|
||||
self.at = len(self.data)
|
||||
return chunk
|
||||
|
||||
def done(self):
|
||||
if self.at != len(self.data):
|
||||
raise DecodeError('trailing octets')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Header:
|
||||
opcode: int
|
||||
flags: int = 0
|
||||
sequence: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
header: Header
|
||||
body: bytes = b''
|
||||
|
||||
|
||||
def encode_message(message: Message) -> bytes:
|
||||
if HEADER_SIZE + len(message.body) > MAXIMUM_MESSAGE:
|
||||
raise ValueError('message exceeds %d octets' % MAXIMUM_MESSAGE)
|
||||
return struct.pack('<BBH', int(message.header.opcode), message.header.flags, message.header.sequence) + bytes(message.body)
|
||||
|
||||
|
||||
def decode_message(octets: bytes) -> Message:
|
||||
if len(octets) < HEADER_SIZE or len(octets) > MAXIMUM_MESSAGE:
|
||||
raise DecodeError('bad message length %d' % len(octets))
|
||||
opcode, flags, sequence = struct.unpack('<BBH', octets[:HEADER_SIZE])
|
||||
return Message(Header(opcode, flags, sequence), bytes(octets[HEADER_SIZE:]))
|
||||
|
||||
|
||||
# ---- bodies ----------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class StationConfigure:
|
||||
station_type: int = 2
|
||||
security: int = 1
|
||||
address_configuration: int = 1
|
||||
mid: bytes = b'\x02\x00\x00\x00\x00\x01'
|
||||
beaconing: int = 1
|
||||
channel_number: int = 180
|
||||
transmit_power_dbm: int = 10
|
||||
radio: int = 0
|
||||
default_traffic_class: int = 2
|
||||
default_lifetime: int = 0x05
|
||||
|
||||
def encode(self) -> bytes:
|
||||
assert len(self.mid) == 6
|
||||
return struct.pack('<BBB6sBHBBBB', self.station_type, self.security, self.address_configuration, self.mid,
|
||||
self.beaconing, self.channel_number, self.transmit_power_dbm, self.radio,
|
||||
self.default_traffic_class, self.default_lifetime)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'StationConfigure':
|
||||
if len(body) != 16:
|
||||
raise DecodeError('StationConfigure length')
|
||||
f = struct.unpack('<BBB6sBHBBBB', body)
|
||||
return cls(*f)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StationInfo:
|
||||
"""RESULT detail of STATION_CONFIGURE."""
|
||||
gn_address: bytes
|
||||
identifier: bytes
|
||||
credentials_loaded: bool
|
||||
tickets: int
|
||||
|
||||
@classmethod
|
||||
def decode(cls, detail: bytes) -> 'StationInfo':
|
||||
if len(detail) != 18:
|
||||
raise DecodeError('StationInfo length')
|
||||
return cls(detail[:8], detail[8:16], detail[16] != 0, detail[17])
|
||||
|
||||
|
||||
@dataclass
|
||||
class PotiUpdate:
|
||||
timestamp_ms: int
|
||||
latitude: int
|
||||
longitude: int
|
||||
semi_major_cm: int = 0
|
||||
semi_minor_cm: int = 0
|
||||
orientation_deci_degree: int = 0
|
||||
altitude_cm: Optional[int] = None
|
||||
speed_cm_s: Optional[int] = None
|
||||
heading_deci_degree: Optional[int] = None
|
||||
pai: bool = False
|
||||
|
||||
def encode(self) -> bytes:
|
||||
flags = ((1 if self.altitude_cm is not None else 0) | (2 if self.speed_cm_s is not None else 0) |
|
||||
(4 if self.heading_deci_degree is not None else 0) | (8 if self.pai else 0))
|
||||
return struct.pack('<QiiHHHBiHH', self.timestamp_ms, self.latitude, self.longitude, self.semi_major_cm,
|
||||
self.semi_minor_cm, self.orientation_deci_degree, flags, self.altitude_cm or 0,
|
||||
self.speed_cm_s or 0, self.heading_deci_degree or 0)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'PotiUpdate':
|
||||
if len(body) != 31:
|
||||
raise DecodeError('PotiUpdate length')
|
||||
t, lat, lon, a, b, o, flags, alt, spd, hdg = struct.unpack('<QiiHHHBiHH', body)
|
||||
return cls(t, lat, lon, a, b, o, alt if flags & 1 else None, spd if flags & 2 else None,
|
||||
hdg if flags & 4 else None, bool(flags & 8))
|
||||
|
||||
|
||||
@dataclass
|
||||
class DestinationArea:
|
||||
shape: int = 0
|
||||
latitude: int = 0
|
||||
longitude: int = 0
|
||||
distance_a: int = 0
|
||||
distance_b: int = 0
|
||||
angle: int = 0
|
||||
|
||||
def encode(self) -> bytes:
|
||||
return struct.pack('<BiiHHH', self.shape, self.latitude, self.longitude, self.distance_a, self.distance_b, self.angle)
|
||||
|
||||
@classmethod
|
||||
def read(cls, r: _Reader) -> 'DestinationArea':
|
||||
return cls(r.u8(), r.i32(), r.i32(), r.u16(), r.u16(), r.u16())
|
||||
|
||||
|
||||
@dataclass
|
||||
class BtpDataRequest:
|
||||
fl_sdu: bytes
|
||||
btp_type: int = 1
|
||||
destination_port: int = 2018
|
||||
destination_port_info: int = 0
|
||||
gn_packet_transport_type: int = TransportType.SHB
|
||||
gn_communication_profile: int = 1
|
||||
gn_security_profile: int = 0
|
||||
gn_traffic_class: int = 0xFF
|
||||
gn_maximum_packet_lifetime: int = 0xFF
|
||||
gn_maximum_hop_limit: int = 0
|
||||
gn_repetition_interval_ms: int = 0
|
||||
gn_repetition_maximum_ms: int = 0
|
||||
its_aid: int = 638
|
||||
permissions: bytes = b''
|
||||
context: bytes = b''
|
||||
area: Optional[DestinationArea] = None
|
||||
|
||||
def encode(self) -> bytes:
|
||||
if len(self.permissions) > 31:
|
||||
raise ValueError('SSP longer than 31 octets')
|
||||
out = struct.pack('<BHHBBBBBBHHI', self.btp_type, self.destination_port, self.destination_port_info,
|
||||
self.gn_packet_transport_type, self.gn_communication_profile, self.gn_security_profile,
|
||||
self.gn_traffic_class, self.gn_maximum_packet_lifetime, self.gn_maximum_hop_limit,
|
||||
self.gn_repetition_interval_ms, self.gn_repetition_maximum_ms, self.its_aid)
|
||||
out += bytes([len(self.permissions)]) + self.permissions + bytes([len(self.context)]) + self.context
|
||||
if self.gn_packet_transport_type == TransportType.GBC:
|
||||
out += (self.area or DestinationArea()).encode()
|
||||
out += struct.pack('<H', len(self.fl_sdu)) + self.fl_sdu
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'BtpDataRequest':
|
||||
r = _Reader(body)
|
||||
f = struct.unpack('<BHHBBBBBBHHI', r.bytes(19))
|
||||
permissions = r.bytes(r.u8())
|
||||
context = r.bytes(r.u8())
|
||||
area = DestinationArea.read(r) if f[3] == TransportType.GBC else None
|
||||
sdu = r.bytes(r.u16())
|
||||
r.done()
|
||||
return cls(sdu, *f, permissions=permissions, context=context, area=area)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BtpDataIndication:
|
||||
received_fl_sdu: bytes
|
||||
btp_type: int = 1
|
||||
destination_port: int = 0
|
||||
destination_port_info: int = 0
|
||||
gn_packet_transport_type: int = 1
|
||||
gn_traffic_class: int = 0
|
||||
gn_remaining_packet_lifetime: int = 0xFF
|
||||
gn_remaining_hop_limit: int = 0xFF
|
||||
source_gn_address: bytes = b'\0' * 8
|
||||
source_timestamp: int = 0
|
||||
source_latitude: int = 0
|
||||
source_longitude: int = 0
|
||||
security_report: int = 0
|
||||
its_aid: int = 0
|
||||
permissions: bytes = b''
|
||||
certificate_id: Optional[bytes] = None
|
||||
area: Optional[DestinationArea] = None
|
||||
|
||||
@property
|
||||
def report_name(self) -> str:
|
||||
return VERIFICATION_REPORTS[self.security_report] if self.security_report < len(VERIFICATION_REPORTS) else str(self.security_report)
|
||||
|
||||
def encode(self) -> bytes:
|
||||
out = struct.pack('<BHHBBBB8sIiiBI', self.btp_type, self.destination_port, self.destination_port_info,
|
||||
self.gn_packet_transport_type, self.gn_traffic_class, self.gn_remaining_packet_lifetime,
|
||||
self.gn_remaining_hop_limit, self.source_gn_address, self.source_timestamp,
|
||||
self.source_latitude, self.source_longitude, self.security_report, self.its_aid)
|
||||
out += bytes([len(self.permissions)]) + self.permissions
|
||||
out += bytes([1 if self.certificate_id else 0]) + (self.certificate_id or b'\0' * 8)
|
||||
out += bytes([1 if self.area else 0]) + (self.area.encode() if self.area else b'')
|
||||
out += struct.pack('<H', len(self.received_fl_sdu)) + self.received_fl_sdu
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'BtpDataIndication':
|
||||
r = _Reader(body)
|
||||
f = struct.unpack('<BHHBBBB8sIiiBI', r.bytes(34))
|
||||
permissions = r.bytes(r.u8())
|
||||
present = r.u8()
|
||||
certificate = r.bytes(8)
|
||||
area = DestinationArea.read(r) if r.u8() else None
|
||||
sdu = r.bytes(r.u16())
|
||||
r.done()
|
||||
return cls(sdu, *f, permissions=permissions, certificate_id=certificate if present else None, area=area)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CredentialsProvision:
|
||||
total_length: int
|
||||
offset: int
|
||||
segment: bytes
|
||||
|
||||
def encode(self) -> bytes:
|
||||
return struct.pack('<HHB', self.total_length, self.offset, len(self.segment)) + self.segment
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'CredentialsProvision':
|
||||
r = _Reader(body)
|
||||
total, offset = r.u16(), r.u16()
|
||||
segment = r.bytes(r.u8())
|
||||
r.done()
|
||||
return cls(total, offset, segment)
|
||||
|
||||
|
||||
def provision_segments(bundle: bytes, segment_size: int = 200) -> List[CredentialsProvision]:
|
||||
"""Split a VCR1 bundle into CREDENTIALS_PROVISION messages."""
|
||||
return [CredentialsProvision(len(bundle), at, bundle[at:at + segment_size]) for at in range(0, len(bundle), segment_size)]
|
||||
|
||||
|
||||
@dataclass
|
||||
class IdChangeEvent:
|
||||
subscription: int
|
||||
command: int
|
||||
id: bytes
|
||||
subscriber_data: bytes = b''
|
||||
|
||||
def encode(self) -> bytes:
|
||||
return struct.pack('<QB8sB', self.subscription, self.command, self.id, len(self.subscriber_data)) + self.subscriber_data
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'IdChangeEvent':
|
||||
r = _Reader(body)
|
||||
s, c, i = struct.unpack('<QB8s', r.bytes(17))
|
||||
data = r.bytes(r.u8())
|
||||
r.done()
|
||||
return cls(s, c, i, data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IdChangeEventResponse:
|
||||
subscription: int
|
||||
return_code: bool
|
||||
|
||||
def encode(self) -> bytes:
|
||||
return struct.pack('<QB', self.subscription, 1 if self.return_code else 0)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'IdChangeEventResponse':
|
||||
if len(body) != 9:
|
||||
raise DecodeError('IdChangeEventResponse length')
|
||||
s, c = struct.unpack('<QB', body)
|
||||
return cls(s, c != 0)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MfSetRequest:
|
||||
fac_id: int = 0
|
||||
command_ref: int = 0
|
||||
params: List[tuple] = field(default_factory=list) # (f_param_no, value)
|
||||
|
||||
def encode(self) -> bytes:
|
||||
out = struct.pack('<IBB', self.fac_id, self.command_ref, len(self.params))
|
||||
for no, value in self.params:
|
||||
out += struct.pack('<BI', no, value)
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'MfSetRequest':
|
||||
r = _Reader(body)
|
||||
fac, ref, count = struct.unpack('<IBB', r.bytes(6))
|
||||
params = [struct.unpack('<BI', r.bytes(5)) for _ in range(count)]
|
||||
r.done()
|
||||
return cls(fac, ref, params)
|
||||
|
||||
|
||||
STATUS_FORMAT = '<IB8s8sBB' + 'I' * 7 + 'I' * 3 + 'I' * 4 + 'I' * 4 + 'Q'
|
||||
STATUS_FIELDS = ['uptime_ms', 'configured', 'gn_address', 'identifier', 'change_pending', 'tickets',
|
||||
'signed_messages', 'refused_no_ticket', 'refused_change_pending', 'refused_permission',
|
||||
'sign_failed', 'verified', 'rejected', 'requests_accepted', 'requests_refused', 'indications',
|
||||
'radio_submitted', 'radio_failed', 'radio_received', 'radio_dropped', 'link_rx_frames',
|
||||
'link_crc_errors', 'link_malformed', 'poti_updates', 'its_time_ms']
|
||||
|
||||
|
||||
@dataclass
|
||||
class Status:
|
||||
uptime_ms: int = 0
|
||||
configured: int = 0
|
||||
gn_address: bytes = b'\0' * 8
|
||||
identifier: bytes = b'\0' * 8
|
||||
change_pending: int = 0
|
||||
tickets: int = 0
|
||||
signed_messages: int = 0
|
||||
refused_no_ticket: int = 0
|
||||
refused_change_pending: int = 0
|
||||
refused_permission: int = 0
|
||||
sign_failed: int = 0
|
||||
verified: int = 0
|
||||
rejected: int = 0
|
||||
requests_accepted: int = 0
|
||||
requests_refused: int = 0
|
||||
indications: int = 0
|
||||
radio_submitted: int = 0
|
||||
radio_failed: int = 0
|
||||
radio_received: int = 0
|
||||
radio_dropped: int = 0
|
||||
link_rx_frames: int = 0
|
||||
link_crc_errors: int = 0
|
||||
link_malformed: int = 0
|
||||
poti_updates: int = 0
|
||||
its_time_ms: int = 0
|
||||
|
||||
def encode(self) -> bytes:
|
||||
return struct.pack(STATUS_FORMAT, *[getattr(self, name) for name in STATUS_FIELDS])
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'Status':
|
||||
if len(body) != struct.calcsize(STATUS_FORMAT):
|
||||
raise DecodeError('Status length %d' % len(body))
|
||||
return cls(**dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, body))))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
code: int
|
||||
detail: bytes = b''
|
||||
|
||||
def encode(self) -> bytes:
|
||||
return bytes([int(self.code), len(self.detail)]) + self.detail
|
||||
|
||||
@classmethod
|
||||
def decode(cls, body: bytes) -> 'Result':
|
||||
r = _Reader(body)
|
||||
code = r.u8()
|
||||
detail = r.bytes(r.u8())
|
||||
r.done()
|
||||
return cls(code, detail)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
try:
|
||||
return Code(self.code).name
|
||||
except ValueError:
|
||||
return 'code_0x%02x' % self.code
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Serial transport of the station-internal link (the app's Phase 03 framing).
|
||||
|
||||
``[AA][55][type][length LE][payload][crc16 LE]``, CRC-16/CCITT-FALSE over type+length+payload.
|
||||
Frame types: 0x10 link message, 0x11 test channel, 0x7F log line. Requires pyserial.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import struct
|
||||
import threading
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
MAXIMUM_PAYLOAD = 1536
|
||||
|
||||
|
||||
class FrameType(enum.IntEnum):
|
||||
LINK = 0x10
|
||||
TEST = 0x11
|
||||
LOG = 0x7F
|
||||
|
||||
|
||||
def crc16_ccitt_false(data: bytes) -> int:
|
||||
crc = 0xFFFF
|
||||
for octet in data:
|
||||
crc ^= octet << 8
|
||||
for _ in range(8):
|
||||
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
|
||||
return crc
|
||||
|
||||
|
||||
def encode_frame(frame_type: int, payload: bytes) -> bytes:
|
||||
if len(payload) > MAXIMUM_PAYLOAD:
|
||||
raise ValueError('frame payload exceeds %d octets' % MAXIMUM_PAYLOAD)
|
||||
head = struct.pack('<BH', int(frame_type), len(payload)) + payload
|
||||
return b'\xaa\x55' + head + struct.pack('<H', crc16_ccitt_false(head))
|
||||
|
||||
|
||||
class FrameDecoder:
|
||||
"""Byte-at-a-time state machine, the app's SerialFrameDecoder with the larger payload limit."""
|
||||
|
||||
def __init__(self):
|
||||
self.state = 'SYNC0'
|
||||
self.type = 0
|
||||
self.length = 0
|
||||
self.payload = bytearray()
|
||||
self.crc = 0
|
||||
self.crc_errors = 0
|
||||
self.frames = 0
|
||||
|
||||
def feed(self, data: bytes) -> List[Tuple[int, bytes]]:
|
||||
out = []
|
||||
for b in data:
|
||||
s = self.state
|
||||
if s == 'SYNC0':
|
||||
self.state = 'SYNC1' if b == 0xAA else 'SYNC0'
|
||||
elif s == 'SYNC1':
|
||||
self.state = 'TYPE' if b == 0x55 else ('SYNC1' if b == 0xAA else 'SYNC0')
|
||||
elif s == 'TYPE':
|
||||
self.type = b
|
||||
self.state = 'LEN_LO'
|
||||
elif s == 'LEN_LO':
|
||||
self.length = b
|
||||
self.state = 'LEN_HI'
|
||||
elif s == 'LEN_HI':
|
||||
self.length |= b << 8
|
||||
self.payload = bytearray()
|
||||
if self.length > MAXIMUM_PAYLOAD:
|
||||
self.state = 'SYNC0'
|
||||
else:
|
||||
self.state = 'CRC_LO' if self.length == 0 else 'PAYLOAD'
|
||||
elif s == 'PAYLOAD':
|
||||
self.payload.append(b)
|
||||
if len(self.payload) >= self.length:
|
||||
self.state = 'CRC_LO'
|
||||
elif s == 'CRC_LO':
|
||||
self.crc = b
|
||||
self.state = 'CRC_HI'
|
||||
elif s == 'CRC_HI':
|
||||
self.crc |= b << 8
|
||||
head = struct.pack('<BH', self.type, self.length) + bytes(self.payload)
|
||||
if crc16_ccitt_false(head) == self.crc:
|
||||
self.frames += 1
|
||||
out.append((self.type, bytes(self.payload)))
|
||||
else:
|
||||
self.crc_errors += 1
|
||||
self.state = 'SYNC0'
|
||||
return out
|
||||
|
||||
|
||||
class SerialTransport:
|
||||
"""Reader thread plus a locked writer over one pyserial port."""
|
||||
|
||||
def __init__(self, port: str, on_frame: Callable[[int, bytes], None], baudrate: int = 115200):
|
||||
import serial
|
||||
self.port = serial.Serial(port=None, baudrate=baudrate, timeout=0.05, write_timeout=2)
|
||||
# do not toggle DTR/RTS: the USB Serial/JTAG controller would reset the board
|
||||
self.port.dtr = False
|
||||
self.port.rts = False
|
||||
self.port.port = port
|
||||
self.port.open()
|
||||
self.on_frame = on_frame
|
||||
self.decoder = FrameDecoder()
|
||||
self.write_lock = threading.Lock()
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._reader, name='microbu-link-rx', daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def _reader(self):
|
||||
while self.running:
|
||||
try:
|
||||
data = self.port.read(4096)
|
||||
except Exception:
|
||||
if self.running:
|
||||
continue
|
||||
return
|
||||
if data:
|
||||
for frame_type, payload in self.decoder.feed(data):
|
||||
try:
|
||||
self.on_frame(frame_type, payload)
|
||||
except Exception as error: # a handler failure must not stop the reader
|
||||
print('frame handler error:', error)
|
||||
|
||||
def write(self, frame_type: int, payload: bytes):
|
||||
frame = encode_frame(frame_type, payload)
|
||||
with self.write_lock:
|
||||
if self.port.write(frame) != len(frame):
|
||||
raise IOError('incomplete serial write')
|
||||
|
||||
def close(self):
|
||||
self.running = False
|
||||
try:
|
||||
self.port.cancel_read()
|
||||
except Exception:
|
||||
pass
|
||||
self.thread.join(timeout=1)
|
||||
self.port.close()
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Common adapter boundary for station-link transports."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class TransportAdapter(Protocol):
|
||||
"""The only transport surface used by LinkClient."""
|
||||
|
||||
def write(self, frame_type: int, payload: bytes) -> None: ...
|
||||
|
||||
def close(self) -> None: ...
|
||||
@@ -0,0 +1,239 @@
|
||||
"""A small VRU basic service for the phone emulator: VAM assembly with asn1tools from the
|
||||
ETSI ASN.1 modules, the individual-VAM generation rules of ETSI TS 103 300-3 V2.3.1
|
||||
clause 6.4 (items 1 to 4, Tables 16 and 17), the VBS PCI of Table 4, and a PoTi stand-in.
|
||||
|
||||
It is a test stand-in for the phone app's VBS, not a conformant VBS: no clustering,
|
||||
no redundancy mitigation, no reception management, no motion prediction.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from . import messages as m
|
||||
|
||||
ITS_EPOCH_UNIX = 1072915200
|
||||
LEAP_SECONDS_SINCE_2004 = 5 # TAI-UTC 37 s now, 32 s at the epoch (2017-01-01 was the last insertion)
|
||||
|
||||
# ETSI TS 103 300-3 V2.3.1 Table 16 / Table 17 recommended values
|
||||
T_GEN_VAM_MIN_MS = 100
|
||||
T_GEN_VAM_MAX_MS = 5000
|
||||
T_GEN_VAM_LF_MIN_MS = 2000
|
||||
MIN_POSITION_CHANGE_M = 4.0
|
||||
MIN_SPEED_CHANGE_MPS = 0.5
|
||||
MIN_ORIENTATION_CHANGE_DEG = 4.0
|
||||
|
||||
# TS 102 965 ITS-AID of the VRU service, TS 103 248 BTP port of VAM
|
||||
ITS_AID_VRU = 638
|
||||
BTP_PORT_VAM = 2018
|
||||
|
||||
|
||||
def its_timestamp_ms(unix_seconds: Optional[float] = None) -> int:
|
||||
"""TS 102 894-2 TimestampIts: TAI milliseconds since 2004-01-01T00:00:00Z."""
|
||||
if unix_seconds is None:
|
||||
unix_seconds = time.time()
|
||||
return int((unix_seconds - ITS_EPOCH_UNIX + LEAP_SECONDS_SINCE_2004) * 1000)
|
||||
|
||||
|
||||
def default_asn1_directory() -> Path:
|
||||
here = Path(__file__).resolve()
|
||||
return here.parents[3] / 'external' / 'vanetza-idf' / 'asn1' / 'release2'
|
||||
|
||||
|
||||
class Codec:
|
||||
"""UPER codec for VAM (and CAM/DENM for the displays) from the submodule's ASN.1 modules."""
|
||||
|
||||
def __init__(self, directory: Optional[Path] = None):
|
||||
import asn1tools
|
||||
directory = directory or default_asn1_directory()
|
||||
cdd = directory / 'TS102894-2v241-CDD.asn'
|
||||
self.vam = asn1tools.compile_files([cdd, directory / 'TS103300-3v231' / 'VAM-PDU-Descriptions.asn',
|
||||
directory / 'TS103300-3v231' / 'motorcyclist-special-container.asn'], 'uper')
|
||||
self.vam_type = self.vam.modules['VAM-PDU-Descriptions']['VAM']
|
||||
self.cam_type = None
|
||||
self.denm_type = None
|
||||
try:
|
||||
cam = asn1tools.compile_files([cdd, directory / 'TS103900v231-CAM.asn'], 'uper')
|
||||
self.cam_type = cam.modules['CAM-PDU-Descriptions']['CAM']
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
denm = asn1tools.compile_files([cdd, directory / 'TS103831v231-DENM.asn'], 'uper')
|
||||
self.denm_type = denm.modules['DENM-PDU-Description']['DENM']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def encode_vam(self, vam: dict) -> bytes:
|
||||
return self.vam_type.encode(vam)
|
||||
|
||||
def decode_vam(self, octets: bytes) -> dict:
|
||||
return self.vam_type.decode(octets)
|
||||
|
||||
def decode_by_port(self, port: int, octets: bytes):
|
||||
if port == BTP_PORT_VAM:
|
||||
return 'VAM', self.decode_vam(octets)
|
||||
if port == 2001 and self.cam_type:
|
||||
return 'CAM', self.cam_type.decode(octets)
|
||||
if port == 2002 and self.denm_type:
|
||||
return 'DENM', self.denm_type.decode(octets)
|
||||
return None, None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PotiState:
|
||||
"""What PoTi reports: WGS84 position with a 95 % confidence ellipse, kinematics, time."""
|
||||
timestamp_ms: int
|
||||
latitude: float
|
||||
longitude: float
|
||||
speed_mps: float = 0.0
|
||||
heading_deg: float = 0.0
|
||||
altitude_m: Optional[float] = None
|
||||
semi_major_m: float = 1.5
|
||||
semi_minor_m: float = 1.0
|
||||
orientation_deg: float = 0.0
|
||||
|
||||
def poti_update(self) -> m.PotiUpdate:
|
||||
return m.PotiUpdate(
|
||||
timestamp_ms=self.timestamp_ms,
|
||||
latitude=int(round(self.latitude * 1e7)), longitude=int(round(self.longitude * 1e7)),
|
||||
semi_major_cm=min(65535, int(round(self.semi_major_m * 100))),
|
||||
semi_minor_cm=min(65535, int(round(self.semi_minor_m * 100))),
|
||||
orientation_deci_degree=int(round(self.orientation_deg * 10)) % 3600,
|
||||
altitude_cm=None if self.altitude_m is None else int(round(self.altitude_m * 100)),
|
||||
speed_cm_s=min(65535, int(round(self.speed_mps * 100))),
|
||||
heading_deci_degree=int(round(self.heading_deg * 10)) % 3600,
|
||||
pai=self.semi_major_m * 2 <= 40.0) # itsGnPaiInterval / 2, EN 302 636-4-1 / TS 103 836-4-1 clause 8.4
|
||||
|
||||
|
||||
class PotiSimulator:
|
||||
"""Static position, or a bicycle riding a circle (radius r at speed v) for the triggers."""
|
||||
|
||||
def __init__(self, latitude: float, longitude: float, speed_mps: float = 0.0, radius_m: float = 30.0,
|
||||
altitude_m: Optional[float] = 12.0):
|
||||
self.center = (latitude, longitude)
|
||||
self.speed = speed_mps
|
||||
self.radius = radius_m
|
||||
self.altitude = altitude_m
|
||||
self.start = time.time()
|
||||
|
||||
def state(self, now: Optional[float] = None) -> PotiState:
|
||||
now = time.time() if now is None else now
|
||||
if self.speed <= 0:
|
||||
return PotiState(its_timestamp_ms(now), self.center[0], self.center[1], 0.0, 0.0, self.altitude)
|
||||
angle = (now - self.start) * self.speed / self.radius # rad, counter-clockwise
|
||||
lat = self.center[0] + math.degrees(self.radius * math.sin(angle) / 6371000.0)
|
||||
lon = self.center[1] + math.degrees(self.radius * math.cos(angle) / (6371000.0 * math.cos(math.radians(self.center[0]))))
|
||||
heading = (math.degrees(angle) * -1 + 0.0) % 360 # tangent of a counter-clockwise circle, clockwise from north
|
||||
return PotiState(its_timestamp_ms(now), lat, lon, self.speed, heading, self.altitude)
|
||||
|
||||
|
||||
def haversine_m(a: PotiState, b: PotiState) -> float:
|
||||
r = 6371000.0
|
||||
p1, p2 = math.radians(a.latitude), math.radians(b.latitude)
|
||||
dp = p2 - p1
|
||||
dl = math.radians(b.longitude - a.longitude)
|
||||
h = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
|
||||
return 2 * r * math.asin(math.sqrt(h))
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbsLite:
|
||||
"""VRU-ACTIVE-STANDALONE VBS state for one bicyclist (VRU profile 2, sub-profile bicyclist)."""
|
||||
codec: Codec
|
||||
station_id: int
|
||||
t_gen_vam_ms: int = T_GEN_VAM_MIN_MS # T_GenVam, from the management entity (DCC) in a real VBS
|
||||
fixed_period_ms: Optional[int] = None # test override: one VAM every period regardless of the triggers
|
||||
active: bool = True # false between PREPARE and COMMIT of an identifier change
|
||||
last_vam: Optional[PotiState] = None
|
||||
last_vam_at_ms: int = 0
|
||||
last_lf_at_ms: int = -10 ** 9
|
||||
generated: int = 0
|
||||
security_profile: int = 2 # Table 4: SECURED or UNSECURED
|
||||
ssp: bytes = b'\x01' # SSP of the VRU ITS-AID carried in the authorization ticket
|
||||
traffic_class: int = 0xFF # "same GN traffic class value as for the CAM": the station default
|
||||
lifetime: int = 0x05 # GN maximum packet lifetime 1 s (Table 4: shall not exceed 1 000 ms)
|
||||
|
||||
def due(self, now: PotiState) -> bool:
|
||||
if not self.active:
|
||||
return False
|
||||
elapsed = now.timestamp_ms - self.last_vam_at_ms
|
||||
if self.fixed_period_ms is not None:
|
||||
return self.last_vam is None or elapsed >= self.fixed_period_ms
|
||||
if self.last_vam is None:
|
||||
return True
|
||||
if elapsed < max(self.t_gen_vam_ms, T_GEN_VAM_MIN_MS):
|
||||
return False
|
||||
if elapsed > T_GEN_VAM_MAX_MS:
|
||||
return True # item 1
|
||||
if haversine_m(now, self.last_vam) > MIN_POSITION_CHANGE_M:
|
||||
return True # item 2
|
||||
if abs(now.speed_mps - self.last_vam.speed_mps) > MIN_SPEED_CHANGE_MPS:
|
||||
return True # item 3
|
||||
delta = abs((now.heading_deg - self.last_vam.heading_deg + 180) % 360 - 180)
|
||||
return delta > MIN_ORIENTATION_CHANGE_DEG # item 4
|
||||
|
||||
def assemble(self, now: PotiState) -> bytes:
|
||||
"""VAM per TS 103 300-3 clause 7.3: basic + HF container, LF container every T_GenVamLFMin."""
|
||||
include_lf = now.timestamp_ms - self.last_lf_at_ms >= T_GEN_VAM_LF_MIN_MS
|
||||
vam = {
|
||||
'header': {'protocolVersion': 3, 'messageId': 16, 'stationId': self.station_id},
|
||||
'vam': {
|
||||
'generationDeltaTime': now.timestamp_ms % 65536,
|
||||
'vamParameters': {
|
||||
'basicContainer': {
|
||||
'stationType': 2, # cyclist
|
||||
'referencePosition': {
|
||||
'latitude': int(round(now.latitude * 1e7)),
|
||||
'longitude': int(round(now.longitude * 1e7)),
|
||||
'positionConfidenceEllipse': {
|
||||
'semiMajorAxisLength': min(4094, int(round(now.semi_major_m * 100))),
|
||||
'semiMinorAxisLength': min(4094, int(round(now.semi_minor_m * 100))),
|
||||
'semiMajorAxisOrientation': int(round(now.orientation_deg * 10)) % 3600,
|
||||
},
|
||||
'altitude': ({'altitudeValue': max(-100000, min(800000, int(round(now.altitude_m * 100)))),
|
||||
'altitudeConfidence': 'alt-020-00'} if now.altitude_m is not None
|
||||
else {'altitudeValue': 800001, 'altitudeConfidence': 'unavailable'}),
|
||||
},
|
||||
},
|
||||
'vruHighFrequencyContainer': {
|
||||
'heading': {'value': int(round(now.heading_deg * 10)) % 3600, 'confidence': 50},
|
||||
'speed': {'speedValue': min(16382, int(round(now.speed_mps * 100))), 'speedConfidence': 50},
|
||||
'longitudinalAcceleration': {'longitudinalAccelerationValue': 0, 'longitudinalAccelerationConfidence': 102},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if include_lf:
|
||||
vam['vam']['vamParameters']['vruLowFrequencyContainer'] = {
|
||||
'profileAndSubprofile': ('bicyclistAndLightVruVehicle', 1), # bicyclist
|
||||
'sizeClass': 1, # low
|
||||
}
|
||||
self.last_lf_at_ms = now.timestamp_ms
|
||||
octets = self.codec.encode_vam(vam)
|
||||
self.last_vam = now
|
||||
self.last_vam_at_ms = now.timestamp_ms
|
||||
self.generated += 1
|
||||
return octets
|
||||
|
||||
def btp_data_request(self, vam: bytes) -> m.BtpDataRequest:
|
||||
"""The VBS networking PCI of TS 103 300-3 Table 4 for one VAM."""
|
||||
return m.BtpDataRequest(
|
||||
fl_sdu=vam, btp_type=1, destination_port=BTP_PORT_VAM, destination_port_info=0,
|
||||
gn_packet_transport_type=m.TransportType.SHB, gn_communication_profile=1,
|
||||
gn_security_profile=self.security_profile, gn_traffic_class=self.traffic_class,
|
||||
gn_maximum_packet_lifetime=self.lifetime, its_aid=ITS_AID_VRU, permissions=self.ssp)
|
||||
|
||||
# TS 103 300-3 clause 5.3.5: stop on PREPARE, resume with a new StationId after COMMIT
|
||||
def prepare(self):
|
||||
self.active = False
|
||||
|
||||
def commit(self, new_station_id: int):
|
||||
self.station_id = new_station_id
|
||||
self.active = True
|
||||
self.last_vam = None
|
||||
|
||||
def abort(self):
|
||||
self.active = True
|
||||
@@ -0,0 +1,491 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phone emulator: the phone half of the VRU ITS-S, on a PC, over serial or BLE.
|
||||
|
||||
It does what the app will do over BLE: configure the micrOBU, provision credentials
|
||||
(a VCR1 bundle), feed PoTi, run a small VRU basic service that assembles VAMs and hands
|
||||
them to the micrOBU as BTP-DATA.request with the PCI of TS 103 300-3 Table 4, subscribe
|
||||
to the identifier-change events (stop on PREPARE, new StationId after COMMIT), show the
|
||||
BTP-DATA.indications the micrOBU passes up and its status/log lines.
|
||||
|
||||
phone_emulator.py --port COM11 --bundle chain.vcr --radio txrx --duration 30
|
||||
phone_emulator.py --port COM11 --bundle chain.vcr --radio off --divert --pcap vams.pcap --duration 20
|
||||
phone_emulator.py --ble --bundle chain.vcr --radio off --duration 20
|
||||
|
||||
--divert keeps the packets from the radio and mirrors them through the USB test channel
|
||||
(test firmware only), --pcap writes those as IEEE 802.11 frames for an independent verifier
|
||||
(c-its-pcap). BLE uses the same LinkClient API but cannot expose the USB-only test
|
||||
channel. Dependencies are listed in requirements.txt.
|
||||
|
||||
The "TS 102 941 client" role (architecture/diagrams/microbu-architecture-2b-revision-b-security.drawio):
|
||||
--pki-* replaces --bundle with a real online enrolment (EA) then authorization (AA) round
|
||||
trip over HTTP (TS 102 941 V2.2.1 clause 6.2.3), building the VCR1 bundle from the
|
||||
resulting AT instead of an offline-issued ticket:
|
||||
|
||||
phone_emulator.py --port COM11 --radio txrx --duration 30 \
|
||||
--pki-url http://127.0.0.1:8090/ --pki-root ROOT.oer --pki-ea EA.oer --pki-aa AA.oer \
|
||||
--pki-canonical-key canonical.pem --pki-its-id vidf-vru-station \
|
||||
--pki-issue-tool vidf_issue.exe --pki-client-tool pki_client.py --pki-bundle-tool credential_bundle.py
|
||||
|
||||
--pki-url is a tools/local_pki.py access point; --pki-issue-tool/--pki-client-tool/
|
||||
--pki-bundle-tool are vanetza-idf's ports/esp_idf/{tools/local_pki.py's vidf_issue binary,
|
||||
tools/pki_client.py, tools/credential_bundle.py}. Lab tooling stands in for the phone's own
|
||||
TS 102 941 stack; see docs/idf/evidence/enrolment-authorization-01 in vanetza-idf.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import queue
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from microbu_link import BleTransport, LinkClient, LinkError, TestChannel, messages as m # noqa: E402
|
||||
from microbu_link.vbs import Codec, PotiSimulator, VbsLite, its_timestamp_ms # noqa: E402
|
||||
|
||||
WILDCARD_BSSID = b'\xff' * 6
|
||||
GEONETWORKING_ETHERTYPE = b'\x89\x47'
|
||||
|
||||
# Simulation-only trajectory over open water in the middle of the Außenalster.
|
||||
# Keeping the synthetic VRU away from roads prevents it from appearing to be a
|
||||
# real road user in receiving applications. This does not eliminate RF airtime;
|
||||
# actual transmission remains opt-in through ``--radio txrx``.
|
||||
AUSSENALSTER_CENTER_LAT = 53.565247
|
||||
AUSSENALSTER_CENTER_LON = 10.008239
|
||||
AUSSENALSTER_RADIUS_M = 20.0
|
||||
AUSSENALSTER_SPEED_MPS = 2.0
|
||||
|
||||
|
||||
def tool_command(path, *arguments):
|
||||
"""Build a portable command line for a native tool or Python script."""
|
||||
executable = str(path)
|
||||
prefix = [sys.executable, executable] if Path(executable).suffix.lower() == '.py' else [executable]
|
||||
return prefix + [str(argument) for argument in arguments]
|
||||
|
||||
|
||||
def run_tool(path, *arguments):
|
||||
"""Run one PKI helper and turn its diagnostic into a library-style error."""
|
||||
command = tool_command(path, *arguments)
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
except OSError as error:
|
||||
raise RuntimeError('%s failed: %s' % (Path(path).name, error)) from error
|
||||
if result.returncode:
|
||||
diagnostic = result.stderr.strip() or result.stdout.strip() or 'exit status %d' % result.returncode
|
||||
raise RuntimeError('%s failed: %s' % (Path(path).name, diagnostic))
|
||||
return result
|
||||
|
||||
|
||||
def write_pcap(path, frames):
|
||||
"""IEEE 802.11 data frames (linktype 105), LLC/SNAP 0x8947, like capture_pcap.py."""
|
||||
with open(path, 'wb') as out:
|
||||
out.write(struct.pack('<IHHiIII', 0xa1b2c3d4, 2, 4, 0, 0, 65535, 105))
|
||||
for sequence, (timestamp, source, pdu) in enumerate(frames):
|
||||
header = (struct.pack('<HH', 0x0008, 0) + WILDCARD_BSSID + source + WILDCARD_BSSID +
|
||||
struct.pack('<H', (sequence & 0x0fff) << 4))
|
||||
frame = header + b'\xaa\xaa\x03\x00\x00\x00' + GEONETWORKING_ETHERTYPE + pdu
|
||||
out.write(struct.pack('<IIII', int(timestamp), int((timestamp % 1) * 1_000_000), len(frame), len(frame)))
|
||||
out.write(frame)
|
||||
|
||||
|
||||
class Emulator:
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.codec = Codec(Path(args.asn1) if args.asn1 else None)
|
||||
self.log_lines = []
|
||||
if args.ble is not None:
|
||||
self.client = LinkClient(timeout=args.timeout, transport_factory=lambda receive: BleTransport(
|
||||
args.ble or None, receive, connect_timeout=args.connect_timeout,
|
||||
operation_timeout=max(args.timeout, 5.0)))
|
||||
else:
|
||||
self.client = LinkClient(args.port, timeout=args.timeout)
|
||||
self.client.on_log = self.on_log
|
||||
self.client.on_indication = self.on_indication
|
||||
self.client.on_id_event = self.on_id_event
|
||||
self.client.on_mf_set = lambda mf: self.say('MF-SET fac_id=%d params=%s' % (mf.fac_id, mf.params))
|
||||
self.client.on_status = self.on_status
|
||||
self.poti = PotiSimulator(args.lat, args.lon, args.speed, args.radius)
|
||||
self.vbs = VbsLite(self.codec, args.station_id or random.randrange(1, 2 ** 32 - 1),
|
||||
fixed_period_ms=args.period, security_profile=2,
|
||||
ssp=bytes.fromhex(args.ssp))
|
||||
self.subscription = None
|
||||
self.stop = threading.Event()
|
||||
self.lock = threading.Lock()
|
||||
self.frames = [] # (unix time, source mac, gnpdu) from the test channel
|
||||
self.events = []
|
||||
self.station_info = None
|
||||
self.test = TestChannel(self.client) if (args.divert or args.mirror) else None
|
||||
self.pending = {}
|
||||
self.pending_lock = threading.Lock()
|
||||
self.results_accepted = 0
|
||||
self.results_refused = 0
|
||||
self.assembly_ms = []
|
||||
self.request_latency_ms = []
|
||||
self.generation_times = []
|
||||
self.stream_started = None
|
||||
self.stream_ended = None
|
||||
|
||||
# ---- output ----
|
||||
def say(self, text):
|
||||
line = '%s %s' % (time.strftime('%H:%M:%S'), text)
|
||||
print(line, flush=True)
|
||||
self.events.append(line)
|
||||
|
||||
def on_log(self, line):
|
||||
if self.args.quiet_log:
|
||||
return
|
||||
print(' [micrOBU] %s' % line, flush=True)
|
||||
self.log_lines.append(line)
|
||||
|
||||
def on_status(self, s: m.Status):
|
||||
if self.args.verbose:
|
||||
self.say('STATUS its_time=%d signed=%d refused(no ticket %d, pending %d, permission %d) failed=%d '
|
||||
'req ok/refused=%d/%d ind=%d radio tx/fail/rx/drop=%d/%d/%d/%d id=%s pending=%d tickets=%d' % (
|
||||
s.its_time_ms, s.signed_messages, s.refused_no_ticket, s.refused_change_pending,
|
||||
s.refused_permission, s.sign_failed, s.requests_accepted, s.requests_refused, s.indications,
|
||||
s.radio_submitted, s.radio_failed, s.radio_received, s.radio_dropped, s.identifier.hex(),
|
||||
s.change_pending, s.tickets))
|
||||
|
||||
def on_indication(self, ind: m.BtpDataIndication):
|
||||
kind, decoded = None, None
|
||||
try:
|
||||
kind, decoded = self.codec.decode_by_port(ind.destination_port, ind.received_fl_sdu)
|
||||
except Exception as error:
|
||||
kind = 'undecodable (%s)' % error
|
||||
summary = ''
|
||||
if isinstance(decoded, dict):
|
||||
header = decoded.get('header', {})
|
||||
summary = ' stationId=%s' % header.get('stationId')
|
||||
self.say('BTP-DATA.indication port=%d %d B from %s report=%s its_aid=%d ssp=%s cert=%s %s%s' % (
|
||||
ind.destination_port, len(ind.received_fl_sdu), ind.source_gn_address.hex(), ind.report_name, ind.its_aid,
|
||||
ind.permissions.hex(), ind.certificate_id.hex() if ind.certificate_id else '-', kind or 'port %d' % ind.destination_port, summary))
|
||||
|
||||
# TS 103 300-3 clause 5.3.5 through the SF-SAP identifier-change events
|
||||
def on_id_event(self, event: m.IdChangeEvent, sequence: int):
|
||||
command = m.IdChangeCommand(event.command).name
|
||||
self.say('SF-IDCHANGE-EVENT %s id=%s' % (command, event.id.hex()))
|
||||
if event.command == m.IdChangeCommand.PREPARE:
|
||||
self.vbs.prepare()
|
||||
self.client.event_response(event, sequence, True)
|
||||
elif event.command == m.IdChangeCommand.COMMIT:
|
||||
new_id = random.randrange(1, 2 ** 32 - 1)
|
||||
self.vbs.commit(new_id)
|
||||
self.say('VBS resumes with StationId %d' % new_id)
|
||||
self.client.event_response(event, sequence, True)
|
||||
elif event.command == m.IdChangeCommand.ABORT:
|
||||
self.vbs.abort()
|
||||
|
||||
# ---- session ----
|
||||
def acquire_online_credentials(self) -> bytes:
|
||||
"""Obtain an EC and AT from the online PKI and pack the station's VCR1 bundle."""
|
||||
a = self.args
|
||||
with tempfile.TemporaryDirectory(prefix='microbu-pki-') as temporary_directory:
|
||||
temporary = Path(temporary_directory)
|
||||
ec_certificate = temporary / 'EC.oer'
|
||||
ec_key = temporary / 'EC.vkey'
|
||||
at_certificate = temporary / 'AT.oer'
|
||||
at_key = temporary / 'AT.vkey'
|
||||
|
||||
run_tool(a.pki_client_tool, '--issue-tool', a.pki_issue_tool, '--pki', a.pki_url,
|
||||
'enrol', '--ea', a.pki_ea, '--canonical-key', a.pki_canonical_key,
|
||||
'--its-id', a.pki_its_id, '--out', ec_certificate, '--out-key', ec_key)
|
||||
self.say('online PKI: enrolment credential obtained')
|
||||
|
||||
run_tool(a.pki_client_tool, '--issue-tool', a.pki_issue_tool, '--pki', a.pki_url,
|
||||
'authorize', '--ea', a.pki_ea, '--aa', a.pki_aa,
|
||||
'--ec', ec_certificate, '--ec-key', ec_key, '--hours', a.pki_hours,
|
||||
'--out', at_certificate, '--out-key', at_key)
|
||||
self.say('online PKI: authorization ticket obtained')
|
||||
|
||||
pool = temporary / 'pool'
|
||||
pool.mkdir()
|
||||
(pool / 'ROOT.oer').write_bytes(Path(a.pki_root).read_bytes())
|
||||
(pool / 'AA.oer').write_bytes(Path(a.pki_aa).read_bytes())
|
||||
(pool / 'AT.oer').write_bytes(at_certificate.read_bytes())
|
||||
(pool / 'AT.vkey').write_bytes(at_key.read_bytes())
|
||||
bundle_path = temporary / 'credentials.vcr'
|
||||
run_tool(a.pki_bundle_tool, 'build', '--pool', pool, '--root', 'ROOT',
|
||||
'--aa', 'AA', '--at', 'AT', '--out', bundle_path)
|
||||
bundle = bundle_path.read_bytes()
|
||||
self.say('online PKI: VCR1 credential bundle built')
|
||||
return bundle
|
||||
|
||||
def start(self):
|
||||
a = self.args
|
||||
if a.ble is not None:
|
||||
transport = self.client.transport
|
||||
self.say('BLE connected: %s (%s), GATT value=%d B' %
|
||||
(transport.device_name, transport.device_address, transport.attribute_value))
|
||||
else:
|
||||
self.say('serial connected: %s' % a.port)
|
||||
self.say('PoTi trajectory: centre=%.7f,%.7f radius=%.1f m speed=%.1f m/s' %
|
||||
(a.lat, a.lon, a.radius, a.speed))
|
||||
config = m.StationConfigure(security=1, address_configuration=1,
|
||||
beaconing=0 if a.no_beacons else 1, channel_number=a.channel,
|
||||
transmit_power_dbm=a.power, radio={'off': 0, 'rx': 1, 'txrx': 2}[a.radio])
|
||||
if self.test: # armed before the station exists: even its first beacon reaches the tester
|
||||
self.test.reset()
|
||||
self.test.mirror(2 if a.divert else 1)
|
||||
self.say('test channel: %s' % ('divert (nothing reaches the radio)' if a.divert else 'mirror'))
|
||||
self.station_info = self.client.configure(config)
|
||||
self.say('station configured: gn_address=%s identifier=%s credentials=%s tickets=%d' % (
|
||||
self.station_info.gn_address.hex(), self.station_info.identifier.hex(),
|
||||
self.station_info.credentials_loaded, self.station_info.tickets))
|
||||
if a.erase_credentials:
|
||||
self.client.erase_credentials()
|
||||
self.say('credentials erased')
|
||||
if a.bundle or a.pki_url:
|
||||
bundle = self.acquire_online_credentials() if a.pki_url else Path(a.bundle).read_bytes()
|
||||
roots, authorities, tickets = self.client.provision(bundle)
|
||||
self.say('credentials provisioned: %d roots, %d authorities, %d tickets' % (roots, authorities, tickets))
|
||||
self.station_info = self.client.configure(config) # the station was rebuilt with them
|
||||
self.say('station rebuilt: gn_address=%s identifier=%s' % (self.station_info.gn_address.hex(), self.station_info.identifier.hex()))
|
||||
# first PoTi fix before anything is sent: the micrOBU needs time and position
|
||||
self.client.poti(self.poti.state().poti_update())
|
||||
try:
|
||||
self.subscription = self.client.subscribe(b'VBS')
|
||||
self.say('SF-IDCHANGE-SUBSCRIBE: subscription %d' % self.subscription)
|
||||
except TimeoutError:
|
||||
self.subscription = 1
|
||||
self.say('SF-IDCHANGE-SUBSCRIBE acknowledgement missed; proceeding with fallback subscription')
|
||||
|
||||
def poti_loop(self):
|
||||
period = 1.0 / self.args.poti_rate
|
||||
while not self.stop.is_set():
|
||||
with self.lock:
|
||||
self.client.poti(self.poti.state().poti_update())
|
||||
self.stop.wait(period)
|
||||
|
||||
def drain_loop(self):
|
||||
while not self.stop.is_set():
|
||||
self.stop.wait(0.5)
|
||||
self.drain()
|
||||
|
||||
def drain(self):
|
||||
if not self.test:
|
||||
return
|
||||
with self.lock:
|
||||
overflow, records = self.test.drain()
|
||||
if overflow:
|
||||
self.say('test channel queue overflowed: records were lost')
|
||||
for kind, data in records:
|
||||
if kind == 1:
|
||||
source = self.station_info.gn_address[2:8] if self.station_info else b'\0' * 6
|
||||
self.frames.append((time.time(), source, data))
|
||||
secured = len(data) > 4 and (data[0] & 0x0f) == 2
|
||||
self.say('AL_DATA.request %d B GNPDU (%s)' % (len(data), 'secured' if secured else 'unsecured'))
|
||||
|
||||
def vbs_loop(self):
|
||||
deadline = time.time() + self.args.duration if self.args.duration else None
|
||||
next_trigger = time.time() + self.args.id_change_every if self.args.id_change_every else None
|
||||
fixed_period = self.args.period / 1000.0 if self.args.period is not None else None
|
||||
next_vam = time.perf_counter() if fixed_period is not None else None
|
||||
self.stream_started = time.perf_counter()
|
||||
while not self.stop.is_set():
|
||||
now = self.poti.state()
|
||||
due = time.perf_counter() >= next_vam if next_vam is not None else self.vbs.due(now)
|
||||
if due and not self.args.no_vam:
|
||||
assembly_started = time.perf_counter()
|
||||
vam = self.vbs.assemble(now)
|
||||
request = self.vbs.btp_data_request(vam)
|
||||
encoded_request = request.encode()
|
||||
assembled_at = time.perf_counter()
|
||||
self.assembly_ms.append((assembled_at - assembly_started) * 1000.0)
|
||||
self.generation_times.append(assembled_at)
|
||||
if next_vam is not None:
|
||||
next_vam += fixed_period
|
||||
if next_vam < assembled_at:
|
||||
next_vam = assembled_at + fixed_period
|
||||
try:
|
||||
with self.lock:
|
||||
self.client.btp_data_request(request)
|
||||
self.say('VAM %d (%d B, StationId %d) -> BTP-DATA.request accepted' % (self.vbs.generated, len(vam), self.vbs.station_id))
|
||||
except LinkError as error:
|
||||
self.say('VAM %d refused: %s' % (self.vbs.generated, error.result.name))
|
||||
except TimeoutError as error:
|
||||
self.say('VAM %d: %s' % (self.vbs.generated, error))
|
||||
if next_trigger and time.time() >= next_trigger:
|
||||
next_trigger = time.time() + self.args.id_change_every
|
||||
try:
|
||||
with self.lock:
|
||||
self.client.trigger()
|
||||
self.say('SF-IDCHANGE-TRIGGER sent')
|
||||
except LinkError as error:
|
||||
self.say('SF-IDCHANGE-TRIGGER refused: %s' % error.result.name)
|
||||
if deadline and time.time() >= deadline:
|
||||
break
|
||||
wait = 0.01
|
||||
if next_vam is not None:
|
||||
wait = min(wait, max(0.0, next_vam - time.perf_counter()))
|
||||
self.stop.wait(wait)
|
||||
self.stream_ended = time.perf_counter()
|
||||
|
||||
def run(self):
|
||||
self.start()
|
||||
threads = [threading.Thread(target=self.poti_loop, daemon=True)]
|
||||
if self.test:
|
||||
threads.append(threading.Thread(target=self.drain_loop, daemon=True))
|
||||
for t in threads:
|
||||
t.start()
|
||||
try:
|
||||
self.vbs_loop()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.stop.set()
|
||||
time.sleep(0.3)
|
||||
self.drain()
|
||||
try:
|
||||
with self.lock:
|
||||
status = self.client.status()
|
||||
self.say('final STATUS: signed=%d refused(no ticket %d, pending %d, permission %d) failed=%d req ok/refused=%d/%d '
|
||||
'ind=%d radio tx/fail/rx/drop=%d/%d/%d/%d' % (
|
||||
status.signed_messages, status.refused_no_ticket, status.refused_change_pending,
|
||||
status.refused_permission, status.sign_failed, status.requests_accepted, status.requests_refused,
|
||||
status.indications, status.radio_submitted, status.radio_failed, status.radio_received, status.radio_dropped))
|
||||
except Exception as error:
|
||||
self.say('final STATUS unavailable: %s' % error)
|
||||
status = None
|
||||
if self.subscription is not None:
|
||||
try:
|
||||
with self.lock:
|
||||
self.client.unsubscribe(self.subscription)
|
||||
except Exception:
|
||||
pass
|
||||
if self.test:
|
||||
try:
|
||||
self.test.mirror(0)
|
||||
except Exception:
|
||||
pass
|
||||
if self.args.pcap and self.frames:
|
||||
write_pcap(self.args.pcap, self.frames)
|
||||
self.say('%d frames written to %s' % (len(self.frames), self.args.pcap))
|
||||
if self.args.report:
|
||||
def summary(values):
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return {
|
||||
'samples': len(ordered),
|
||||
'mean': sum(ordered) / len(ordered),
|
||||
'p95': ordered[min(len(ordered) - 1, int(len(ordered) * 0.95))],
|
||||
'max': ordered[-1],
|
||||
}
|
||||
|
||||
intervals_ms = [(right - left) * 1000.0 for left, right in
|
||||
zip(self.generation_times, self.generation_times[1:])]
|
||||
stream_seconds = ((self.stream_ended or time.perf_counter()) - self.stream_started
|
||||
if self.stream_started is not None else 0.0)
|
||||
Path(self.args.report).write_text(json.dumps({
|
||||
'arguments': {k: v for k, v in vars(self.args).items()},
|
||||
'station': None if not self.station_info else {'gn_address': self.station_info.gn_address.hex(),
|
||||
'identifier': self.station_info.identifier.hex(),
|
||||
'tickets': self.station_info.tickets},
|
||||
'vams_generated': self.vbs.generated,
|
||||
'results_accepted': self.results_accepted,
|
||||
'results_refused': self.results_refused,
|
||||
'results_pending': len(self.pending),
|
||||
'timing': {
|
||||
'stream_seconds': stream_seconds,
|
||||
'generation_rate_hz': self.vbs.generated / stream_seconds if stream_seconds else 0.0,
|
||||
'generation_interval_ms': summary(intervals_ms),
|
||||
'vam_assembly_ms': summary(self.assembly_ms),
|
||||
'request_to_result_ms': summary(self.request_latency_ms),
|
||||
},
|
||||
'frames_captured': len(self.frames),
|
||||
'final_status': None if not status else {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(status).items()},
|
||||
'events': self.events,
|
||||
'micrOBU_log': self.log_lines,
|
||||
}, indent=1), encoding='utf-8')
|
||||
self.say('report written to %s' % self.args.report)
|
||||
self.client.close()
|
||||
|
||||
|
||||
def build_argument_parser():
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
transport = p.add_mutually_exclusive_group(required=True)
|
||||
transport.add_argument('--port', help='serial port of the micrOBU (native USB Serial/JTAG)')
|
||||
transport.add_argument('--ble', nargs='?', const='', metavar='ADDRESS_OR_NAME',
|
||||
help='use BLE; optionally select a device address or advertised name')
|
||||
p.add_argument('--connect-timeout', type=float, default=20.0, help='BLE discovery/pairing timeout in seconds')
|
||||
p.add_argument('--bundle', help='VCR1 credential bundle to provision (credential_bundle.py build)')
|
||||
p.add_argument('--pki-url', help='EA/AA access point (tools/local_pki.py), e.g. http://127.0.0.1:8090/')
|
||||
p.add_argument('--pki-root', metavar='ROOT.oer', help='root CA certificate for the VCR1 trust anchor')
|
||||
p.add_argument('--pki-ea', metavar='EA.oer', help='EA certificate used for enrolment')
|
||||
p.add_argument('--pki-aa', metavar='AA.oer', help='AA certificate used for authorization and the VCR1 chain')
|
||||
p.add_argument('--pki-canonical-key', metavar='KEY', help='canonical private key for initial enrolment')
|
||||
p.add_argument('--pki-its-id', metavar='ID', help='canonical station identifier for enrolment')
|
||||
p.add_argument('--pki-issue-tool', metavar='PATH', help='vidf_issue binary built with VIDF_PKI=ON')
|
||||
p.add_argument('--pki-client-tool', metavar='PATH', help='tools/pki_client.py')
|
||||
p.add_argument('--pki-bundle-tool', metavar='PATH', help='tools/credential_bundle.py')
|
||||
p.add_argument('--pki-hours', type=int, default=24, metavar='N', help='AT validity in hours (default: 24)')
|
||||
p.add_argument('--erase-credentials', action='store_true')
|
||||
p.add_argument('--unsecured', action='store_true', help='itsGnSecurity off (no signing, AUTO address)')
|
||||
p.add_argument('--radio', choices=['off', 'rx', 'txrx'], default='off', help='micrOBU radio mode (txrx = laboratory transmission)')
|
||||
p.add_argument('--channel', type=int, default=180)
|
||||
p.add_argument('--power', type=int, default=10, help='transmit power dBm')
|
||||
p.add_argument('--no-beacons', action='store_true')
|
||||
p.add_argument('--lat', type=float, default=AUSSENALSTER_CENTER_LAT,
|
||||
help='circle centre latitude (default: middle of the Außenalster)')
|
||||
p.add_argument('--lon', type=float, default=AUSSENALSTER_CENTER_LON,
|
||||
help='circle centre longitude (default: middle of the Außenalster)')
|
||||
p.add_argument('--speed', type=float, default=AUSSENALSTER_SPEED_MPS,
|
||||
help='m/s on the simulated circle (0 = stationary; default: 2)')
|
||||
p.add_argument('--radius', type=float, default=AUSSENALSTER_RADIUS_M,
|
||||
help='simulated circle radius in metres (default: 20)')
|
||||
p.add_argument('--poti-rate', type=float, default=5.0, help='PoTi updates per second')
|
||||
p.add_argument('--period', type=int, help='fixed VAM period in ms instead of the clause 6.4 triggers')
|
||||
p.add_argument('--station-id', type=int)
|
||||
p.add_argument('--ssp', default='01', help='SSP octets of the VRU ITS-AID (hex)')
|
||||
p.add_argument('--no-vam', action='store_true')
|
||||
p.add_argument('--id-change-every', type=float, help='trigger an identifier change every N seconds')
|
||||
p.add_argument('--duration', type=float, help='seconds to run (default: until Ctrl-C)')
|
||||
p.add_argument('--mirror', action='store_true', help='test channel: copy transmissions (radio still transmits)')
|
||||
p.add_argument('--divert', action='store_true', help='test channel: keep transmissions from the radio')
|
||||
p.add_argument('--pcap', help='write mirrored/diverted GNPDUs as an 802.11 pcap')
|
||||
p.add_argument('--report', help='write a JSON session report')
|
||||
p.add_argument('--asn1', help='directory with the ETSI ASN.1 modules (default: the submodule)')
|
||||
p.add_argument('--timeout', type=float, default=6.0)
|
||||
p.add_argument('--verbose', action='store_true', help='print every STATUS')
|
||||
p.add_argument('--quiet-log', action='store_true', help='hide the micrOBU log lines')
|
||||
return p
|
||||
|
||||
|
||||
def validate_arguments(parser, args):
|
||||
"""Validate credential-source invariants after argument parsing."""
|
||||
required_pki = ('pki_root', 'pki_ea', 'pki_aa', 'pki_canonical_key', 'pki_its_id',
|
||||
'pki_issue_tool', 'pki_client_tool', 'pki_bundle_tool')
|
||||
if args.bundle and args.pki_url:
|
||||
parser.error('--bundle and --pki-url are mutually exclusive')
|
||||
if args.pki_url:
|
||||
missing = ['--' + name.replace('_', '-') for name in required_pki if not getattr(args, name)]
|
||||
if missing:
|
||||
parser.error('--pki-url requires %s' % ', '.join(missing))
|
||||
if args.pki_hours <= 0:
|
||||
parser.error('--pki-hours must be greater than zero')
|
||||
elif any(getattr(args, name) for name in required_pki):
|
||||
parser.error('--pki-* options require --pki-url')
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_argument_parser()
|
||||
args = parser.parse_args()
|
||||
validate_arguments(parser, args)
|
||||
if args.ble is not None and (args.mirror or args.divert or args.pcap):
|
||||
parser.error('--mirror, --divert and --pcap require the USB serial test channel')
|
||||
if args.unsecured:
|
||||
parser.error('All radio transmissions must be signed (unsecured mode not permitted)')
|
||||
try:
|
||||
Emulator(args).run()
|
||||
except ConnectionError as error:
|
||||
parser.exit(2, 'BLE connection error: %s\n' % error)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
asn1tools
|
||||
bleak>=3,<4
|
||||
pyserial
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print micrOBU LOG frames without participating in the station-link session."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import serial
|
||||
|
||||
from microbu_link.serial_transport import FrameDecoder, FrameType
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--port', required=True)
|
||||
parser.add_argument('--duration', type=float, default=15.0)
|
||||
parser.add_argument('--reset', action='store_true', help='pulse EN through USB Serial/JTAG before monitoring')
|
||||
args = parser.parse_args()
|
||||
port = serial.Serial(args.port, 115200, timeout=0.05, write_timeout=1)
|
||||
port.dtr = False
|
||||
port.rts = False
|
||||
if args.reset:
|
||||
port.rts = True
|
||||
time.sleep(0.1)
|
||||
port.rts = False
|
||||
decoder = FrameDecoder()
|
||||
deadline = time.monotonic() + args.duration
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
for frame_type, payload in decoder.feed(port.read(4096)):
|
||||
if frame_type == FrameType.LOG:
|
||||
print(payload.decode('utf-8', 'replace'), flush=True)
|
||||
finally:
|
||||
port.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SUT process for the vanetza-idf ETSI adapters, with the micrOBU behind the phone interfaces.
|
||||
|
||||
The adapters (ports/esp_idf/tests/etsi_*_adapter.cpp) drive a SUT through the hex-line
|
||||
diagnostic protocol of hil_sut.hpp. This bridge answers that protocol the way the phone
|
||||
half of the VRU ITS-S would: BTP-DATA.request, PoTi time/position and credential
|
||||
provisioning go over the station-internal link; the software lower tester (observing
|
||||
AL_DATA.request, injecting AL_DATA.indication) and the GN-DATA.request of the Security
|
||||
ATS use the USB test channel of the test firmware. The CAM/DENM carrier stimuli of the
|
||||
Security ATS are assembled here with asn1tools, as a phone would assemble them.
|
||||
|
||||
Commands (first octet, as in hil_sut.hpp): 0 reset; 1 BTP-DATA.request; 2 AL_DATA.indication;
|
||||
3 GN-DATA.request SHB; 4 position; 5 advance the ITS clock (reports transmissions); 6 carrier
|
||||
CAM/DENM now; 7 periodic carrier; 8 carrier with the next clock advance; 9 credential bundle.
|
||||
Reply: [result][count]{[kind][u16 BE length][bytes]}, kind 1 AL_DATA.request, 2 BTP-DATA.indication,
|
||||
3 GN-DATA.indication.
|
||||
|
||||
sut_bridge.py --port COM11 --bundle certificates.vcr # stdin/stdout, like serial_sut.py
|
||||
sut_bridge.py --port COM11 --bundle certificates.vcr --listen 0.0.0.0:7788 # TCP for a WSL relay
|
||||
|
||||
WSL interop drops piped stdin of Windows executables (docs/idf/test-campaigns.md), so the
|
||||
campaigns use --listen here and sut_tcp_relay.py inside WSL as the SUT executable.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from microbu_link import LinkClient, LinkError, TestChannel, messages as m # noqa: E402
|
||||
from microbu_link.vbs import Codec # noqa: E402
|
||||
|
||||
RESULT_REJECTED = 6
|
||||
|
||||
|
||||
def log(text):
|
||||
print('[sut_bridge] ' + text, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
class Bridge:
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.codec = Codec(Path(args.asn1) if args.asn1 else None)
|
||||
self.client = LinkClient(args.port, timeout=args.timeout)
|
||||
self.client.on_log = lambda line: log('micrOBU: ' + line) if args.verbose else None
|
||||
self.test = TestChannel(self.client, timeout=args.timeout)
|
||||
self.bundle = Path(args.bundle).read_bytes() if args.bundle else None
|
||||
self.secured = self.bundle is not None
|
||||
self.configured = False
|
||||
self.provisioned = False
|
||||
self.latitude, self.longitude = 52.0, 13.0
|
||||
self.time_ms = None
|
||||
self.carrier_interval = [0, 0]
|
||||
self.carrier_next = [0, 0]
|
||||
self.carrier_pending = [None, None]
|
||||
self.denm_sequence = 0
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# ---- station handling ----
|
||||
def reset(self):
|
||||
self.test.reset()
|
||||
config = m.StationConfigure(
|
||||
station_type=5 if self.args.station_type is None else self.args.station_type,
|
||||
security=1 if self.secured else 0,
|
||||
address_configuration=1 if (self.secured and self.args.anonymous) else 0,
|
||||
mid=b'\x02\x00\x00\x00\x00\x01',
|
||||
beaconing=1 if self.secured else 0, # the Security ATS observes GN-MGMT beacons; BTP/GN PICS are unsecured
|
||||
radio=0)
|
||||
info = self.client.configure(config)
|
||||
if self.secured and self.bundle and not self.provisioned:
|
||||
# the campaign's pool, whatever an earlier session left in the micrOBU's NVS
|
||||
roots, authorities, tickets = self.client.provision(self.bundle)
|
||||
log('bundle provisioned: %d roots, %d authorities, %d tickets' % (roots, authorities, tickets))
|
||||
self.provisioned = True
|
||||
info = self.client.configure(config)
|
||||
log('station: gn_address=%s identifier=%s credentials=%s tickets=%d' % (
|
||||
info.gn_address.hex(), info.identifier.hex(), info.credentials_loaded, info.tickets))
|
||||
self.test.mirror(2) # divert: every AL_DATA.request comes to the lower tester, nothing to a radio
|
||||
self.configured = True
|
||||
self.carrier_interval = [0, 0]
|
||||
self.carrier_pending = [None, None]
|
||||
if self.time_ms is None:
|
||||
self.time_ms = int((time.time() - 1072915200 + 5) * 1000)
|
||||
self.poti()
|
||||
return 0
|
||||
|
||||
def poti(self):
|
||||
update = m.PotiUpdate(self.time_ms, int(round(self.latitude * 1e7)), int(round(self.longitude * 1e7)),
|
||||
semi_major_cm=150, semi_minor_cm=100, speed_cm_s=0, heading_deci_degree=0, pai=True)
|
||||
self.client.poti(update)
|
||||
|
||||
def carrier(self, kind, sequence=0):
|
||||
"""Syntactically valid Release 2 CAM (kind 0) or DENM (kind 1), the hil_sut.cpp carriers."""
|
||||
if kind == 0:
|
||||
if not self.codec.cam_type:
|
||||
return 2
|
||||
cam = {'header': {'protocolVersion': 2, 'messageId': 2, 'stationId': 42},
|
||||
'cam': {'generationDeltaTime': self.time_ms % 65536,
|
||||
'camParameters': {
|
||||
'basicContainer': {'stationType': 5, 'referencePosition': {
|
||||
'latitude': 520000000, 'longitude': 130000000,
|
||||
'positionConfidenceEllipse': {'semiMajorAxisLength': 4095, 'semiMinorAxisLength': 4095, 'semiMajorAxisOrientation': 3601},
|
||||
'altitude': {'altitudeValue': 800001, 'altitudeConfidence': 'unavailable'}}},
|
||||
'highFrequencyContainer': ('rsuContainerHighFrequency', {})}}}
|
||||
sdu = self.codec.cam_type.encode(cam)
|
||||
request = m.BtpDataRequest(sdu, btp_type=1, destination_port=2001, destination_port_info=0,
|
||||
gn_packet_transport_type=m.TransportType.SHB, its_aid=36, permissions=b'')
|
||||
else:
|
||||
if not self.codec.denm_type:
|
||||
return 2
|
||||
denm = {'header': {'protocolVersion': 2, 'messageId': 1, 'stationId': 42},
|
||||
'denm': {'management': {
|
||||
'actionId': {'originatingStationId': 42, 'sequenceNumber': sequence},
|
||||
'detectionTime': self.time_ms, 'referenceTime': self.time_ms,
|
||||
'eventPosition': {'latitude': 520000000, 'longitude': 130000000,
|
||||
'positionConfidenceEllipse': {'semiMajorConfidence': 4095, 'semiMinorConfidence': 4095, 'semiMajorOrientation': 3601},
|
||||
'altitude': {'altitudeValue': 800001, 'altitudeConfidence': 'unavailable'}},
|
||||
'validityDuration': 600, 'stationType': 5}}}
|
||||
sdu = self.codec.denm_type.encode(denm)
|
||||
request = m.BtpDataRequest(sdu, btp_type=1, destination_port=2002, destination_port_info=0,
|
||||
gn_packet_transport_type=m.TransportType.GBC, its_aid=37, permissions=b'',
|
||||
area=m.DestinationArea(0, 520000000, 130000000, 500, 0, 0))
|
||||
return self.btp(request)
|
||||
|
||||
def btp(self, request):
|
||||
try:
|
||||
self.client.btp_data_request(request)
|
||||
return 0
|
||||
except LinkError as error:
|
||||
return int(error.result.code)
|
||||
|
||||
# ---- the diagnostic protocol ----
|
||||
def execute(self, command: bytes) -> bytes:
|
||||
with self.lock:
|
||||
return self._execute(command)
|
||||
|
||||
def _execute(self, c: bytes) -> bytes:
|
||||
result = 1
|
||||
try:
|
||||
if len(c) == 1 and c[0] == 0:
|
||||
result = self.reset()
|
||||
elif len(c) > 1 and c[0] == 9:
|
||||
self.bundle = bytes(c[1:])
|
||||
self.secured = True
|
||||
self.provisioned = False # applied by the next reset, as hil_sut.cpp does
|
||||
result = 0
|
||||
elif not self.configured:
|
||||
result = RESULT_REJECTED
|
||||
elif len(c) >= 6 and c[0] == 1 and c[1] <= 1:
|
||||
port, info = struct.unpack('>HH', c[2:6])
|
||||
request = m.BtpDataRequest(bytes(c[6:]), btp_type=c[1], destination_port=port, destination_port_info=info,
|
||||
its_aid=self.args.its_aid, permissions=b'')
|
||||
result = self.btp(request)
|
||||
elif len(c) > 13 and c[0] == 2:
|
||||
result = self.test.inject(bytes(c[1:7]), bytes(c[7:13]), bytes(c[13:]))
|
||||
elif len(c) >= 2 and c[0] == 3:
|
||||
result = self.test.gn_request(c[1], bytes(c[2:]))
|
||||
elif len(c) == 9 and c[0] == 4:
|
||||
lat, lon = struct.unpack('>ii', c[1:9])
|
||||
self.latitude, self.longitude = lat / 1e7, lon / 1e7
|
||||
self.poti()
|
||||
result = 0
|
||||
elif len(c) == 9 and c[0] == 5:
|
||||
microseconds = struct.unpack('>Q', c[1:9])[0]
|
||||
self.time_ms = microseconds // 1000
|
||||
self.poti()
|
||||
result = 0
|
||||
for kind in (0, 1):
|
||||
if self.carrier_pending[kind] is not None:
|
||||
sequence = self.carrier_pending[kind]
|
||||
self.carrier_pending[kind] = None
|
||||
result = self.carrier(kind, sequence)
|
||||
elif self.carrier_interval[kind] and self.time_ms >= self.carrier_next[kind]:
|
||||
self.carrier_next[kind] = self.time_ms + self.carrier_interval[kind]
|
||||
if kind == 1:
|
||||
self.denm_sequence += 1
|
||||
result = self.carrier(kind, self.denm_sequence if kind == 1 else 0)
|
||||
elif len(c) in (2, 4) and c[0] in (6, 8) and c[1] < 2:
|
||||
sequence = struct.unpack('>H', c[2:4])[0] if len(c) == 4 else 0
|
||||
if c[0] == 6:
|
||||
result = self.carrier(c[1], sequence)
|
||||
else:
|
||||
self.carrier_pending[c[1]] = sequence
|
||||
result = 0
|
||||
elif len(c) == 4 and c[0] == 7 and c[1] < 2:
|
||||
self.carrier_interval[c[1]] = struct.unpack('>H', c[2:4])[0]
|
||||
self.carrier_next[c[1]] = self.time_ms
|
||||
result = 0
|
||||
elif c and c[0] > 9:
|
||||
result = 2
|
||||
except LinkError as error:
|
||||
result = int(error.result.code)
|
||||
except TimeoutError as error:
|
||||
log('timeout: %s' % error)
|
||||
result = RESULT_REJECTED
|
||||
records = []
|
||||
if self.configured:
|
||||
try:
|
||||
overflow, records = self.test.drain()
|
||||
if overflow:
|
||||
result = 5
|
||||
except Exception as error:
|
||||
log('drain failed: %s' % error)
|
||||
out = bytes([result & 0xFF, len(records)])
|
||||
for kind, data in records:
|
||||
out += struct.pack('>BH', kind, len(data)) + data
|
||||
return out
|
||||
|
||||
def serve_stdio(self):
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
print(self.execute(bytes.fromhex(line)).hex(), flush=True)
|
||||
|
||||
def serve_tcp(self, host, port):
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind((host, port))
|
||||
server.listen(1)
|
||||
log('listening on %s:%d' % (host, port))
|
||||
while True:
|
||||
connection, peer = server.accept()
|
||||
log('relay connected from %s:%d' % peer)
|
||||
with connection, connection.makefile('rb') as reader, connection.makefile('wb') as writer:
|
||||
for line in reader:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
writer.write(self.execute(bytes.fromhex(line.decode())).hex().encode() + b'\n')
|
||||
writer.flush()
|
||||
log('relay disconnected')
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.test.mirror(0)
|
||||
except Exception:
|
||||
pass
|
||||
self.client.close()
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument('--port', required=True)
|
||||
p.add_argument('--bundle', help='VCR1 credential bundle: secured profile with these credentials')
|
||||
p.add_argument('--anonymous', action='store_true', help='itsGnLocalAddrConfMethod ANONYMOUS')
|
||||
p.add_argument('--station-type', type=int)
|
||||
p.add_argument('--its-aid', type=int, default=36, help='ITS-AID for command 1 (BTP-DATA.request) in a secured profile')
|
||||
p.add_argument('--listen', help='host:port to serve the hex-line protocol over TCP instead of stdin/stdout')
|
||||
p.add_argument('--asn1')
|
||||
p.add_argument('--timeout', type=float, default=5.0)
|
||||
p.add_argument('--verbose', action='store_true')
|
||||
args = p.parse_args()
|
||||
bridge = Bridge(args)
|
||||
try:
|
||||
if args.listen:
|
||||
host, port = args.listen.rsplit(':', 1)
|
||||
bridge.serve_tcp(host, int(port))
|
||||
else:
|
||||
bridge.serve_stdio()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
bridge.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""stdin/stdout <-> TCP relay: the SUT executable an ETSI adapter forks inside WSL, forwarding
|
||||
the hex-line protocol to sut_bridge.py --listen on the Windows side (which owns the COM port).
|
||||
|
||||
run_etsi.py ... --sut /usr/bin/python3 --sut-args "/root/sut_tcp_relay.py --connect 172.x.x.x:7788"
|
||||
"""
|
||||
import argparse
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--connect', required=True, help='host:port of sut_bridge.py --listen')
|
||||
args = p.parse_args()
|
||||
host, port = args.connect.rsplit(':', 1)
|
||||
with socket.create_connection((host, int(port))) as connection:
|
||||
reader = connection.makefile('rb')
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
connection.sendall(line.encode() + b'\n')
|
||||
reply = reader.readline()
|
||||
if not reply:
|
||||
sys.exit(2)
|
||||
sys.stdout.write(reply.decode().strip() + '\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user