Commit Graph
35 Commits
Author SHA1 Message Date
Ashin Walpola 75d6d3b85c Receive signed ITS messages and forward each at its declared length
Signed packets. A GeoNetworking Basic Header NextHeader of 2 means a
TS 103 097 (IEEE 1609.2) envelope follows, with the Common Header
inside it. gn_unwrap_its rejected all of these, and most real traffic is
signed: the 2026-08-17 capture holds 157 signed frames from 15 source
MACs against 2 unsecured stations. It now opens a COER-encoded
signedData, or a bare unsecuredData, and parses the inner packet as
before. The inner packet comes first inside tbsData, so the certificate
and signature are never parsed, and the signature is not verified - the
firmware has no trust store. Such messages reach the phone with the new
V2X_RX flags bit1, signed but not verified. The app reads only bit0 and
is unaffected until it learns the flag. Encrypted payloads, nested
signing and the legacy v1.2.1 envelope are still rejected. All 157
recorded signed frames have the layout this reads, in all three COER
length forms, and asn1tools decodes every envelope to the same inner
packet.

Payload bounds. Every frame recorded through the ESP32-C5's promiscuous
RX, about 15 000 of them, ends in 8 bytes that are not part of the
802.11 frame and not a valid FCS. obu-firmware reads frames through the
same API and took the rest of the frame as the message, so it forwarded
those 8 bytes to the phone after every message. UPER decoders stop where
the message ends, so nothing visibly broke, but the bytes cost serial
bandwidth and 8 bytes of the DENM's headroom, and they stayed attached
wherever raw payloads were stored or passed on. The payload is now
exactly what the Common Header's payload-length field declares, which is
also what separates a signed message from its signature.

A frame longer than main.c's 800-byte capture buffer is now reported as
truncated instead of being forwarded cut off, and counted as an oversize
drop through the new serial_link_note_oversize_drop, as it was when the
cut-off frame failed serial_link's size check.

Host tests in obu-firmware/test/host build the firmware sources
unmodified with MSYS2 gcc; `make` runs all three.
- test_chain: frames from the firmware's TX code checked byte by byte
  against EN 302 636-4-1 and parsed back, including hand-built signed
  frames, the payload-length rule, the RX trailer, and every truncation
  length against a no-access guard page. 1731 checks, 0 failures.
- test_replay and check_replay.py: all 15 145 recorded frames through
  gn_unwrap_its, cut to 800 bytes as on the board, and re-derived
  independently in Python with the envelope decoded by asn1tools. They
  agree on every record; 15 131 accepted, 157 of them signed. 11 043 of
  the 11 106 distinct messages re-encode byte-identically. The other 63
  fail the same way with the old 8 bytes put back, so the boundary is
  not the cause: 5 are our own CAMs from before the 2026-08-20
  yawRateConfidence fix, and the rest, from other stations, are a
  follow-up in TODO.md.
- fuzz_gn_unwrap: random edits of every recorded frame, each run against
  the guard page. 50 000 000 iterations, no crash.

obu-firmware/test/pcap_gn_tally.py tallies GeoNetworking header fields
per station over captures; it is how the other stations' lifetimes were
measured. TODO.md collects what is still open, including the on-air
check for this change: it builds on IDF 6.1 but has not been flashed.
2026-09-11 20:19:40 +02:00
Ashin Walpola 1baae2c5f6 Encode yawRateConfidence in 4 bits in the firmware CAM encoders
YawRateConfidence has nine enumerands, degSec-000-01(0) to
unavailable(8) (cdd_1_3_1_1.asn), so UPER needs 4 bits and
"unavailable" is 8. Both firmware copies of cam.c wrote 3 bits with
value 7, which is also the wrong symbol (outOfRange), and every field
after it shifted by one bit. The app's CamUperCodec fixed the same line
on 2026-08-20; these two copies were missed.

obu-cam-transmistter compiles its cam.c, so a board running that bench
beacon sent CAMs no standards-compliant station could decode.
obu-firmware's copy is reference only - it is not in SRCS, since the
phone encodes the CAM - and is kept in step because the app's encoder
was ported from it. The production OBU runs obu-firmware and was never
affected.

Every other field width was compared against CamUperCodec.kt and
matches. Checked with asn1tools against asn1/cam_1_4_1.asn and
cdd_1_3_1_1.asn: the CAM both fixed copies emit decodes with every
expected value and re-encodes byte-identically, while the version before
this change fails on yawRateConfidence. obu-cam-transmistter builds on
IDF 5.5.4.
2026-09-11 20:19:40 +02:00
Ashin Walpola 8871708a98 Send the GeoNetworking lifetime as 1 s, not 3200 s
geonet_wrap_shb wrote lifetime 0x83, commented as about 60 s. The field
holds the multiplier in its upper six bits and the base in the lower two
(50 ms, 1 s, 10 s, 100 s), so 0x83 is 32 x 100 s = 3200 s. That is over
the 600 s itsGnMaxPacketLifetime a sender may use at all; vanetza
refuses to send such a packet. The byte arrived with the Phase 03 commit
as a placeholder and was never checked against the encoding.

For a single-hop CAM this is non-compliance rather than a functional
fault: nothing stores or forwards an SHB packet, so no receiver acts on
the value, and no dropped CAM was ever traced to it.

0x05 (1 x 1 s) is what every other station in
its-g5-receiver-firmware/recordings sends its CAMs with; the recorded
GeoBroadcast DENMs use 0x79 (30 s). Changed in obu-cam-transmistter's
copy as well. Both firmwares build (IDF 6.1 and 5.5.4), and the
disassembled geonet_wrap_shb of each stores 0x05. Not yet seen on air:
the production OBU still runs the 2026-09-10 build, and the on-air
check is listed in TODO.md.
2026-09-11 20:19:39 +02:00
Ashin Walpola d7043bb04d Ignore the vanetza checkout and Python bytecode caches
vanetza is a clone of the open-source ETSI C-ITS stack, kept beside the
project as a reference in the same way as C-ITS-Parser. It is read, not
built, and it carries its own .git, so a plain `git add .` would have
picked it up as an embedded repository.

__pycache__/ appears when obu-firmware/test/host/check_replay.py is
imported rather than run.
2026-09-11 20:19:39 +02:00
Ashin Walpola 83153a0971 Send each CAM with its position vector, a rotating pseudonym and GNSS time
The app side of the firmware's CAM_TX_PV message. Until now the phone
handed the ESP32 bare CAM bytes, so the GeoNetworking header around them
could only carry the firmware's bench placeholders.

GnPositionVector.fromCam builds the Source Position Vector from the same
Cam the UPER is encoded from, so the two layers cannot disagree about
where the rider is. Position is rounded exactly as CamUperCodec rounds
it, heading wraps into 0..3599, and non-finite values become 0. PAI is
set when Android's horizontal accuracy is at most 24.7 m, the 40 m
itsGnPaiInterval/2 threshold converted from a 95% to a 68% confidence
radius. UsbSerialTransport.sendCamTx sends 0x05 once the heartbeat
advertises the capability and 0x01 otherwise, so this build still
transmits against older firmware, and logs which path it is on.

Pseudonyms. The station ID used to be created once per install and never
changed, under a MAC that never changed either, so every CAM this phone
ever sent was linkable to every other. PseudonymManager now owns the
station ID and the MAC as one identity and replaces both together every
10 minutes, or immediately if the clock goes backwards. Both are
persisted in a single edit, so a crash cannot leave them mismatched.
MACs are locally administered unicast and can never equal the bench
ping's. CamTransmitLoop takes the current pseudonym per CAM, and the two
most recently retired IDs still count as ours, so a frame sent just
before a rotation is not taken for a stranger.

GNSS time. On 2026-09-10 the bench phone's clock was 24 minutes fast:
with no SIM and no internet time it had no automatic time source, and
every CAM went out stamped in the future. GnssTimeSource moves transmit
timestamps onto SystemClock.currentGnssTimeClock() and falls back to the
wall clock without a fix, logging which one is in use and the measured
error. ItsTime is now the single rule for both the CAM's
generationDeltaTime and the GN TST. Receive paths stay on the wall clock
so everything they stamp remains comparable.

The bench pinger keeps its fixed station 999999 and a fixed MAC, so a
ping stays recognisable in a capture. 999999 now counts as ours only
while this phone's pinger runs and for 5 s after it stops. The previous
rule treated it as ours unconditionally, which hid another phone's pings
on the same bench.

Leap seconds are an open question, recorded in ItsTime: TimestampIts may
be TAI-based, which would put it 5 s higher. 85 tests, 0 failures.
2026-09-10 14:47:30 +02:00
Ashin Walpola 3eeccfb268 Send CAMs under the phone's position vector, not bench placeholders
Every field of the GeoNetworking Source Position Vector this firmware sent
was a compile-time constant: the bench coordinates, speed 0, heading 0,
TST 0, station type passengerCar and one fixed MAC. The CAM inside
described a moving cyclist while the GN header around it described a car
parked at the bench.

SERIAL_MSG_CAM_TX_PV (0x05) puts a 24-byte prefix ahead of the CAM UPER:
MAC, station type, PAI, TST, latitude, longitude, speed and heading, all
values the phone already has when it builds the CAM and none of which
this chip can know. geonet_wrap_shb now takes them as a gn_lpv_t, and
tx_radio_task hands the same MAC to dot11p_build_frame, so the 802.11
source address and the GN_ADDR MID stay one address across a pseudonym
change. Speed is clamped rather than masked, since an overflowing 15-bit
value flips its sign bit and reads as travelling backwards.

This reverses the Phase 03 decision that the firmware owns the
pseudonym. A pseudonym only protects anyone if the MAC, the GN_ADDR and
the CAM's stationID change together, and the phone owns the stationID.

The heartbeat gains a capability byte (payload[7], bit0 = CAM_TX_PV),
appended so an app reading the first 7 bytes is unaffected. The app sends
0x05 only once it sees that bit, so app and firmware can be updated in
either order. CAM_TX (0x01) is still handled and falls back to the bench
values, with the station type corrected to cyclist to match the CAM.

Verified on air from the COM10 test board, decoded independently by the
CiT One's gnHeader: 24 of 24 CAM_TX_PV frames matched the sent position
vector field by field, and so did the CAM station ID. The legacy path
delivered 23 of 24 frames with no field mismatches. Flashed on the COM3
OBU and its boot log is clean.

Also corrects the SERIAL_LINK_MAX_PAYLOAD comment, which still named the
400-byte receive capture buffer as the ceiling on the RX path. That
buffer is 800 bytes now, so the serial link is the ceiling, and larger
payloads are dropped and counted there.
2026-09-10 14:47:30 +02:00
Ashin Walpola 5ec3619cbe Stop retaining detected manoeuvres; the CAM rate bump is their only consumer
The detector runs to raise the CAM transmit rate through a manoeuvre. Nothing
else read its output once the UI was removed, so keeping the rows was storing
data with no reader on the chance it would one day be analysed.

Drops the detected_events table in schema v5, deletes DetectedEventEntity and
the DAO and repository methods behind it, removes the insertEvent call from the
recording service, and removes the per-event rows and their five columns
(event_type, confidence, peak_accel, peak_gyro, duration_ms) from the trip CSV
along with the events parameter threaded through buildTripCsv and shareTripCsv.
A detected manoeuvre now lives for the length of one onDetectedEvent call.

MIGRATION_1_2 still creates the table: a v1 install upgrades 1-2-3-4-5 and so
creates it before v5 drops it. Removing it from the earlier migration would
break that path for anyone who has not upgraded yet.

trips.eventCount is kept. Dropping a SQLite column means recreating the table
and copying every recorded ride across, which is real risk for one unused
integer; the service still writes an accurate count and the CSV header still
reports it. It is the only thing left about detected manoeuvres.

This closes off the route to the false-positive measurement that 11.3 flags as
missing, so 11.3 now says that outright rather than pointing at an export that
no longer carries the data. Docs 11.3/11.4, the user guide, the README and the
traceability matrix updated to match. 55 tests, 0 failures.
2026-09-08 16:29:02 +02:00
Ashin Walpola 1ad123a6f8 Make the event detector a CAM rate input, not a ride-stats readout
The detector's only live consumer is the CAM transmit-rate policy: every
emitted event calls CamTransmitLoop.onDetectedEvent, raising the beacon
rate from 1 Hz to the elevated rate for five seconds so nearby stations
get denser updates through a manoeuvre. Counting one's own braking events
is not a goal of this project, so the display is gone and the detector
stays: the live per-type counters and their notification text, the event
pins and detail sheet on the trip review map, and the event chip on the
history card. Events are still persisted and exported to CSV, which is
the only route to the tuning measurement section 11.3 says is missing.

Fix two defects found while documenting the detector.

TripRecordingService overrode nine of DetectionConfig's twelve parameters
in its constructor, so the tests validated the Phase A defaults while the
phone ran something materially less sensitive. The tuned values are now
the defaults and the override is deleted; the numbers moved location, not
value, so detector sensitivity is unchanged. EventDetectorTest now sets
only windowSize and the sustained-frame counts and inherits every signal
threshold, which cannot drift again. That was not a free change and makes
the same point from the other side: at the real thresholds the old stimuli
triggered nothing. Accel alternating 3.5/0.5 gives a std dev of 1.5 and
never clears 1.8, and the moderate-braking case used a 0.8 m/s drop that
never clears 1.0. Those stimuli are re-derived against the real values.

brakingHighConfidenceRate was documented as a rate but has always been
compared against the peak cumulative drop from the onset speed, which
grows with episode length, so HIGH was assigned more readily than the name
implied. Renamed to brakingHighConfidencePeakDrop rather than changing the
comparison: "lost more than 1.5 m/s in one episode" is coherent, whereas a
rate off a 1 Hz speed signal sampled at 50 Hz spikes on a near-zero
divisor early in an episode. Output is unchanged, so the existing
confidence assertions stay evidence instead of being re-baselined.

Docs 11.3/11.4 updated in place, including the correction of a claim that
detected events do not reach the V2X side; the rate-bump path already
existed when that was written. 55 tests, 0 failures.
2026-09-08 16:20:14 +02:00
Ashin Walpola 83ccf335bb Document the event detector's specification and trigger conditions
Section 11 described how the detector works and the test-design finding from
2026-08-25, but carried no threshold values, no trigger conditions and no
emission semantics. That was inconsistent with section 10.4, which tabulates
all nineteen UseCaseDetectionConfig parameters for the V2X side. Adds 11.3 and
11.4 to close the gap.

11.3 tabulates all twelve DetectionConfig parameters in three columns, because
three different configurations exist and they do not agree. DetectionConfig's
KDoc says its defaults match the Phase A specification; TripRecordingService
overrides nine of the twelve when it constructs the detector, every one of them
in the direction of lower sensitivity. The shipping detector is not the
specified detector, and that was recorded nowhere outside a constructor.

11.4 gives the input rates, the qualifying condition for each of the three
event types, when each emits, and how confidence is assigned. It also explains
why braking compares against a reference speed latched at onset rather than a
per-frame delta: GNSS updates at 1 Hz against a 50 Hz detector, so a per-frame
delta is non-zero on one frame in fifty and could never coincide with a
25-frame sustain requirement. That is the same sampling lag that made four
tests unsatisfiable, seen from the implementation side.

Two discrepancies found while writing this are recorded rather than fixed,
since fixing either changes behaviour and belongs in its own change:

- EventDetectorTest states it keeps production thresholds for all signal
  values. The values it keeps are the DetectionConfig defaults, not the ones
  TripRecordingService runs. All 18 tests validate a configuration that never
  executes on a phone. The logic under test is shared, so they remain valid
  logic tests; they are not evidence about the shipped system.
- brakingHighConfidenceRate is documented as a rate in m/s per GNSS update but
  is compared against the peak cumulative drop from the onset reference, which
  is not a rate and grows with episode length. HIGH confidence is therefore
  assigned more readily than the name implies.

Also notes the emission asymmetry: turning and stopping emit once per episode,
braking re-arms and re-fires roughly every half second at the shipping values.

Edited in place through the existing package rather than regenerated, so Word's
own parts and the manual edits from 312f094 survive. All sixteen package parts
verified present afterwards, section order unchanged, all nine image
placeholders intact.
2026-09-07 16:42:58 +02:00
Ashin Walpola 034ef22336 Decode raw v2x/rx on the CiT One path, and stop tracking our own CAM pings
The Use Case app's v2x-uca/output/json topics are a rate-limited and
lossy view: traffic the OBU's radio actually heard, the ESP32's CAM
pinger among it, never reached the app. The raw v2x/rx topics carry
everything, as RecvV2XMessage protobuf with the ITS-G5 PDU in one bytes
field (CI-CiT MQTT API section 2.4).

RecvV2xMessage is a minimal protobuf wire-format reader for the three
fields needed: btpHeader type and destination port, the GeoNetworking
destination-area radius, and the payload. Hand-written for the same
reason the ASN.1 codecs are, rather than adding protoc and the protobuf
Gradle plugin and vendoring a third-party .proto into this repository.
Field numbers are pinned by a byte fixture written out by hand from the
encoding rules, not generated by our own encoder.

Raw payloads now travel as bytes rather than String. The previous UTF-8
round trip replaced every byte that is not valid UTF-8, leaving a
payload that still looked plausible in a log and decoded to nothing.

CAM, DENM and SPATEM from both transports now meet in shared handlers,
so everything downstream is transport-agnostic. SPATEM works on the CiT
One path for the first time, and DENM gains its relevance radius there.
Where both sources describe the same event the decoded one wins: remote
CAMs from the processed topic are suppressed while the raw topic is
live, and DENMs dedup on ETSI's actionID with the decoded list last.
The processed topics stay subscribed as a fallback for an OBU whose
configuration does not publish the raw ones.

Two defects found while testing this:

CamPinger transmits under a fixed bench station id, deliberately
distinct from the persisted one, but the self-heard filter only knew
the persisted id. Every ping therefore came back through the ESP32's
promiscuous receive as a remote road user sitting exactly on top of the
ego position, moving at the ego's own speed and heading, and was handed
to the detection engine as a collision partner for itself. The rule now
lives in OwnStationIds, covers both ids, and has tests, so a third
transmit path cannot reintroduce the same gap quietly.

Self-heard frames are now counted and reported on the pinger card
instead of being discarded. That round trip is the only direct evidence
the serial link, the ESP32's transmit path and its receive path all
work, which is what the bench pinger exists to demonstrate.

Also: the stationType warning banner no longer shows in ESP32-C5 mode.
It reads a value from the CiT One's obu_gnss topic, which that hardware
never publishes, so it stayed on screen reporting on an OBU that was no
longer in use.
2026-09-02 15:25:31 +02:00
Ashin Walpola ebe1c9edfd Dashboard: surface the nearest hazard and the next signal change
Two cards below the status cards, each shown only when there is
something to show and each opening the V2X screen when tapped.

Hazard: cause name, distance, and a count of the others behind it,
ranked closest first. A hazard whose distance cannot be resolved,
because there is no fix yet, sorts last rather than being dropped.

Traffic light: the intersection changing soonest, its leading phase
with a countdown, and every signal group as a colour-coded chip. The
countdown runs on its own 500 ms clock rather than on SPATEM arrivals,
so it cannot freeze mid-count and keep claiming a light is about to
change after the RSU stops transmitting.

Signals are ranked by time-to-change rather than by distance because
SPATEM carries no position at all. Placing an intersection needs MAPEM
geometry, which nothing on air is currently sending.

Also fixes bottom-nav taps. One rule now applies to every tab: a tap
lands on that tab's own screen, popping back to it when it is still on
the stack so the gesture behaves exactly like Back or a back swipe.
saveState/restoreState are gone, since on this flat graph a restored
back stack brought back the sub-screen the rider was on instead of the
tab root, which is the opposite of what the tap asked for. Tabs also
now stay lit on the screens that belong to them.
2026-09-02 15:25:05 +02:00
Ashin Walpola 312f094909 Save the Word-edited copies of both documents
Both files were opened and edited in Word and are re-saved here as the
authoritative versions. They supersede the generated originals from 16998bf.

The files grew by roughly 9 to 11 KB, which is Word repackaging them: its own
settings part, embedded font references and per-run revision identifiers, none
of which the generator wrote. The technical document also lost two empty
paragraphs, which is Word trimming trailing empties.

Extracted text is byte-for-byte unchanged in the user guide and identical in
word count in the technical document, and both still carry their full section
structure and all twenty image placeholders. Neither file contains an embedded
image yet, so the placeholders are all still waiting on artwork.

Word is now the source of truth for these two files. There is no longer a
script that can regenerate them without discarding whatever was changed here,
so edit them in Word rather than rebuilding.
2026-09-01 15:10:09 +02:00
Ashin Walpola b6a687982d Update the README for the ESP32-C5 path and the two documents
The README still described the project as it was before Phase 03. Several
claims had become actively wrong rather than merely dated, and the last one
is the kind a reviewer would catch:

- "no ASN.1 encoding in the app" - the app hand-encodes and decodes CAM,
  DENM and SPATEM bit by bit on the ESP32-C5 path. domain/asn1/ is now the
  highest-risk code in the project, and the README denied it existed.
- The phase table listed Phase 03 as Bluetooth BLE. Phase 03 is the
  ESP32-C5; Bluetooth is not implemented and the requirements leave it open.
- "communicates with the OBU exclusively via the consider it MQTT API v6"
  is true of one of the two hardware paths.
- The feature list claimed MAP and CPM display. There is no MAPEM decoder
  and nothing in the tree supports CPM, so both claims are dropped rather
  than carried forward.
- DENM transmission was listed as a headline feature with no indication that
  it is a manual antenna and range test tool, CiT One only, and deliberately
  never triggered by a detected event or a use case alert. That decoupling is
  the project's central architectural rule and the README implied the
  opposite.
- The architecture tree predated domain/asn1, domain/usecase, domain/cam,
  data/cam, the serial transport, obu-firmware/ and asn1/.

Added: the project goal, the two hardware paths and the point where they
converge, a verification section, a documentation index, and a status
section.

The convergence point is worth stating in the README rather than only in the
technical document, because it is what makes the second OBU a drop-in rather
than a fork: both paths normalise into the domain Cam type at
CamUseCaseRepository, and everything above it is shared and
transport-agnostic.

The status section says plainly that requirement 11.6 is not met, that
messages are not signed, and that the detection thresholds are untuned
estimates. Someone arriving at this repository should learn that from the
front page rather than from page forty of a Word document.
2026-09-01 15:07:57 +02:00
Ashin Walpola 3da60d3a99 Ignore Office lock files
Word creates ~$<name>.docx beside a document while it is open and removes it on
close. docs/~$crOBU-User-Guide.docx was showing as untracked while the user guide
was being edited, which is exactly the kind of transient file a git add -A sweeps
in by accident.
2026-08-26 17:13:50 +02:00
Ashin Walpola 081347f31b Tapping the active bottom-nav tab returns to that tab's root screen
Tapping Settings while on Settings > Connection appeared to do nothing. The tab
navigated to its own route, but restoreState = true then restored that tab's
saved back stack, putting the sub-screen straight back on top. The only way out
was the back button or a back swipe.

When the tap targets the tab already in use and the current destination is deeper
inside it, pop back to the tab's own screen instead of navigating. Only entries
above the tab root are removed, so Back and back-swipe behave exactly as before -
both routes out of a sub-screen now work.

The tab also stayed unhighlighted while any sub-screen was open, because selected
compared the current route for equality with the tab's route. Ownership is now
derived from the existing route naming convention, so settings/connection belongs
to Settings and trip_review/{tripId} belongs to Trips. A new settings/* screen is
picked up automatically; a sub-screen named outside its tab's prefix would need a
line in ownsRoute.
2026-08-26 17:12:46 +02:00
Ashin Walpola 16998bf478 Add the user guide and technical documentation as Word documents
Two documents, wiki-style so they import cleanly: short titled sections,
tables over prose where the content is comparative, and cross-references
between sections rather than a narrative that has to be read start to end.

MicrOBU-User-Guide.docx is for riders. Features, setup for both hardware
paths, what each screen shows, what the five alerts mean in plain language,
troubleshooting. No ASN.1, no BTP ports, no bit widths anywhere in it. The
alert descriptions and the link states are taken from values/strings.xml so
the guide and the interface use the same words.

MicrOBU-Technical-Documentation.docx is for supervisors and stakeholders.
Architecture, the message path end to end, the serial protocol, the codecs
and how they are verified, the full requirements matrix, the bench results,
and the decisions. Section 15 is fourteen decisions written as chosen /
alternative / reasoning / cost accepted, because the alternative is the part
a supervisor asks about and it was previously recorded only in commit
messages.

Nothing is re-derived. Every measurement is cited from
05-obu-bench-test-2026-08-25.md, 04-transmit-setup.md, asn1/README.md or
the commit history. Where a claim has no evidence it is marked as unverified
rather than asserted:

- 11.6 Phase A success criteria is stated as not met, in its own subsection.
  The bench proves reception; it cannot prove the use case with two moving
  stations because nothing on the bench moves. This is the central claim of
  the project and it needs a real ride.
- The tx_custom.c bypass is described as the least defensible component in
  the system and load bearing, with the reverse-engineered struct layouts
  and the skipped sanity checking spelled out.
- The 5900 MHz transmit story is marked a mitigation for a hypothesis, not a
  diagnosis, and the isolation test that would settle it is named as not run.
- The detection thresholds are presented as untuned engineering estimates in
  both documents, since presenting them as validated is the easiest and most
  damaging overstatement available here.

Twenty image placeholders, none of them filled. Each is a shaded block
carrying a caption and a "Must show" line. For the three screenshots that
exist the line names the bench session and section they came from; for the
four diagrams that do not exist yet it is a full drawing spec, so the two
hardware paths, the message path, the frame layout and the verification loop
can be drawn from the document without re-reading the source.

references.bib collects the standards as BibTeX for a later publication:
EN 302 637-2/3, TS 103 301, SAE J2735, TS 102 894-2, EN 302 636-4-1 and
-5-1, TS 103 248, TS 103 097, IEEE 802.11 OCB, plus the C2C-CC white paper
and the vendored parser provenance.

Also tracks two documents the new ones cite that had never been committed:
docs/01-requirements-traceability.md and 04-transmit-setup.md. Section 18.3
lists them as repository sources, which would have been a dangling reference
otherwise.

Not included, deliberately: the two consider it PDFs cited in section 18.2.
They are third-party vendor documentation and redistributing them is a
licensing decision, not a documentation one.

Still missing: the German user guide. values-de/strings.xml already fixes the
terminology for it.
2026-08-26 16:03:11 +02:00
Ashin Walpola cc35994e68 Fix four EventDetectorTest cases that never passed
These have been red since the initial commit. All four failed for the same
reason, and in every case the test was wrong rather than the detector.

EventDetector requires two conditions to hold on the SAME frame, and one of them
is a rolling-window statistic that takes time to respond. The tests ignored that
lag, so they described situations the detector cannot see - and could not have
seen at any point in its history.

Braking (3 tests). Each filled the accel window with a CONSTANT value, then
stepped the GPS speed down. The speed drop is therefore true on exactly one
frame, and on that frame the accel std dev is still ~0.75 against a 1.2
threshold, because the window is full of the constant. By the time the window
recovers (frame 4), prevSpeedMps has caught up and the drop is 0. The two
conditions never coincide and no BRAKING is possible. Constant accelerometer
output right up to the instant of a brake is not physical either: the IMU is
sampled continuously while GPS speed lags, so the shaking precedes the reported
drop. The tests now establish that variability during the cruise phase.

Stopping. The second episode ran for stoppingFrames + 5. But stopping requires
the accel std dev to be BELOW a threshold, and the window still held the five
moving samples from the acceleration burst between episodes. Those take 8 frames
to drain far enough for the std dev to fall under 0.15, leaving 17 of the 21
frames the event needs. The first episode is unaffected because the window starts
empty. The episode is now long enough to cover the settling time.

Verified by simulating the detector's exact arithmetic against both the old and
new inputs before touching the file: the old profiles produce no events, the new
ones produce BRAKING/HIGH, BRAKING/HIGH, BRAKING/MEDIUM and two STOPPING.

No production code changed - the detector behaves consistently and defensibly.
Assertions are unchanged; only the stimulus is now something a bicycle could
actually produce. Suite is 41 tests, 0 failures.
2026-08-25 15:21:34 +02:00
Ashin Walpola 04b0076b8b Move the RX capture buffer off the WiFi driver's callback stack
rx_item_t is ~800 bytes at RX_FRAME_MAX_LEN, and wifi_promisc_rx_cb declared one
as a local. That callback runs on the WiFi driver's own task, already several
frames deep in the driver's call chain, on a stack of roughly 3.5 KB
(CONFIG_ESP_WIFI_TASK_STACK_SIZE, left at its default). Putting a fifth of that
stack into a single local is a stack-overflow risk that only appears under real
traffic - in front of an RSU rather than on the bench - and would present as a
random panic rather than anything pointing at its cause.

Both instances are now static: one in the callback, one in rx_forward_task. Safe
because each is touched by exactly one task, so there is no re-entrancy to guard
against; the same reasoning serial_link.c already uses for its static send
buffers. xQueueSend copies the struct out before returning, so reusing the
callback's buffer on the next frame is fine.

Firmware-only, no protocol change, so it does not require a matching app install.

Re-verified against live traffic after flashing: 1094 frames over 125 s with zero
decode failures, USB errors, detaches, crashes or mutex timeouts. SPATEM capture
rose from 3.20/s to 3.98/s against a theoretical maximum of 4.00/s, which is the
direction relieving stack pressure would produce, though RF geometry moves
between runs and this is not proof.

Report updated with T9, the accepted 512-byte ceiling, and the decision to drop
Phase B: the intersection use case is CAM-driven and needs none of it.
2026-08-25 15:13:23 +02:00
Ashin Walpola d0701ccea4 Show roadside units in the station list; log ESP32 drop counters
Bench test on 2026-08-25 against live RSU and CiT One traffic found that 611 RSU
CAMs decoded correctly and none of them were ever displayed. Excluding RSUs from
UseCaseDetectionEngine - correct, since a permanently stationary station at a
fixed point trips the stopped-vehicle use case for as long as it is in range -
also removed them from the map and station list, because remotePositions is the
engine's own map.

RSUs are now tracked in a separate rsuStations flow and merged with the engine's
road users for display only. Expiry is clock-driven for the same reason as
hazards and signals: an RSU going out of range simply stops transmitting, and no
further emission would arrive to recompute the list. Cleared on link-down
alongside engine.reset(), so a stale RSU cannot outlive an unplug.

The kinematics line is suppressed for them. An RSU's CAM uses
rsuContainerHighFrequency, which carries no kinematics at all, so the zeroes in
the model are placeholders - printing "0.0 km/h - heading 0" would assert a
stationary vehicle pointing due north.

Also logs the ESP32's STATUS heartbeat counters whenever one changes. They
previously reached only the CAM Pinger card, so a bench run captured through
logcat had no record of whether the firmware dropped anything. Logged on change
rather than per beat: the interesting event is a drop appearing, and a
once-per-second line would bury it.

Test report in 05-obu-bench-test-2026-08-25.md.
2026-08-25 14:37:46 +02:00
Ashin Walpola b2b57fa39e Vendor the ASN.1 modules the codecs are verified against; untrack IDE churn
asn1/
Three tests assert exact bytes - CamEncodeGoldenTest, DenmAirReceiveTest and
SpatemUperCodecTest - and their expected values came from asn1tools compiled
against ETSI modules that existed only as an untracked working copy on one
machine. A golden-byte fixture nobody else can regenerate is a fixture nobody
can safely touch, so the modules are now in the repo.

Only the seven .asn files those tests need are copied, 576 KB of a 4.2 MB
checkout; the upstream Rust parser is not used by this project at all. Verified
sufficient in isolation: copied into an empty directory, all three specs compile
and reproduce the committed golden CAM bytes byte-identically.

Source is consider it GmbH's C-ITS-Parser (github.com/consider-it/C-ITS-Parser)
at f457426, MIT licensed - LICENSE is retained alongside as that requires. The
schemas themselves are ETSI's standard definitions; upstream's contribution is
assembling them into a compilable set. asn1/README.md records the provenance,
which module pairs with which message, and the rule that matters: never
regenerate a golden fixture from this project's own encoder, because sharing a
mistake between encoder and decoder is exactly the failure these files exist to
catch.

Doc references in the codecs and tests now point at asn1/ instead of the
untracked checkout, and C-ITS-Parser/ is gitignored so the working copy beside
the project is never picked up.

Untracked local state
- .idea/deploymentTargetSelector.xml rewrites itself on every deploy, so it has
  been showing as modified in essentially every commit. Along with
  deviceManager.xml, appInsightsSettings.xml and studiobot.xml it is per-machine
  state, not project configuration.
- obu-firmware/sdkconfig.old is ESP-IDF build output - it is the previous
  sdkconfig, rewritten on every build. sdkconfig.defaults remains tracked, since
  that is the configuration actually chosen.

All five stay on disk; only the tracking is removed. Also ignores
.claude/settings.local.json, which is per-machine, while leaving the skills
beside it committable as project knowledge.
2026-08-21 14:28:57 +02:00
Ashin Walpola eb6150260b Fix NPE crash on the V2X map when messages arrive during teardown
osmdroid's MapView.onDetach() permanently tears the view down: afterwards its
MapViewRepository holds a null MapView, so constructing a Marker against it
throws NullPointerException from inside InfoWindow's constructor.

It was being called from a DisposableEffect keyed on the lifecycle owner, which
disposes independently of the AndroidView that owns the map. The update block
could therefore still run against an already-detached MapView and rebuild its
markers:

  java.lang.NullPointerException: Attempt to invoke virtual method
    'MapViewRepository MapView.getRepository()' on a null object reference
      at org.osmdroid.views.overlay.Marker.<init>(Marker.java:116)
      at V2xLiveMapViewKt...(V2xLiveMapView.kt:119)

The crash is dated 2026-08-17 18:19, ten minutes after DENM reception went live
on the device. The defect was always there, but every incoming message
recomposes this view, so going from occasional updates to one per second made
the window easy to land in - and SPATEM at ~2 Hz makes it easier still.

Moves the teardown to AndroidView's onRelease, which is the callback that means
"this View is gone" and after which Compose guarantees no further update.
2026-08-20 16:31:41 +02:00
Ashin Walpola a5ad3dcc5d SPATEM receive, RSU CAM decode, and two ASN.1 encoding fixes
SPATEM over the air
- gn_unwrap.c accepts BTP-B port 2004 alongside 2001/2002. The serial protocol
  already carries the port in its V2X_RX prefix, so nothing else changed there.
  Note the crossover that makes this easy to get wrong: SPATEM is port 2004 but
  messageID 4, while MAPEM is port 2003 and messageID 5.
- SpatemUperCodec decodes SPAT down to per-signal-group phase and timing. The
  bit layout was validated by replaying 79,042 real SPATEMs - the whole
  2026-03-18 drive across 7+ RSUs plus the bench trigger - against asn1tools
  using the ETSI modules. All 79,042 matched on every field, none hit an
  unsupported branch. Two traps are pinned by tests: TimeChangeDetails is the
  one SEQUENCE here that is NOT extensible (5 optional bits, no extension bit),
  and maneuverAssistList cannot be skipped when present - it is variable-length,
  so it has to be walked to find where the next movement starts.
- The V2X list shows one row per intersection with each signal group coloured by
  phase and a countdown where the RSU supplies timing. TimeMark wraps hourly, so
  the countdown corrects for it; without that it reads hugely negative once an
  hour, precisely when someone is watching it.
- Entries expire after 15 s, much shorter than DENM's window: a traffic light
  that stopped updating is not "still green".

  Size caveat, deliberately deferred: SERIAL_LINK_MAX_PAYLOAD is still 512, so a
  SPATEM over ~498 bytes is counted as an oversize drop. The bench RSU sends 58
  bytes and is unaffected, but real road RSUs measured 555 median / 1243 max, so
  roughly 70% would not arrive. Raising the cap also requires enlarging
  RX_FRAME_MAX_LEN and moving rx_item_t off the WiFi driver's callback stack,
  where it would otherwise overflow.

RSU CAM decode
- HighFrequencyContainer is a CHOICE, and a roadside unit picks
  rsuContainerHighFrequency, which carries no kinematics at all. The decoder
  bailed on that branch, so every RSU CAM was dropped - including the bench RSU,
  which sends CAM and SPATEM from the same station id. It now decodes for
  position and stationType.
- RSU CAMs are kept out of UseCaseDetectionEngine. They arrive as a permanently
  stationary station at a fixed point, which is exactly the shape the
  stopped-vehicle and intersection-movement use cases match, and would raise a
  standing false alert for as long as the RSU was in range.

CAM transmit: yawRateConfidence
- YawRateConfidence has nine enumerands (0..8), so UPER needs 4 bits and
  "unavailable" is 8. The encoder wrote 3 bits with value 7 - one bit short and
  the wrong symbol - shifting every field after yawRate for any standards-strict
  receiver. The decoder read 3 bits too, so phone and ESP32 agreed with each
  other and with nothing else.
- This is the third instance of that exact failure mode in this project, after
  CurvatureCalculationMode and the GeoNetworking reserved bytes. A round-trip
  test through our own decoder structurally cannot catch it, so CamEncodeGolden
  Test asserts the bytes asn1tools produces instead: it decoded this encoder's
  output and re-encoded it byte-identically. Confirmed on air afterwards - 26 of
  our own CAMs captured back off the OBU's receiver, all 26 accepted, where the
  same decoder rejected them before.

DENM
- Hazards now expire 60 s after their last repetition. This needs a clock, not
  just a filter: both source flows only emit when a DENM arrives, so a sender
  that drives away or loses power would never trigger a recompute and its hazard
  would stay on screen indefinitely.
- The MQTT path was dropping every DENM for two independent reasons, both found
  by checking the payload against CI-CiT-MQTT_API_Documentation-v6 listing 2.6
  rather than guessing: the station id key is originatingStationId, and
  eventPosition IS a GeoJSON Point rather than an object containing one. Also
  parses termination (presence is the signal), sequenceNumber, stationType and
  the RFC3339 detectionTime. Note roadSideUnit is 15, not 12 - the enumeration
  has a gap after tram(11).

V2X screen
- The decoded CAM/DENM list now renders on the CiT One path too; it was gated to
  the ESP32-C5 path and CiT One fell through to the raw MQTT topic list. Those
  topics move to their own tab, hidden on the ESP32-C5 path where there is no
  broker.

Testing
- Adds org.json as a test-only dependency: the android.jar stub throws
  "not mocked" on every JSONObject call, which made the MQTT payload parsers
  untestable off-device.
- 23 V2X tests pass. EventDetectorTest's 4 failures are pre-existing and
  untouched by this change.
2026-08-20 15:47:14 +02:00
Ashin Walpola 0ccb867228 DENM over-the-air receive on the ESP32-C5 path
The firmware forwarded CAM only: gn_unwrap_cam accepted single-hop broadcast
(HT=5) and BTP port 2001, so every DENM was dropped before it reached the phone.
Real OBUs disseminate DENM by GeoBroadcast (HT=4), whose 44-byte extended header
also carries the hazard's relevance area - materially more useful on a map than
the sender's own position, since a sender may be relaying for someone else.

Firmware
- gn_unwrap_cam -> gn_unwrap_its: accepts GeoBroadcast alongside TSB/SHB, and
  BTP ports 2001 and 2002, extracting the GeoBroadcast destination area. Both
  extended-header lengths were measured against live air capture rather than
  read off a spec table. Secured packets (Basic Header NextHeader=2) are
  rejected rather than misparsed.
- SERIAL_MSG_CAM_RX (0x02) superseded by SERIAL_MSG_V2X_RX (0x04): a 14-byte
  prefix carrying BTP port, RSSI and the destination area. Adding MAPEM later
  needs a decoder on the phone but no protocol change. 0x02 stays reserved so
  the numbering is not silently reused.
- Promiscuous RX capture buffer 400 -> 800 bytes. A real GeoBroadcast DENM is
  around 500 bytes on air and was being truncated mid-payload, which no amount
  of correct unwrapping downstream could have recovered from.
- geonet_wrap_shb, both firmwares: the SHB extended header is 28 bytes, not 24.
  The Source Position Vector is followed by a 4-byte reserved field; without it
  a standards-strict receiver reads the CAM payload's first two bytes as the BTP
  destination port.

App
- DenmUperCodec: UPER decoder for the ManagementContainer and the
  SituationContainer's eventType. ValidityDuration is 17 bits, not 16, and
  ManagementContainer, SituationContainer and CauseCode each carry their own
  extension bit - a single wrong bit made a real frame read causeCode 47
  instead of 94.
- DenmEvent gains actionID (originatingStationID + sequenceNumber), stationType,
  termination, detectionTime, relevance radius and RSSI. Dedup keys on actionID
  where available, so a termination lands on the event it ends instead of
  creating a second pin.
- denmEvents merges the MQTT and over-the-air sources and drops terminated
  events. The V2X list view now shows hazards above the CAM stations; it
  previously took no DENM parameter at all, so hazards reached the map but never
  the list.
- DenmParser: the Use Case API sends causeCode as a string enum, so reading it
  as an Int always yielded null.

Testing
- DenmAirReceiveTest covers the V2X_RX prefix and the decoder using real frames
  from a live capture as fixtures. Expected values were cross-checked against
  the ETSI ASN.1 modules via asn1tools, which agreed on all 1885 decodable
  DENMs across the capture set, every field including detectionTime.
- Verified on hardware: a CiT One HLN-SV DENM decodes as cause 94/0 with a
  1000 m relevance radius at 1 Hz alongside CAM, with no decode failures and no
  unexpected BTP ports.

Also replaces em dashes with hyphens throughout the user-facing strings,
including the German translation.
2026-08-17 18:42:48 +02:00
Ashin Walpola f1770e11dd ESP32 link indicators, trip export with V2X + RSSI, trip/CSV cleanup
UI:
- Top bar shows a USB glyph reflecting the serial link on the ESP32-C5 path,
  instead of a Wi-Fi glyph driven by an MQTT state that is permanently
  disconnected there.
- Recording screen's OBU stream row follows whichever transport the selected
  hardware uses. It previously read as offline throughout a recording that was
  actively beaconing CAMs.
- Live map uses distinct markers: a centred dot for own position (a fact about
  the viewer, not a tracked object) and a teardrop pin for remote stations,
  tinted by alert severity. Both were osmdroid's identical default pin before,
  and severity required tapping a marker to read its label.
- Sensor Monitor moves out of the bottom nav to Settings > Developer. A live
  phone-sensor feed is a bench tool; the Dashboard already reports GNSS/IMU
  health. Screen and route are unchanged, just not in the rider's way.

Detection engine on the ESP32 path:
- Seed our own StationID from the persisted value CamTransmitLoop transmits.
  It was null here (the CiT One learns it from v2x/rx/obu_gnss, which doesn't
  exist on this path), so the self-heard-TX filter never fired: the ESP32 runs
  promiscuous for raw TX to work at all, hears our own CAMs back off the air,
  and they were tracked as a remote station - a ghost vehicle on top of the ego
  position, fed to the engine as a collision partner for itself.

RSSI:
- The firmware has always sent per-frame RSSI in byte 0 of every CAM_RX frame;
  the app discarded it. Now carried on Cam, persisted per V2X message, shown per
  station in the received-CAM list, and exported. Null for own CAMs and the
  whole CiT One path, neither of which has a measurement.

Trips, CSV and export (schema v3 -> v4):
- trips.sessionId links a trip to the CSV session recorded alongside it. The two
  are written by independent subsystems that the Recording button happens to
  start together; without the link, deleting a trip orphaned its CSV forever.
  Timestamp matching was rejected - close recordings would delete the wrong file.
- Deleting a trip now removes the CSV file AND its sessions row, so it stops
  appearing in the Session Log pointing at nothing.
- Per-trip combined CSV export: GPS track, detected events, V2X messages (with
  RSSI) and the raw sensor samples in one file, keyed by a leading type column.
  One file rather than a zip of tables because the point of the export is
  correlating those streams, and splitting them pushes the join downstream.
- Export takes the Activity context. Sharing from the ViewModel's Application
  context threw AndroidRuntimeException on startActivity - this crashed on the
  first tap of the share button.

Trips recorded before v4 have a null sessionId, so their exports omit raw sensor
rows and their CSVs still need clearing by hand once.
2026-08-11 15:47:54 +02:00
Ashin Walpola f507a8a9fd Fix UPER encoding of CurvatureCalculationMode; verified on hardware
CurvatureCalculationMode is the one extensible ENUMERATED in CAM:
  ENUMERATED {yawRateUsed(0), yawRateNotUsed(1), unavailable(2), ...}
UPER encodes an extensible ENUMERATED as an extension bit followed by the root
index - 1 + 2 = 3 bits. All three of our encoders wrote only the 2-bit index,
shifting yawRate and the entire low-frequency container one bit early for any
standards-compliant receiver.

It went unnoticed because every end of this project shared the mistake: the
Kotlin codec was ported bit-for-bit from cam.c, so phone and ESP32 agreed
perfectly with each other and with nothing else. Confirmed against the ETSI
ASN.1 in the C-ITS-Parser checkout, where rasn marks this type - and only this
type - #[non_exhaustive].

Fixed in all three copies of the encoder (app CamUperCodec.kt,
obu-firmware/main/cam.c, obu-cam-transmistter/main/cam.c) plus the decoder,
which now rejects rather than misreads a set extension bit. Frame size is
unchanged at 43 bytes. Transmitter reflashed and the phone decodes its CAMs.

Also in this change:

- serial_link: skip send_frame entirely when no USB host is attached, and raise
  the tx mutex timeout above the worst-case hold. With the phone unplugged every
  write blocked its full timeout while holding the lock, so forwarded CAM_RX
  traffic starved the 1 Hz heartbeat - observed as "tx mutex timeout, dropping
  frame" on the console, and it would have tripped the phone's link watchdog.
  Verified gone on hardware.
- Log decoded and failed CAMs in CamUseCaseRepository. "The app shows nothing"
  had two indistinguishable causes; a silent `?: return` made this bug much
  harder to find than it needed to be.
- Remove the ESP32 send-only/send-and-receive toggle. Reception can't be
  disabled in firmware (raw TX only works while promiscuous), so it was an
  app-side filter pretending to be a radio control.
- V2X monitor follows the serial link state on the ESP32 path instead of MQTT,
  which is permanently disconnected there; CAM intake is gated on the link being
  up, and engine state is cleared when it drops.
- About screen: 0.5.0, Phase 03.
- Track obu-cam-transmistter, the bench CAM transmitter. Its cam.c is compiled
  (unlike obu-firmware's reference copy) and must stay bit-identical to the other
  two - this commit is what that coupling costs when it's broken.
- Document the two-toolchain split: this project builds on IDF 5.5.4, obu-firmware
  on the pinned 6.1. Exporting both in one shell fails confusingly.
2026-08-11 14:50:35 +02:00
Ashin Walpola b91eb460dc Phase 03: CAM decode coverage, real sensor data in TX, V2X monitor for ESP32 path
CAM codec:
- Stop rejecting CAMs carrying a specialVehicleContainer. It is declared last in
  CamParameters, after everything this decoder reads, so buses / emergency
  vehicles / road-works vehicles now decode for position and kinematics instead
  of being dropped outright
- Drop the lowFrequencyContainer parse - it extracted nothing into Cam, and its
  reads were only correct when no high-frequency optionals were present
- Document why the 7 optional-presence bits are consumed but not acted on: UPER
  writes a SEQUENCE's presence bitmap up front but each field's value in
  declaration order, and all seven are declared after yawRate
- Field widths and container ordering verified against the ETSI ASN.1 sources in
  the C-ITS-Parser checkout, not from memory

Transmit path:
- Own StationID is now a persisted random 32-bit value instead of a hardcoded 0.
  Receivers key on StationID to track a station across CAMs, so every unit
  broadcasting 0 made two MicrOBUs indistinguishable - including to this app's
  own detection engine
- Populate longitudinalAcceleration from successive GNSS speed samples. Not from
  the accelerometer: CAM wants signed along-track acceleration, and the raw
  sensor is device-frame with gravity in it. Null outside a usable sample gap
  rather than a fabricated value
- CAM pinger builds from live GNSS/IMU via PhoneCamBuilder instead of beaconing a
  hardcoded bench coordinate with speed and heading pinned to zero, so it now
  exercises the sensor pipeline and not just the wire. Sends nothing without a
  fix, and reports that rather than sitting at "Sent: 0"

V2X monitor:
- Received-CAM pane for the ESP32-C5 path, replacing the MQTT topic list that is
  permanently empty there. One row per station rather than per message - CAMs
  arrive at 1-10 Hz per station, so the pane is bounded by road users nearby, not
  by traffic rate. Nearest first, tinted by active alert level
- DENM hazard pins on the live map as a warning triangle, drawn above vehicle
  markers. CiT One path only: the ESP32 firmware forwards BTP-B port 2001 (CAM)
  and drops port 2002 before it reaches the phone

DenmParser uses tolerant field-name matching - the Use Case API's DENM JSON
schema is not yet confirmed against real payloads.
2026-08-10 14:09:18 +02:00
Ashin Walpola 64fb78590e Phase 03: ESP32-C5 serial link hardening + bench diagnostics
Enumeration:
- Merge the library's stock probe table instead of replacing it, so adding
  Espressif 0x303A/0x1001 doesn't drop every other supported device
- Select the ESP32-C5 by VID/PID rather than list position
- Log USB interface descriptors to distinguish CDC data from the JTAG interface
Lifecycle:
- Don't close the shared port in MqttViewModel.onCleared() - the foreground
  recording service outlives the ViewModel and would beacon into a dead port
- Handle ACTION_USB_DEVICE_DETACHED so the UI stops reporting a stale link
- Implement the STATUS heartbeat on both sides (1 Hz) plus a phone-side watchdog
- Surface write failures and firmware drop counters on the CAM Pinger card
Protocol:
- Assert DTR/RTS on open (unverified on hardware - see FLASHING.md step 5)
- Raise SERIAL_LINK_MAX_PAYLOAD 160 -> 512 on both sides; real third-party CAMs
  exceed 160 and were being silently dropped at the resync branch
- Move the enlarged buffers off task stacks; serialize send_frame with a mutex
Firmware and app must be updated together - a 512/160 mismatch fails silently.
2026-08-10 11:38:01 +02:00
Ashin Walpola 33c4ec5998 Phase 03: real CAM UPER codec + ESP32-C5 TX/RX serial link
- Firmware: rewrite obu-firmware TX loop to be serial-driven (no on-chip timer), add promiscuous RX + GeoNetworking/BTP unwrap (gn_unwrap.c), add binary UART framing to the phone (serial_link.c/.h). Drop local cam_encode() - CAM is now built on the phone.
- Kotlin: byte-exact UPER CAM encoder/decoder ported from cam.c (BitWriter/BitReader/CamUperCodec), matching SerialFrame codec, real UsbSerialTransport (usb-serial-for-android), CamTransmitLoop (1Hz base rate, event/geofence boost, ESP32-C5-only), wired into CamUseCaseRepository for RX and TripRecordingService for TX.
- Add V2X message retention: persist all CAM (own+remote) to Room while recording, drop otherwise (DB v2 -> v3 migration).
- Add jitpack repo + usb-serial-for-android dependency.

Fixes: UsbSerialTransport now uses SerialInputOutputManager.start()/stop() (this lib version manages its own thread internally) instead of manual Runnable/Thread submission, which didn't compile.
2026-08-04 17:58:07 +02:00
Ashin Walpola 842d362c8f initial commit 2026-07-24 11:41:29 +02:00
Ashin Walpola 7b8a8cbb32 feat: CAM-based use case detection engine, decouple DENM from sensor triggers
Added:
- UseCaseDetectionEngine: IMA-B, IMA-S, RTW-B, LTW-B, SMVA/BCW-B via per-station
  CAM history, yaw-rate/heading-trend turn detection, Info/Awareness/Warning alerts
- Ego state from obu_gnss (~4Hz, yaw rate) with phone GPS fallback when stale
- Human-readable alert narratives + technical detail in V2X Monitor
- Settings > Use Case Alerts per-use-case toggles

Fixed:
- DENM no longer auto-fires on braking/stopping; manual-only (antenna/RSU test)
- Removed unwired CamBuilder.kt (dead code, contradicted CAM-only architecture)
- TripRecordingService no longer depends on MQTT/OBU connectivity
2026-07-02 15:04:34 +02:00
Ashin Walpola 4b20820ded docs: add project README 2026-06-08 16:23:08 +02:00
Ashin Walpola dbd2fc780b fix: prevent LazyColumn crash on duplicate timestamps in V2X topic detail view 2026-06-08 16:15:42 +02:00
Ashin Walpola baf4853c0f 03,06.2026 change 2026-06-03 15:15:39 +02:00
Ashin Walpola 1dc0286af8 edit 03.06.2026 2026-06-03 14:58:46 +02:00
Ashin Walpola ae17d4fcba Initial Commit 2026-06-03 14:51:31 +02:00