Files
julian c8a5f4cd68 Add Phase 1 and Phase 2 planning documents
ROADMAP plus granular task files per phase. Phase 1 (12 tasks + 1.13
device authority) covers Codec 8/8E/16 telemetry ingestion; Phase 2
(6 tasks) covers Codec 12/14 outbound commands; Phase 3 enumerates
deferred items.
2026-04-30 15:50:49 +02:00
..

Phase 2 — Outbound commands

Add server-to-device command delivery using Teltonika codecs 12 (0x0C) and 14 (0x0E). Codec 13 is one-way device→server (not in scope for outbound); codec 15 is FMX6-only (out of scope entirely).

Prerequisite

Phase 1 must be complete and stable in production. Phase 2 adds code alongside Phase 1, never in the inbound parsing path.

Outcome statement

When Phase 2 is done:

  • Each Ingestion instance maintains its IMEI→instance mapping in connections:registry (Redis hash) and a heartbeat key.
  • A Directus Flow on commands table inserts can publish a command to commands:outbound:{instance_id} after looking up the routing.
  • Each Ingestion instance runs a command consumer in parallel with the TCP listener; consumed commands are dispatched to the right per-socket write queue, encoded as Codec 12 or 14, and written to the device.
  • Device responses (Codec 12 Type 0x06 or Codec 14 Type 0x06/0x11) are correlated to the in-flight command and published to commands:responses for Directus to update the row.
  • The TCP read path is never blocked by outbound work.
  • Phase 1 code is unchanged.

Architectural anchors

docs/wiki/concepts/phase-2-commands.md is the design source of truth. Read it before starting any Phase 2 task.

Key invariants:

  1. Ingestion exposes no user-facing HTTP — never. All command authorization happens in Directus.
  2. Commands are data before transport. Every command has a row in Directus's commands table before it ever reaches Redis.
  3. One outstanding command per device socket. Teltonika command codecs have no correlation ID; the protocol assumes serialization. Subsequent commands queue on the per-socket write queue.
  4. Per-instance routing. Only the Ingestion instance currently holding a device's socket can deliver commands to it. The connection registry exists so Directus knows which instance to publish to.

Sequencing

2.1 Connection registry & heartbeat ─┐
2.2 Registry janitor                  ├─→ 2.4 Command consumer ─┐
2.3 Per-socket write queue ──────────┘                          ├─→ 2.5 Codec 12 handler
                                                                └─→ 2.6 Codec 14 handler

Tasks 2.1, 2.2, 2.3 can be done in parallel; they are independent infrastructure pieces. 2.5 and 2.6 can be parallelized once 2.4 lands.

Files added

Phase 2 introduces these new files (no Phase 1 file is modified except src/main.ts to wire in the command consumer):

src/
├── adapters/teltonika/
│   ├── codec/command/
│   │   ├── codec12.ts        ← NEW (encoder + response parser)
│   │   └── codec14.ts        ← NEW (encoder + ACK/nACK parser)
│   └── command-consumer.ts   ← NEW (stream reader, dispatch)
├── core/
│   ├── connection-registry.ts ← NEW
│   ├── write-queue.ts         ← NEW
│   └── janitor.ts             ← NEW (separate small process or in-process worker)
└── main.ts                    ← updated to start consumer + registry

src/adapters/teltonika/codec/command/ already exists from Phase 1 (empty placeholder); Phase 2 fills it.

Out of scope for this phase

  • The Directus side of the system (commands table, Flows, sweeper) is owned by the Directus repo, not this one. Phase 2 in this repo only handles the Ingestion-side consumer and writer behavior.
  • The pending-command sweeper runs in Directus, not Ingestion. Ingestion publishes terminal status (delivered, responded, or failed reasons) and Directus updates the row.