# 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 0โ€“255 and pass through** โ€” the Teltonika spec lists 0โ€“2, 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 (0โ€“360). - **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.)