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.
This commit is contained in:
2026-04-30 15:47:06 +02:00
parent 95e60a2c75
commit c8a5f4cd68
23 changed files with 2508 additions and 0 deletions
@@ -0,0 +1,89 @@
# Task 1.2 — Core shell & framing types
**Phase:** 1 — Inbound telemetry
**Status:** ⬜ Not started
**Depends on:** 1.1
**Wiki refs:** `docs/wiki/concepts/protocol-adapter.md`, `docs/wiki/concepts/codec-dispatch.md`, `docs/wiki/concepts/position-record.md`
## Goal
Build the vendor-agnostic shell: TCP server bootstrap, per-socket session loop, and the type/registry definitions that adapters plug into. **No Teltonika-specific code in this task.**
## Deliverables
- `src/core/types.ts`:
- `Position` type matching the [[position-record]] shape exactly.
- `Adapter` interface: `{ name: string; ports: number[]; handleSession(socket: net.Socket, ctx: AdapterContext): Promise<void> }`.
- `AdapterContext` interface: `{ publish: (p: Position) => Promise<void>; logger: Logger; metrics: Metrics }` — a narrow contract giving adapters what they need without leaking shell internals.
- `src/core/registry.ts`:
- `AdapterRegistry` class (or simple module) holding `Map<port, Adapter>`. Methods: `register(adapter)`, `get(port)`.
- This is the *adapter* registry. The *codec* registry (per-vendor, in Teltonika's case) is internal to the adapter — it lives in `src/adapters/teltonika/` (task 1.4).
- `src/core/session.ts`:
- `runSession(socket, adapter, ctx)` that wraps `adapter.handleSession` with:
- Initial socket configuration (`setNoDelay`, `setKeepAlive` with a sane delay, e.g. 60s).
- Standard error handling: `error`, `close`, `end` events all logged at `debug` level with the connection's remote address.
- A `try { await handleSession() } catch (e) { logger.warn(e) }` that ensures the socket is destroyed if the handler throws.
- Crucially, `runSession` does *not* know about IMEI, framing, or codecs — those are entirely the adapter's business.
- `src/core/server.ts`:
- `startServer(port, adapter, ctx)` returning a closable handle. Uses `net.createServer((socket) => runSession(socket, adapter, ctx))`.
- Logs server bind, accepts, and close events.
- `src/core/publish.ts`:
- Stub `publishPosition(position)` returning `Promise<void>`. Real implementation lands in task 1.8. For now, it accepts a `Position` and logs at debug. The shape should already match what task 1.8 will produce so `Adapter` types stabilize early.
## Specification
### Vendor-agnostic discipline (re-stated)
**`src/core/` must not import from `src/adapters/` — ever.** This is enforced by ESLint with `eslint-plugin-import`'s `no-restricted-paths` rule. Add the rule in this task; failure to follow it should be a CI error.
```js
// in eslint.config.js
'import/no-restricted-paths': ['error', {
zones: [{ target: './src/core', from: './src/adapters' }]
}]
```
Adapters can import from `core/`; the reverse is forbidden.
### TCP socket settings
- `socket.setNoDelay(true)` — disable Nagle so ACKs are not batched. We're sending small ACKs; latency matters more than packet count.
- `socket.setKeepAlive(true, 60_000)` — TCP keepalive with 60s probe. Defends against idle NAT timeouts; safe because devices already retransmit on disconnect.
- No `socket.setTimeout()` at the shell level. The protocol does not specify per-frame timing; idle sockets are fine. Adapters can impose timeouts if their protocol demands.
### Position type
Mirror [[position-record]] precisely:
```ts
export type Position = {
device_id: string;
timestamp: Date;
latitude: number;
longitude: number;
altitude: number;
angle: number; // 0360
speed: number; // km/h, 0 may mean "GPS invalid" — caller preserves verbatim
satellites: number;
priority: 0 | 1 | 2; // Low | High | Panic
attributes: Record<string, number | bigint | Buffer>;
};
```
Use `Date` not `number` for `timestamp` — the value is a `Date` from the moment it leaves Ingestion, downstream is responsible for serialization choice.
## Acceptance criteria
- [ ] `pnpm typecheck` and `pnpm lint` pass.
- [ ] `src/core/server.ts` can be imported and `startServer` returns a `net.Server` that listens on a configurable port.
- [ ] A trivial test (in `test/core/server.test.ts`) starts a server with a stub adapter, opens a TCP client, and verifies the adapter's `handleSession` is invoked with a real socket.
- [ ] ESLint enforces the no-restricted-paths rule — verified by adding a temporary import-from-adapter into `src/core/server.ts` and confirming the lint error, then removing it.
## Risks / open questions
- The `AdapterContext`'s metrics interface is sketched but not fully specified until task 1.10. Make a minimal placeholder (`{ inc: (name, labels?) => void; observe: ... }`) and tighten in 1.10.
- `Buffer` in `Position.attributes` requires JSON serialization handling at the publish boundary (task 1.8). Decide there: base64-encode buffers, or serialize via msgpack. Recommendation: base64 with a sentinel in the JSON, e.g. `{ "_b64": "..." }`. Defer the decision to task 1.8 and revisit if simpler options surface.
## Done
(Fill in once complete.)