# Kontekst AI — Perun BLE Protocol

Plik przeznaczony dla AI pomagającego pisać aplikację kliencką komunikującą się z urządzeniami MWT.

Zawiera pełny opis protokołu BLE: serwis Perun, ramki danych (Data Frame) dla wszystkich modułów,
protokół Control (SET/GET), klucze, kalibracje i kody błędów.
Firmware dostarcza producent — ten dokument opisuje **wyłącznie interfejs po stronie klienta** (Android, iOS, Web, PC).

[Pobierz plik .md :material-download:](../../ai_context.md){ .md-button .md-button--primary download="ai_context.md" }

---

## 1. BLE — Serwis Perun

Urządzenia MWT eksponują serwis Perun z czterema charakterystykami:

| Charakterystyka   | UUID                                   | Właściwości              |
|-------------------|----------------------------------------|--------------------------|
| Service           | `457bbb14-9c79-44a8-9810-f17bd358a200` | —                        |
| **Data**          | `457bbb14-9c79-44a8-9810-f17bd358a201` | **Notify**               |
| **Control**       | `457bbb14-9c79-44a8-9810-f17bd358a202` | **Write + Indicate**     |
| **Device Info**   | `457bbb14-9c79-44a8-9810-f17bd358a203` | **Read**                 |
| **RTCM Stream**   | `457bbb14-9c79-44a8-9810-f17bd358a204` | **Write Without Response** |

Dodatkowo dostępny jest standardowy **Battery Service (BAS)**:

| Charakterystyka   | UUID                                   | Właściwości       |
|-------------------|----------------------------------------|-------------------|
| BAS Service       | `0000180f-0000-1000-8000-00805f9b34fb` | —                 |
| Battery Level     | `00002a19-0000-1000-8000-00805f9b34fb` | Read + Notify     |

**Varianty urządzeń** (z Device Info):

| Wartość | Wariant             | Typowe moduły |
|:-------:|---------------------|---------------|
| `0x01`  | MWT Base Station    | GNSS, Barometer (×4), Battery |
| `0x02`  | MWT Body Module     | AccGyro, Barometer, Battery, GNSS, Magnetometer, Quaternion |
| `0x03`  | MWT ETU             | AccGyro, Battery, Selector, Trigger, Kickback |
| `0x04`  | MWT Head Module     | AccGyro, Barometer, Battery, Magnetometer, Quaternion |

---

## 2. Device Info (Charakterystyka a203 — READ)

Po połączeniu odczytaj tę charakterystykę. Payload w formacie TLV (Type-Length-Value):

```
[type (1B)][length (1B)][value (length B)] ...
```

| Type   | Nazwa               | Opis |
|:------:|---------------------|------|
| `0x01` | MODULES_AVAILABLE   | Tablica 1B ID dostępnych modułów |
| `0x02` | VARIANT_ID          | `uint8_t` — wariant urządzenia |
| `0x03` | FW_VERSION          | 3B: `[major][minor][patch]` |

**Przykład — MWT Body Module, firmware 2.1.0:**
```
01 06 02 03 04 06 09 0A   ← MODULES_AVAILABLE: AccGyro(2),Baro(3),Bat(4),Mag(6),Quat(9),UI(10)
02 01 02                   ← VARIANT_ID: Body Module
03 03 02 01 00             ← FW_VERSION: 2.1.0
```

---

## 3. Identyfikatory modułów

| ID     | Stała               | Moduł                  | Sensor         |
|:------:|---------------------|------------------------|----------------|
| `0x01` | MODULE_SYSTEM       | System                 | —              |
| `0x02` | MODULE_ACC_GYRO     | Akcelerometr + Żyroskop | LSM6DSV32X    |
| `0x03` | MODULE_BAROMETER    | Barometr               | BMP581         |
| `0x04` | MODULE_BATTERY      | Napięcie baterii       | ADC            |
| `0x05` | MODULE_GNSS         | GNSS / RTK             | ZED-F9P        |
| `0x06` | MODULE_MAGNETOMETER | Magnetometr            | MMC5983MA      |
| `0x07` | MODULE_SELECTOR     | Selektor trybu ognia   | TMAG5273C1     |
| `0x08` | MODULE_TRIGGER      | Trigger                | TMAG5273C1     |
| `0x09` | MODULE_QUATERNION   | Fuzja orientacji       | (software)     |
| `0x0A` | MODULE_PLAYER_STATE | Stan gracza            | —              |
| `0x0B` | MODULE_UI           | UI                     | —              |
| `0x0C` | MODULE_KICKBACK     | Kickback               | —              |
| `0x0D` | MODULE_DISPLACEMENT | Estymacja przemieszczenia | (software)  |

---

## 4. Data Frame — Ramka danych (Charakterystyka a201 — NOTIFY)

Urządzenie wysyła ramki notify gdy moduł jest włączony i aktywny.
Każda ramka = **12B header + payload** (maks. 232B payload, łącznie maks. 244B przy MTU=247).

### Header (12B)

| Offset | Rozmiar | Pole             | Format         | Opis |
|:------:|:-------:|------------------|----------------|------|
| 0      | 1B      | `module_id`      | `uint8_t`      | ID modułu źródłowego |
| 1      | 1B      | `flags`          | `uint8_t`      | Flagi (patrz niżej) |
| 2      | 1B      | `frame_number`   | `uint8_t`      | Numer ramki (0–255, wrap) |
| 3      | 1B      | `sample_count`   | `uint8_t`      | Liczba próbek w payloadzie |
| 4      | 5B      | `timestamp_first`| `uint40_t` LE  | Znacznik czasu pierwszej próbki [µs] |
| 9      | 3B      | `timestamp_delta`| `uint24_t` LE  | Różnica czasu do ostatniej próbki [µs] |

**Flagi ramki:**

| Bit | Maska  | Nazwa         | Opis |
|:---:|:------:|---------------|------|
| 7   | `0x80` | `FLAG_ERROR`  | Ramka błędu — payload to 9B `error_t` (patrz sekcja 5) |
| 0   | `0x01` | Moduł-specyficzna | Znaczenie zależy od modułu (patrz opisy modułów) |

### Pomocnik — int24 little-endian (TypeScript)

```typescript
function readInt24LE(view: DataView, offset: number): number {
  const lo = view.getUint8(offset);
  const mi = view.getUint8(offset + 1);
  const hi = view.getInt8(offset + 2); // sign-extended
  return lo | (mi << 8) | (hi << 16);
}
```

---

## 5. Ramka błędu (FLAG_ERROR = 1)

Gdy `flags & 0x80 != 0`, payload = 9B struktura `error_t`:

| Offset | Rozmiar | Pole           | Format    | Opis |
|:------:|:-------:|----------------|-----------|------|
| 0      | 1B      | `level`        | `uint8_t` | Poziom błędu |
| 1      | 1B      | `source`       | `uint8_t` | Moduł źródłowy |
| 2      | 1B      | `reason`       | `uint8_t` | Kod przyczyny |
| 3      | 1B      | `instance`     | `uint8_t` | Instancja (np. numer sensora) |
| 4      | 1B      | `sensor_model` | `uint8_t` | Model sensora (opcjonalne) |
| 5      | 1B      | `detail_type`  | `uint8_t` | Typ detalu |
| 6      | 3B      | `detail_value` | `uint24_t` LE | Wartość detalu |

---

## 6. Payloady modułów

### 6.1 ACC_GYRO (ID = 2) — Akcelerometr + Żyroskop

**Rozmiar próbki: 18B** (6 osi × 3B int24 little-endian signed)

| Offset | Rozmiar | Pole     | Jednostka    | Przelicznik      |
|:------:|:-------:|----------|--------------|------------------|
| 0      | 3B      | `acc_x`  | deci-milli-g | ÷ 10000 → g      |
| 3      | 3B      | `acc_y`  | deci-milli-g | ÷ 10000 → g      |
| 6      | 3B      | `acc_z`  | deci-milli-g | ÷ 10000 → g      |
| 9      | 3B      | `gyro_x` | milli-°/s    | ÷ 1000 → °/s     |
| 12     | 3B      | `gyro_y` | milli-°/s    | ÷ 1000 → °/s     |
| 15     | 3B      | `gyro_z` | milli-°/s    | ÷ 1000 → °/s     |

Rozdzielczość: acc = **0.1 mg** (0.0001 g), gyro = **1 milli-°/s** (0.001 °/s).
Domyślne zakresy: ±32 g, ±1000 dps. ODR domyślne: 240 Hz.

```typescript
function parseAccGyroSample(view: DataView, offset: number) {
  return {
    accX:  readInt24LE(view, offset + 0)  / 10000, // g
    accY:  readInt24LE(view, offset + 3)  / 10000,
    accZ:  readInt24LE(view, offset + 6)  / 10000,
    gyroX: readInt24LE(view, offset + 9)  / 1000,  // °/s
    gyroY: readInt24LE(view, offset + 12) / 1000,
    gyroZ: readInt24LE(view, offset + 15) / 1000,
  };
}
```

### 6.2 BAROMETER (ID = 3) — Barometr

**Flags bit 0 = `MULTI_SENSOR`:** `0` = pojedynczy czujnik (5B/próbka), `1` = 4 czujniki (20B/próbka)

**Pojedynczy czujnik — 5B/próbka:**

| Offset | Rozmiar | Pole          | Jednostka | Przelicznik      |
|:------:|:-------:|---------------|-----------|------------------|
| 0      | 3B      | `pressure`    | centipaskal [cPa] | ÷ 100 → hPa |
| 3      | 2B      | `temperature` | 0.01°C    | ÷ 100 → °C       |

`pressure` = `uint24_t` LE (unsigned), `temperature` = `int16_t` LE (signed).

**4 czujniki (Base Station) — 20B/próbka:** 4× powtórzony format 5B.

### 6.3 BATTERY (ID = 4) — Napięcie baterii

**Rozmiar próbki: 2B**

| Offset | Rozmiar | Pole      | Format        | Opis |
|:------:|:-------:|-----------|---------------|------|
| 0      | 2B      | `voltage` | `uint16_t` LE | Napięcie [mV] |

### 6.4 GNSS (ID = 5) — Pozycja GPS/RTK

**Rozmiar próbki: 55B** (`gnss_pvt_data_t`, little-endian)

| Offset | Rozmiar | Pole         | Format        | Opis |
|:------:|:-------:|--------------|---------------|------|
| 0      | 4B      | `lat`        | `int32_t` LE  | Szerokość geogr. [×10⁻⁷ °] |
| 4      | 4B      | `lon`        | `int32_t` LE  | Długość geogr. [×10⁻⁷ °] |
| 8      | 4B      | `alt_msl`    | `int32_t` LE  | Wysokość nad poziomem morza [mm] |
| 12     | 4B      | `vel_n`      | `int32_t` LE  | Prędkość North [mm/s] |
| 16     | 4B      | `vel_e`      | `int32_t` LE  | Prędkość East [mm/s] |
| 20     | 4B      | `vel_d`      | `int32_t` LE  | Prędkość Down [mm/s] |
| 24     | 4B      | `head_mot`   | `int32_t` LE  | Kierunek ruchu [×10⁻⁵ °] |
| 28     | 4B      | `h_acc`      | `uint32_t` LE | Dokładność pozioma [mm] |
| 32     | 4B      | `v_acc`      | `uint32_t` LE | Dokładność pionowa [mm] |
| 36     | 4B      | `geoid_sep`  | `int32_t` LE  | Separacja geoidy [mm] |
| 40     | 4B      | `utc_nano`   | `int32_t` LE  | Ułamek sekundy UTC [ns] |
| 44     | 2B      | `pdop`       | `uint16_t` LE | pDOP [×0.01] |
| 46     | 2B      | `hdop`       | `uint16_t` LE | hDOP [×0.01] |
| 48     | 1B      | `fix_type`   | `uint8_t`     | 0=none, 2=2D, 3=3D, 4=GNSS, 5=Time |
| 49     | 1B      | `num_sv`     | `uint8_t`     | Liczba satelitów |
| 50     | 1B      | `flags`      | `uint8_t`     | bit0=gnssFixOK, bit1=diffSoln, bit2–3=carrSoln (0=none,1=float,2=fixed) |
| 51     | 1B      | `utc_hour`   | `uint8_t`     | Godzina UTC |
| 52     | 1B      | `utc_min`    | `uint8_t`     | Minuty UTC |
| 53     | 1B      | `utc_sec`    | `uint8_t`     | Sekundy UTC |
| 54     | 1B      | `gnss_state` | `uint8_t`     | Stan wewnętrzny modemu GNSS |

### 6.5 MAGNETOMETER (ID = 6) — Magnetometr

**Rozmiar próbki: 9B** (3 osie × 3B int24 little-endian signed)

**Flags bit 0 = `CALIBRATED`:** `0` = dane raw (bez korekcji hard/soft iron), `1` = dane skalibrowane.

| Offset | Rozmiar | Pole    | Jednostka    | Przelicznik    |
|:------:|:-------:|---------|--------------|----------------|
| 0      | 3B      | `mag_x` | nanoTesla    | ÷ 1000 → µT    |
| 3      | 3B      | `mag_y` | nanoTesla    | ÷ 1000 → µT    |
| 6      | 3B      | `mag_z` | nanoTesla    | ÷ 1000 → µT    |

Rozdzielczość: **1 nT** (0.001 µT). Typowe ziemskie pole: 40 000–60 000 nT.

```typescript
function parseMagSample(view: DataView, offset: number, flags: number) {
  return {
    x_uT:        readInt24LE(view, offset + 0) / 1000,
    y_uT:        readInt24LE(view, offset + 3) / 1000,
    z_uT:        readInt24LE(view, offset + 6) / 1000,
    isCalibrated: (flags & 0x01) !== 0,
  };
}
```

### 6.6 SELECTOR (ID = 7) — Selektor trybu ognia

**Rozmiar próbki: 1B.** Ramka wysyłana przy każdej zmianie pozycji.

| Wartość | Pozycja  |
|:-------:|----------|
| `1`     | SAFE     |
| `2`     | SEMI     |
| `3`     | AUTO     |

### 6.7 TRIGGER (ID = 8) — Trigger

**Rozmiar próbki: 1B.** Ramka wysyłana przy każdej zmianie stanu.

| Wartość | Stan      |
|:-------:|-----------|
| `1`     | PRESSED   |
| `2`     | RELEASED  |

### 6.8 DISPLACEMENT (ID = 13) — Estymacja przemieszczenia

**Rozmiar próbki: 12B** (3 osie × 4B int32 little-endian signed)

| Offset | Rozmiar | Pole    | Jednostka | Przelicznik |
|:------:|:-------:|---------|-----------|-------------|
| 0      | 4B      | `pos_x` | mm        | ÷ 1000 → m  |
| 4      | 4B      | `pos_y` | mm        | ÷ 1000 → m  |
| 8      | 4B      | `pos_z` | mm        | ÷ 1000 → m  |

Pozycja jest względna do ostatniego resetu (`KEY_DISP_RESET`). Rozdzielczość: **1 mm**.

```typescript
function parseDisplacementSample(view: DataView, offset: number) {
  return {
    x_m: view.getInt32(offset + 0, true) / 1000,
    y_m: view.getInt32(offset + 4, true) / 1000,
    z_m: view.getInt32(offset + 8, true) / 1000,
  };
}
```

### 6.9 QUATERNION (ID = 9) — Fuzja orientacji (Madgwick)

**Rozmiar próbki: 9B** (4 komponenty Q15 + bajt statusu)

| Offset | Rozmiar | Pole     | Format       | Opis |
|:------:|:-------:|----------|--------------|------|
| 0      | 2B      | `w`      | `int16_t` LE | Format Q1.15: ÷ 32768 → float |
| 2      | 2B      | `x`      | `int16_t` LE | |
| 4      | 2B      | `y`      | `int16_t` LE | |
| 6      | 2B      | `z`      | `int16_t` LE | |
| 8      | 1B      | `status` | `uint8_t`    | Bitmaska statusu fuzji (patrz niżej) |

**Bajt statusu fuzji:**

| Bit | Maska  | Nazwa             | Znaczenie |
|:---:|:------:|-------------------|-----------|
| 0   | `0x01` | `MARG`            | `1` = tryb MARG (mag+acc+gyro); `0` = IMU (acc+gyro only) |
| 1   | `0x02` | `MAG_ENABLED`     | `1` = magnetometr aktywny (próbka <2 s temu) |
| 2   | `0x04` | `MAG_CALIBRATED`  | `1` = skalibrowane próbki mag odbierane |
| 3   | `0x08` | `MAG_VALID`       | `1` = norma pola w zakresie 40–60 µT |
| 4   | `0x10` | `MAG_FRESH`       | `1` = ostatnia skalibrowana próbka <30 ms temu |

**Diagnostyka gdy `MARG=0`:**

| MAG_ENABLED | MAG_CALIBRATED | MAG_VALID | MAG_FRESH | Przyczyna |
|:-----------:|:--------------:|:---------:|:---------:|-----------|
| 0           | —              | —         | —         | Magnetometr wyłączony |
| 1           | 0              | —         | —         | Brak kalibracji magnetometru |
| 1           | 1              | 0         | —         | Zakłócenie pola (norma poza 40–60 µT) |
| 1           | 1              | 1         | 0         | Dane nieświeże (>30 ms) |

**Yaw z kwaternionu (tylko gdy `MARG=1` — odniesiony do północy magnetycznej):**
```
yaw_rad = atan2(2·(w·z + x·y),  1 − 2·(y² + z²))
```

```typescript
function parseQuaternionSample(view: DataView, offset: number) {
  const toFloat = (raw: number) => raw / 32768;
  return {
    w:      toFloat(view.getInt16(offset + 0, true)),
    x:      toFloat(view.getInt16(offset + 2, true)),
    y:      toFloat(view.getInt16(offset + 4, true)),
    z:      toFloat(view.getInt16(offset + 6, true)),
    status: view.getUint8(offset + 8),
  };
}

function getYawDeg(q: { w: number; x: number; y: number; z: number }): number {
  const yawRad = Math.atan2(2 * (q.w * q.z + q.x * q.y),
                            1 - 2 * (q.y * q.y + q.z * q.z));
  return yawRad * 180 / Math.PI;
}
```

---

## 7. Protokół Control (Charakterystyka a202 — WRITE + INDICATE)

Klient wysyła komendy **Write** na charakterystykę Control.
Urządzenie odpowiada **Indicate** na tę samą charakterystykę.

### Format SET (Klient → Urządzenie)

```
[transaction_id][module_id][1 = SET][key][value_0 .. value_n]
```

| Offset | Rozmiar | Pole             | Opis |
|:------:|:-------:|------------------|------|
| 0      | 1B      | `transaction_id` | Dowolna wartość 0–255, echo w odpowiedzi |
| 1      | 1B      | `module_id`      | ID modułu docelowego |
| 2      | 1B      | `command`        | `1` = SET |
| 3      | 1B      | `key`            | Klucz parametru |
| 4+     | 0–239B  | `value`          | Wartość (zależy od klucza) |

### Format SET Reply (Urządzenie → Klient)

```
[transaction_id][module_id][1][key][status]
```

| Offset | Rozmiar | Pole             | Opis |
|:------:|:-------:|------------------|------|
| 0–3    | 4B      | nagłówek         | Echo z komendy |
| 4      | 1B      | `status`         | Kod statusu (patrz niżej) |

### Format GET (Klient → Urządzenie)

```
[transaction_id][module_id][2 = GET][key]
```

Bez pola `value`.

### Format GET Reply (Urządzenie → Klient)

```
[transaction_id][module_id][2][key][status][value_0 .. value_n]
```

| Offset | Rozmiar | Pole             | Opis |
|:------:|:-------:|------------------|------|
| 0–3    | 4B      | nagłówek         | Echo z komendy |
| 4      | 1B      | `status`         | Kod statusu |
| 5+     | 0–239B  | `value`          | Wartość (tylko gdy `status = OK`) |

### Kody statusu Control

| Wartość | Nazwa              | Opis |
|:-------:|--------------------|------|
| `0x01`  | OK                 | Sukces |
| `0x02`  | UNKNOWN_ERROR      | Nieznany błąd |
| `0x03`  | NOT_CALIBRATED     | Brak kalibracji |
| `0x04`  | UNKNOWN_COMMAND    | Nieznana komenda |
| `0x05`  | UNSUPPORTED_KEY    | Klucz nieobsługiwany przez ten moduł |
| `0x06`  | INVALID_VALUE      | Nieprawidłowa wartość |
| `0x07`  | BUSY               | Moduł zajęty (np. kalibracja w toku) |
| `0x08`  | NOT_READY          | Nie gotowy (brak wymaganych danych) |
| `0x09`  | HARDWARE_ERROR     | Błąd sprzętowy |

---

## 8. Klucze Control — Uniwersalne (0x01–0x2F)

Identyczna semantyka niezależnie od modułu.

### Lifecycle (0x01–0x0F)

| Key    | Wartość | Typ     | Rozmiar value | Opis |
|--------|:-------:|---------|:-------------:|------|
| `KEY_CTRL_INFO`   | `0x01` | GET | — | Informacje o module: nazwa modelu + TLV obsługiwanych ODR/zakresów. |
| `KEY_CTRL_ENABLE` | `0x02` | SET/GET | 1B `uint8_t` | `1` = włącz moduł, `0` = wyłącz. |

**KEY_CTRL_INFO — format odpowiedzi:**
```
[model_name (null-terminated string)][TLV records...]
```
Każdy rekord TLV: `[key (1B)][count (1B)][value[0]..value[count-1] (2B each, uint16 LE)]`
Przykładowe klucze w TLV: `0x10` = dostępne ODR, `0x11` = dostępne zakresy Range 1, `0x12` = zakresy Range 2, `0x14` = dostępne bandwidth filter.

### Konfiguracja (0x10–0x1F)

| Key                      | Wartość | Typ     | Rozmiar value | Opis |
|--------------------------|:-------:|---------|:-------------:|------|
| `KEY_CFG_ODR`            | `0x10`  | SET/GET | 2B `uint16_t` LE | Częstotliwość próbkowania [Hz] |
| `KEY_CFG_MEASURE_RANGE_1`| `0x11`  | SET/GET | 2B `uint16_t` LE | Zakres pomiaru 1 (np. acc [g]) |
| `KEY_CFG_MEASURE_RANGE_2`| `0x12`  | SET/GET | 2B `uint16_t` LE | Zakres pomiaru 2 (np. gyro [dps]) |
| `KEY_CFG_OVERSAMPLING`   | `0x13`  | SET/GET | 2B `uint16_t` LE | Oversampling ratio |
| `KEY_CFG_FILTER`         | `0x14`  | SET/GET | 2B `uint16_t` LE | Bandwidth filtra [Hz] |
| `KEY_CFG_RAW_MODE`       | `0x15`  | SET/GET | 1B `uint8_t`     | `0` = dane kalibrowane, `1` = surowe. SET `0` bez kalibracji → NOT_READY. |

### Kalibracja (0x20–0x2F)

| Key                  | Wartość | Typ | Rozmiar value | Opis |
|----------------------|:-------:|-----|:-------------:|------|
| `KEY_CALIB_START`    | `0x20`  | SET | 0B            | Rozpocznij pomiar kalibracyjny. BUSY jeśli trwa. |
| `KEY_CALIB_ABORT`    | `0x21`  | SET | 0B            | Przerwij aktywny pomiar. |
| `KEY_CALIB_CAPTURE`  | `0x22`  | SET | —             | (Nieużywane przez większość modułów) |
| `KEY_CALIB_COMMIT`   | `0x23`  | SET | 0B            | Zatwierdź punkt — zapisz do NVS. NOT_READY jeśli brak danych. |
| `KEY_CALIB_GET_BLOB` | `0x24`  | GET | —             | Odczytaj blob kalibracyjny (rozmiar zależy od modułu) |
| `KEY_CALIB_SET_BLOB` | `0x25`  | SET | (rozmiar bloba) | Importuj blob kalibracyjny. Auto-save do NVS. |
| `KEY_CALIB_STATUS`   | `0x26`  | GET | —             | Status kalibracji (format zależy od modułu) |

---

## 9. Klucze Control — Moduł-specyficzne (0x80–0xFF)

Wartości 0x80+ mają różne znaczenia dla różnych modułów — rozróżnia je pole `module_id`.

### ACC_GYRO (module_id = 2)

| Key                        | Wartość | Typ | Rozmiar value | Opis |
|----------------------------|:-------:|-----|:-------------:|------|
| `KEY_AG_GET_TEMPERATURE`   | `0x80`  | GET | 2B reply      | Temperatura chipa IMU: `int16_t` LE [decidegC = 0.1°C] |
| `KEY_AG_DELETE_CALIB_POINT`| `0x81`  | SET | 2B            | Usuń punkt kalibracyjny: T = `int16_t` LE [decidegC]. Brak punktu → INVALID_VALUE. |

### GNSS (module_id = 5)

| Key                      | Wartość | Typ     | Rozmiar value | Opis |
|--------------------------|:-------:|---------|:-------------:|------|
| `KEY_GNSS_UPDATE_RATE`   | `0x80`  | SET/GET | 2B `uint16_t` LE | Częstotliwość aktualizacji [Hz] |
| `KEY_GNSS_DYNAMIC_MODEL` | `0x81`  | SET/GET | 1B `uint8_t`    | Model dynamiki odbiornika |

### TRIGGER (module_id = 8)

| Key                       | Wartość | Typ     | Opis |
|---------------------------|:-------:|---------|------|
| `KEY_TRIGGER_SENSITIVITY` | `0x80`  | SET/GET | Czułość triggera |
| `KEY_TRIGGER_MODE`        | `0x81`  | SET/GET | Tryb detekcji |
| `KEY_TRIGGER_BASES`       | `0x82`  | SET/GET | Baza pomiaru |

### MAGNETOMETER (module_id = 6)

| Key                   | Wartość | Typ     | Rozmiar value | Opis |
|-----------------------|:-------:|---------|:-------------:|------|
| `KEY_MAG_NORM_TOL`    | `0x80`  | SET/GET | 2B `uint16_t` LE | Tolerancja normy pola [nT]. Domyślnie 10000 (10 µT). Okno akceptacji: [norm_ref − tol, norm_ref + tol]. SET → NVS. |
| `KEY_MAG_NORM_REF`    | `0x81`  | GET     | 4B `uint32_t` LE | Bieżąca norma referencyjna [nT]. Domyślnie 50000 (50 µT). Aktualizowana automatycznie po imporcie kalibracji. |

### SELECTOR (module_id = 7)

| Key                        | Wartość | Typ     | Opis |
|----------------------------|:-------:|---------|------|
| `KEY_SELECTOR_HYSTERESIS`  | `0x80`  | SET/GET | Histereza detekcji pozycji (`uint8_t`, wartość Manhattan \|dy\|+\|dz\|) |
| `KEY_SELECTOR_RAW_DATA`    | `0x81`  | GET     | Bieżące surowe dane TMAG: `value[0]` = y (`int8_t`), `value[1]` = z (`int8_t`). Wymaga MODULE_ENABLE=true. |

### KICKBACK (module_id = 12)

| Key                   | Wartość | Typ     | Opis |
|-----------------------|:-------:|---------|------|
| `KEY_KICKBACK_TIMING` | `0x80`  | SET/GET | Parametry czasowe: `value[0]` = on_time_ms (`uint8_t`), `value[1]` = off_time_ms (`uint8_t`) |
| `KEY_KICKBACK_TEST`   | `0x81`  | SET     | Uruchom jeden cykl testowy (brak value). BUSY jeśli cykl w toku. |

### DISPLACEMENT (module_id = 13)

| Key               | Wartość | Typ     | Rozmiar value | Opis |
|-------------------|:-------:|---------|:-------------:|------|
| `KEY_DISP_RESET`  | `0x80`  | SET     | 0B            | Reset KF: pozycja → 0, prędkość → 0, acc_bias → 0. Nowe origin = bieżąca pozycja. |
| `KEY_DISP_SOFT_R` | `0x81`  | SET/GET | 2B `uint16_t` LE | Siła soft constraint prędkości. Jednostka: 0.1 m²/s². `0` = wyłączony. Domyślnie `40` (= 4.0 m²/s²). Większe → słabsza korekcja. Zapisywane do NVS. |

---

## 10. Kalibracja biasu żyroskopu (ACC_GYRO — wielopunktowa temperaturowa)

Firmware obsługuje do **10 punktów kalibracyjnych**, posortowanych rosnąco po temperaturze.
Interpolacja liniowa między punktami, ekstrapolacja poza zakres (slope z krawędziowych punktów).
Kalibracja jest przezroczysta — firmware automatycznie odejmuje bias od danych żyroskopu.

### Kiedy kalibrować

Mierz w różnych temperaturach (minimalna odległość: **5°C** między punktami, weryfikacja po stronie klienta).
Urządzenie powinno leżeć nieruchomo przez cały czas pomiaru (~20 s).

### Sekwencja dodania punktu

```
1. Ustabilizuj temperaturę
2. Control SET: KEY_CALIB_START (module_id=2, brak value)
3. Polluj Control GET: KEY_CALIB_STATUS co ~500 ms
   → sprawdź byte[1] (postęp 0–100%)
   → sprawdź byte[2-3] (bieżąca temperatura)
4. Gdy byte[1] = 100:
   Control SET: KEY_CALIB_COMMIT (module_id=2, brak value)
5. Control GET: KEY_CALIB_GET_BLOB → odśwież listę punktów w UI
```

### KEY_CALIB_STATUS — format odpowiedzi GET (4–16B)

| Offset | Rozmiar | Pole        | Opis |
|:------:|:-------:|-------------|------|
| 0      | 1B      | `count`     | Liczba zatwierdzonych punktów (0–10) |
| 1      | 1B      | `byte1`     | Jeśli pomiar aktywny: postęp 0–100%. Jeśli nieaktywny: `raw_mode` (0/1). |
| 2      | 2B      | `T_now`     | Temperatura chipa: `int16_t` LE [decidegC] |
| 4      | 12B     | `avg_bias`  | **Opcjonalne** — tylko gdy pomiar aktywny i są próbki. 3× `int32_t` LE [milli-°/s]: bias X, Y, Z |

### Blob kalibracyjny — format (164B)

Struktura `ag_calib_nvs_t` — serializacja do/z NVS przez GET_BLOB/SET_BLOB:

| Offset | Rozmiar | Pole         | Format     | Opis |
|:------:|:-------:|--------------|------------|------|
| 0      | 1B      | `count`      | `uint8_t`  | Liczba ważnych punktów (0–10) |
| 1      | 3B      | `reserved`   | —          | Ignorować |
| 4      | 160B    | `points[10]` | patrz niżej | Tablica 10 punktów (indices ≥ count zawierają zera) |

**Każdy punkt (`ag_bias_point_t`, 16B):**

| Offset w punkcie | Rozmiar | Pole     | Format        | Opis |
|:----------------:|:-------:|----------|---------------|------|
| 0                | 2B      | `T`      | `int16_t` LE  | Temperatura [decidegC] |
| 2                | 2B      | `_pad`   | —             | Wyrównanie — ignorować |
| 4                | 4B      | `bias_x` | `int32_t` LE  | Bias osi X [milli-°/s] |
| 8                | 4B      | `bias_y` | `int32_t` LE  | Bias osi Y [milli-°/s] |
| 12               | 4B      | `bias_z` | `int32_t` LE  | Bias osi Z [milli-°/s] |

Punkty zawsze posortowane rosnąco po `T`.

```typescript
interface GyroBiasPoint {
  tempDegC: number;
  biasX_dps: number;
  biasY_dps: number;
  biasZ_dps: number;
}

function parseGyroBiasBlob(data: DataView): GyroBiasPoint[] {
  const count = Math.min(data.getUint8(0), 10);
  const points: GyroBiasPoint[] = [];
  for (let i = 0; i < count; i++) {
    const base = 4 + i * 16;
    points.push({
      tempDegC:  data.getInt16(base, true) / 10,
      biasX_dps: data.getInt32(base + 4, true) / 1000,
      biasY_dps: data.getInt32(base + 8, true) / 1000,
      biasZ_dps: data.getInt32(base + 12, true) / 1000,
    });
  }
  return points;
}
```

### Usunięcie punktu kalibracyjnego

```
Control SET: KEY_AG_DELETE_CALIB_POINT (0x81), module_id=2
value[0-1] = T (int16_t LE, decidegC)
```

Przykład: usuń punkt 25.0°C (= 250 decidegC):
```
TX: [txId][0x02][0x01][0x81][0xFA][0x00]
RX: [txId][0x02][0x01][0x81][0x01]  ← OK
```

### Odczyt temperatury IMU (bez kalibracji)

```
TX: [txId][0x02][0x02][0x80]
RX: [txId][0x02][0x02][0x80][0x01][0xFA][0x00]  ← OK, 250 = 25.0°C
```

---

## 11. Kalibracja magnetometru (hard-iron + soft-iron)

Kalibracja wykonywana jest **po stronie klienta** (aplikacja mobilna).
Firmware tylko przechowuje i stosuje blob — nie ma interaktywnej sekwencji CALIB_START.

**Procedura kalibracji:**
1. Pobierz surowe dane mag (Control SET: `KEY_CFG_RAW_MODE = 1`)
2. Obracaj urządzenie w różnych kierunkach (opisuj sferę)
3. Oblicz hard-iron i soft-iron po stronie klienta (algorytm elipsoidy)
4. Wyślij blob: Control SET `KEY_CALIB_SET_BLOB` (42B)
5. Firmware zapamiętuje do NVS i wyłącza raw mode

### Blob kalibracyjny magnetometru — format (42B)

| Offset | Rozmiar | Pole             | Format       | Jednostka |
|:------:|:-------:|------------------|--------------|-----------|
| 0      | 2B      | `hard_iron_x`    | `int16_t` LE | deci-µT (1 LSB = 0.1 µT) |
| 2      | 2B      | `hard_iron_y`    | `int16_t` LE | deci-µT |
| 4      | 2B      | `hard_iron_z`    | `int16_t` LE | deci-µT |
| 6      | 4B      | `soft_iron[0][0]`| `float32` LE | bezwymiarowa |
| 10     | 4B      | `soft_iron[0][1]`| `float32` LE | bezwymiarowa |
| 14     | 4B      | `soft_iron[0][2]`| `float32` LE | bezwymiarowa |
| 18     | 4B      | `soft_iron[1][0]`| `float32` LE | bezwymiarowa |
| 22     | 4B      | `soft_iron[1][1]`| `float32` LE | bezwymiarowa |
| 26     | 4B      | `soft_iron[1][2]`| `float32` LE | bezwymiarowa |
| 30     | 4B      | `soft_iron[2][0]`| `float32` LE | bezwymiarowa |
| 34     | 4B      | `soft_iron[2][1]`| `float32` LE | bezwymiarowa |
| 38     | 4B      | `soft_iron[2][2]`| `float32` LE | bezwymiarowa |

Uwaga: hard-iron w blobie jest w **deci-µT** (`int16_t`), natomiast dane w ramce Data są w **nT** (`int24`) — to dwie różne reprezentacje.

Domyślne wartości (brak kalibracji): `hard_iron = {0, 0, 0}`, `soft_iron = macierz jednostkowa`.

```typescript
function buildMagCalibBlob(hard_iron_uT: [number,number,number], soft_iron: number[][]): ArrayBuffer {
  const buf = new ArrayBuffer(42);
  const v = new DataView(buf);
  v.setInt16(0, Math.round(hard_iron_uT[0] * 10), true); // µT → deci-µT
  v.setInt16(2, Math.round(hard_iron_uT[1] * 10), true);
  v.setInt16(4, Math.round(hard_iron_uT[2] * 10), true);
  for (let r = 0; r < 3; r++)
    for (let c = 0; c < 3; c++)
      v.setFloat32(6 + (r * 3 + c) * 4, soft_iron[r][c], true);
  return buf;
}
```

### CALIB_STATUS magnetometru — odpowiedź GET (1B)

| `value[0]` | Znaczenie |
|:----------:|-----------|
| `0`        | Brak kalibracji |
| `1`        | Skalibrowany, tryb calibrated |
| `2`        | Skalibrowany, tryb raw |

---

## 12. RTCM Stream (Charakterystyka a204 — Write Without Response)

Służy do przesyłania korekcji RTK do modemu GNSS (format RTCM 3.x).
Strumień należy fragmentować do rozmiarów MTU (max 512B/pakiet).
Urządzenie przekazuje dane bezpośrednio do odbiornika u-blox ZED-F9P przez UART.

---

## 13. Przykłady kompletnych transakcji

### Włączenie modułu Magnetometer

```
TX: [0x01][0x06][0x01][0x02][0x01]
     txId   mag   SET  ENABLE  1

RX: [0x01][0x06][0x01][0x02][0x01]
     txId   mag   SET  ENABLE  OK
```

### Odczyt statusu kalibracji magnetometru

```
TX: [0x02][0x06][0x02][0x26]
     txId   mag   GET  CALIB_STATUS

RX: [0x02][0x06][0x02][0x26][0x01][0x01]
     txId   mag   GET  CALIB_STATUS  OK  calibrated
```

### Odczyt bloba kalibracji żyroskopu

```
TX: [0x03][0x02][0x02][0x24]
     txId   ag   GET  CALIB_GET_BLOB

RX: [0x03][0x02][0x02][0x24][0x01][164 bajty danych]
     txId   ag   GET  CALIB_GET_BLOB  OK  blob
```

### Rozpoczęcie pomiaru kalibracyjnego żyroskopu

```
TX: [0x04][0x02][0x01][0x20]
     txId   ag   SET  CALIB_START   (brak value)

RX: [0x04][0x02][0x01][0x20][0x01]
     txId   ag   SET  CALIB_START   OK
```

### Polling statusu podczas kalibracji

```
TX: [0x05][0x02][0x02][0x26]

RX: [0x05][0x02][0x02][0x26][0x01][0x02][0x01][0x32][0xF4][01 02 03 04 05 06 07 08 09 0A 0B 0C]
                                          count  prog  T_now(little-endian)    avg_bias (12B)
     count=2 istniejące punkty, progress=1%, T=0x01F4=500 decidegC=50.0°C, avg_bias opcjonalne
```

### Zatwierdzenie punktu kalibracyjnego

```
TX: [0x06][0x02][0x01][0x23]
     txId   ag   SET  CALIB_COMMIT  (brak value)

RX: [0x06][0x02][0x01][0x23][0x01]
     txId   ag   SET  CALIB_COMMIT  OK
```
