90d6a73a60
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).
99 lines
5.3 KiB
Markdown
99 lines
5.3 KiB
Markdown
# 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.)
|