Files
tcp-ingestion/.planning/phase-1-telemetry/05-codec-8.md
T
julian 90d6a73a60 Sync ROADMAP statuses with landed work; mark 1.10/1.12/1.13 as paused
Tasks 1.1-1.9 marked done with their landing commit SHAs. Tasks 1.10
(observability), 1.12 (production hardening), and 1.13 (device
authority) marked paused with explicit resume triggers — pilot
deployment on real Teltonika hardware takes priority. Task 1.11
remains as next, in slimmed form for the pilot (no /readyz healthcheck
since the metrics endpoint is part of paused 1.10).
2026-04-30 16:49:07 +02:00

99 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task 1.5 — Codec 8 parser
**Phase:** 1 — Inbound telemetry
**Status:** 🟩 Done — landed in commit `381287b`
**Depends on:** 1.4, 1.9 (fixture infra)
**Wiki refs:** `docs/wiki/concepts/avl-data-format.md` § Codec 8, `docs/wiki/sources/teltonika-data-sending-protocols.md` § Codec 8
## Goal
Parse Codec 8 (`0x08`) AVL data bodies into `Position` records and publish them via `ctx.publish`.
## Deliverables
- `src/adapters/teltonika/codec/data/codec8.ts` exporting `codec8Handler: CodecDataHandler` with `codec_id: 0x08`.
- Helper functions in the same file (or in a sibling `gps-element.ts` if shared with codec 8E and 16):
- `parseGpsElement(buf, offset): { value: GpsElement; nextOffset: number }`
- `parseTimestamp(buf, offset): { value: Date; nextOffset: number }`
- Test file `test/codec8.test.ts` with at least the three fixtures from the canonical Teltonika example doc plus a synthetic empty-IO fixture and a multi-record fixture.
## Specification
### AVL record layout (Codec 8)
```
[Timestamp 8B] [Priority 1B] [GPS Element 15B] [IO Element ...]
```
#### Timestamp
8-byte big-endian unsigned integer = milliseconds since UNIX epoch UTC. Convert to `Date` via `new Date(Number(buf.readBigUInt64BE(offset)))`. Use `BigInt` arithmetic to avoid the `Number` precision concern; values are well within safe range until ~year 285k, but be explicit.
#### Priority
1-byte enum: `0` = Low, `1` = High, `2` = Panic. Reject unexpected values? Decision: **accept any value 0255 and pass through** — the Teltonika spec lists 02, but treating "unexpected priority" as parser failure feels hostile. Log a `debug` line if value > 2 but proceed.
Type-narrow to `0 | 1 | 2` only when value is in range; otherwise record as `2` (Panic, the most conservative) and emit a metric `teltonika_priority_out_of_range_total`. Open question: confirm with operations the right fallback.
#### GPS Element (15 bytes)
```
[Longitude 4B][Latitude 4B][Altitude 2B][Angle 2B][Satellites 1B][Speed 2B]
```
- **Longitude / Latitude**: signed 32-bit big-endian integer (two's complement), divided by `1e7` to get decimal degrees. Negative bit handling: `buf.readInt32BE(offset) / 1e7` does the right thing because Node's `readInt32BE` interprets the value as signed.
- **Altitude**: 2-byte signed big-endian, meters above sea level.
- **Angle**: 2-byte unsigned big-endian, degrees from north pole (0360).
- **Satellites**: 1-byte unsigned.
- **Speed**: 2-byte unsigned, km/h. **Pass through verbatim**`0x0000` may mean "GPS invalid" but that semantic decision belongs to the Processor.
#### IO Element (Codec 8 layout)
```
[Event IO ID 1B]
[N total 1B]
[N1 1B] then N1 × ([IO ID 1B][Value 1B])
[N2 1B] then N2 × ([IO ID 1B][Value 2B BE unsigned])
[N4 1B] then N4 × ([IO ID 1B][Value 4B BE unsigned])
[N8 1B] then N8 × ([IO ID 1B][Value 8B BE — store as bigint])
```
Iterate each section and write into `position.attributes`:
```ts
attributes[String(ioId)] = value;
```
Values:
- 1-byte → `number` (read with `readUInt8`)
- 2-byte → `number` (read with `readUInt16BE`)
- 4-byte → `number` (read with `readUInt32BE`)
- 8-byte → `bigint` (read with `readBigUInt64BE`)
**Do not decode signedness** for IO values. The spec is silent on per-IO signedness; downstream model-aware code in the Processor handles that. If a downstream interpretation needs signed, it can `(unsigned > 0x7FFFFFFF) ? unsigned - 0x100000000 : unsigned` itself.
The `Event IO ID` value is captured into a separate `event_io_id` attribute — propose: store as `attributes['__event']` (or as a typed sibling field on the Position; recommendation: store under `attributes['__event']` to keep the `Position` shape stable and avoid adding a Codec-8-specific field).
> **Open question:** is `__event` the right key? Alternatives: `'_event_io_id'`, `'0'` (it's IO ID 0 in some interpretations, but that's a different "0"). Decide before merging task 1.5.
### Record loop
After parsing N1 (number of records from the framing layer), loop N1 times producing one `Position` per record. Validate that the position of the cursor at the end equals the start of the trailing N2 byte; mismatch is a parser bug → throw with a structured error including offset.
## Acceptance criteria
- [ ] All three canonical doc examples (single record with all IO widths; single record with reduced IO; two records) parse to the expected `Position[]` byte-for-byte (verified via the fixture suite from task 1.9).
- [ ] CRC validation already happened upstream (task 1.4); this task does not re-check.
- [ ] Cursor-end-equals-N2 invariant holds for every fixture.
- [ ] `Position.timestamp` round-trips: `new Date(...).toISOString()` matches the doc example's `GMT: Monday, June 10, 2019, 10:04:46 AM` for the first fixture.
- [ ] All IO IDs from the fixture appear in `attributes` with correct numeric/bigint types.
## Risks / open questions
- The `Event IO ID` field semantics. Storing under `'__event'` keeps things flexible but adds a magic key. Discuss with Processor implementer before settling.
- 8-byte values as `bigint` complicate JSON serialization. Task 1.8 (publisher) must handle this — recommend serializing as a string with a sentinel, e.g. `"123n"` or `{ "_bigint": "123" }`. Keep parser side clean (real `bigint`); push encoding to the publish boundary.
## Done
(Fill in once complete.)