# Ingestion Pipeline

The packet ingestion pipeline moves raw APRS data from the APRS-IS network into Postgres and broadcasts to connected clients.

## Components

### Aprsme.Is (TCP Connection)

- **File:** `lib/aprsme/is/is.ex`
- **Type:** GenServer
- **Role:** Maintains TCP connection to APRS-IS, authenticates with callsign + passcode, receives raw APRS frames
- **Parsing:** Uses `aprs.parse/1` (external Gleam library) to decode APRS frames
- **Filtering:** Configurable APRS-IS filter (by callsign, area, distance) via `Aprsme.Is.LoginParams`
- **Backpressure:** Responds to `{:backpressure, :activate | :deactivate}` signals from `PacketProducer` — sets TCP socket to `:passive` mode when buffer is full

### PacketProducer (GenStage)

- **File:** `lib/aprsme/packet_producer.ex`
- **Type:** GenStage producer
- **Buffer:** Erlang `:queue` with water-mark backpressure
  - **High water** (>80% capacity): signals `Aprsme.Is` to activate backpressure
  - **Low water** (<30% capacity): signals `Aprsme.Is` to deactivate backpressure
- **Demand-driven:** Only emits packets when consumers are ready

### PacketConsumer (GenStage Consumer)

- **File:** `lib/aprsme/packet_consumer.ex`
- **Type:** GenStage consumer (pooled via `PacketConsumerPool`)
- **Processing pipeline per packet:**
  1. Sanitize — `PacketSanitizer.sanitize_packet/1` (overflow prevention)
  2. Normalize — `EncodingUtils` conversions (UTF-8, latin1, floats, decimals)
  3. Extract position — lat/lon → PostGIS geometry
  4. Extract device — `DeviceParser.extract_device_id/1`
  5. Filter fields — `PacketFieldWhitelist` trims to schema columns
  6. Insert — `Repo.insert_all` with `on_conflict: :nothing` (idempotent)
  7. Broadcast — async dispatch to PubSub systems

### Broadcasting After Insert

After successful insert, each packet is routed through:

1. **SpatialPubSub** — Grid-indexed viewport delivery with explicit unbounded subscriptions
2. **Phoenix.PubSub** — Targeted broadcasts:
   - `"packets:#{callsign}"` — per-callsign updates
   - `"weather:#{callsign}"` — per-weather-station updates

Broadcast execution is async via `BroadcastTaskSupervisor`.

## Error Handling

Packets that fail parsing or validation are stored in `badpackets` table via `Packets.store_bad_packet/2` with error type/message, viewable at `/badpackets` page.

## Configuration

- APRS-IS server, callsign, passcode set via application config
- Consumer pool size configurable per environment
- Retention period configurable via `CleanupScheduler`

## Performance Notes

- `PreparedQueries` module caches frequently-used query plans
- `PacketConsumer` uses `insert_all` (not individual inserts) for throughput
- Daily table partitioning via `PartitionManager` keeps index sizes manageable
