# GigLez Submission Format

GigLez accepts Sub-GHz captures from a range of popular tools. Every submission
is a `multipart/form-data` `POST` to:

```
POST /api/v1/captures/upload
```

with two parts:

| Part | Type | Purpose |
|------|------|---------|
| `files` | one or more file uploads | the captures themselves |
| `manifest` | JSON string (form field) | GPS, timestamps, session + privacy options |

Format is **auto-detected per file** (by extension, then by content sniffing), so
you can mix types in a single request. Supported formats:

| Format | Extensions | Detected by | Handling |
|--------|-----------|-------------|----------|
| Flipper Zero | `.sub` | `Filetype:` header | parsed + signature-matched (category router → protocol DB) |
| rtl_433 | `.json`, `.ndjson`, `.jsonl` | leading `{` / `[` | already decoded — model **is** the device |
| Wigle-style tabular | `.csv` | comma in first line | already decoded — one row per observation |
| Batch | `.zip` (`PK` magic) | zip magic | recursed; any mix of the above + optional `manifest.json` |

---

## Manifest

```json
{
  "data_source": "production",
  "session_uuid": "optional-session-id",
  "latitude": 34.0200,
  "longitude": -118.3000,
  "privacy_gps_decimals": 4,
  "captures": [
    {
      "filename": "capture_001.sub",
      "latitude": 34.0201,
      "longitude": -118.3011,
      "timestamp": "2026-07-18T12:05:00Z"
    }
  ]
}
```

| Field | Scope | Notes |
|-------|-------|-------|
| `data_source` | submission | `production`, `test`, or `mock`. Defaults to `production`. |
| `session_uuid` | submission | Optional grouping id, stored as `session_id`. |
| `latitude` / `longitude` | submission | **Session-level GPS fallback** for decoded records that carry no coords. |
| `privacy_gps_decimals` | submission | If set, all coordinates are rounded to this many decimals before storage. Omit to store full precision. |
| `captures[]` | per file | Keyed by `filename`. Supplies per-file GPS + timestamp. |

### GPS resolution order

1. **Coordinates in the record itself** (rtl_433 / CSV `latitude`/`longitude`, or GPS embedded in a `.sub` filename)
2. **Per-file manifest entry** (`captures[]` matched by `filename`)
3. **Session-level manifest GPS** (top-level `latitude`/`longitude`)

The chosen source is recorded on each capture as `gps_source`
(`record` / `manifest` / `session` / `manual` / `none`).

---

## Format details

### Flipper Zero `.sub`
Both decoded key files (`Protocol` / `Bit` / `Key`) and RAW pulse captures
(`RAW_Data`). These are matched through the category router and protocol
database; the result carries `match_confidence` and `match_method`.

### rtl_433 `.json` / `.ndjson`
One JSON object per line (NDJSON) **or** a single JSON array. rtl_433 output is
already decoded, so the reported `model` is taken as the device (no signature
matching). Recognised fields include `model`, `id`, `channel`, `battery_ok`,
`temperature_C`, `humidity`, `pressure_kPa`, `mod`, `freq`, `time`. Frequency in
MHz is normalised to Hz. `match_method` = `rtl_433_decoded`, confidence `1.0`.

### Wigle-style `.csv`
Header row + one observation per row. Useful columns: `model` (or
`device_name` / `protocol`), `manufacturer`/`brand`, `frequency` (Hz or MHz),
`latitude`/`longitude`, `timestamp`, plus optional decoded fields. `match_method`
= `csv_import`, confidence `0.9`.

### `.zip` batch
Any mix of the above. Directory entries, dotfiles, and a top-level
`manifest.json` member are skipped; every other member is ingested by its own
detected format.

---

## Deduplication

Re-submitting the same data is a no-op. Each stored capture carries a stable
`dedup_key`:

- **Whole-file formats (`.sub`)** — SHA256 of the raw file bytes.
- **Decoded records (rtl_433 / CSV)** — composite of
  `protocol · device_name · frequency · id · channel · rounded lat/lon · timestamp` (to the second).

On upload, records whose key already exists are reported under `skipped` rather
than stored again.

---

## Response

```json
{
  "success": true,
  "message": "Processed 9 records (0 duplicates skipped, 0 failed)",
  "successful": [ /* stored capture records */ ],
  "skipped":    [ /* duplicate records, not stored */ ],
  "failed":     [ { "filename": "...", "error": "..." } ],
  "total_uploaded": 2,
  "total_successful": 9,
  "total_skipped": 0,
  "total_failed": 0
}
```

A single uploaded file (rtl_433/CSV/ZIP) can expand into many capture records,
so counts are per-record, not per-file.
