# 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.