# mgf-tele-core

The **vendor-neutral telematics seam** for `mgf-common` consumers: the
`TelematicsProvider` device-session ABC, the codec-agnostic AVL frame
types, a typed error hierarchy, and a scripted mock. A sibling of
`mgf-common` under the `mgf.*` namespace, and the foundation of the
`mgf-tele-*` family — per-vendor codec packages (`mgf-tele-teltonika`,
later `mgf-tele-queclink`, …) depend on this and never the other way.

Extracted from PlasmaMapper's ingest path (ADR-010's two-layer seam):
the provider owns the device session; a per-vendor codec is a pure
`bytes → DecodedAvlRecord` dependency injected into it.

## Install

```bash
pip install mgf-tele-core
```

## What's in it

| Name | What |
|---|---|
| `TelematicsProvider` | the device-session ABC: `open_session` / `receive_packet` / `send_command` / `close_session` |
| `SessionHandle` / `TrackerCommand` / `CommandAck` | the seam's command/session vocabulary |
| `GeoPoint` / `DecodedAvlRecord` / `ReceivedPacket` / `ParseStatus` | codec-agnostic AVL frame types (every vendor normalises to these) |
| `TelematicsError` (+ `MalformedPacketError`, `ChecksumError`, `UnsupportedCodecError`, `MalformedHandshakeError`, `UnknownTrackerError`, `SessionClosedError`) | typed errors, all subclass `mgf.common.exceptions.AppError` |
| `mgf.tele.core.mock.MockTelematicsProvider` | scripted in-memory provider for consumers' tests |

## The two-layer shape

```
mgf-tele-core        TelematicsProvider (ABC)  +  AVL frame types  +  errors
      ▲
      │ depends on
mgf-tele-teltonika   Codec 8/8E/12 parser  +  TeltonikaProvider(codec)
mgf-tele-queclink    (future) …
```

A consumer wires a concrete provider into its ingest loop:

```python
async def session_loop(provider: TelematicsProvider, reader, writer):
    handle = await provider.open_session(reader, writer)
    try:
        while True:
            packet = await provider.receive_packet(handle)   # ReceivedPacket
            persist_raw(packet)                               # always (forensic)
            if packet.parse_status == "ok":
                ingest(packet.records)                        # DecodedAvlRecord[]
    except SessionClosedError:
        pass
    finally:
        await provider.close_session(handle)
```

Tests use `MockTelematicsProvider(batches=[…])` — no socket, no vendor
codec — to drive the consumer's ingest logic deterministically.

## Design notes

- **Frozen frame types** (rule DP-03); **UTC at the boundary** (DP-12).
- `receive_packet` **always returns the raw bytes** even on decode
  failure (`parse_status` ≠ `"ok"`) so the consumer can persist a
  forensic record a future codec update may recover.
- Read-only consumers may have their provider raise
  `NotImplementedError` from `send_command` until a control plane lands.
