{"id":50551868,"url":"https://github.com/supermagnum/gr-ident","last_synced_at":"2026-06-04T04:02:47.579Z","repository":{"id":360752824,"uuid":"1249489592","full_name":"Supermagnum/gr-ident","owner":"Supermagnum","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-27T19:24:42.000Z","size":25,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T19:26:36.203Z","etag":null,"topics":["gnuradio","mode","preamble","radio","sdr"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Supermagnum.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T18:55:12.000Z","updated_at":"2026-05-27T19:24:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Supermagnum/gr-ident","commit_stats":null,"previous_names":["supermagnum/gr-ident"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/Supermagnum/gr-ident","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Supermagnum%2Fgr-ident","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Supermagnum%2Fgr-ident/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Supermagnum%2Fgr-ident/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Supermagnum%2Fgr-ident/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Supermagnum","download_url":"https://codeload.github.com/Supermagnum/gr-ident/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Supermagnum%2Fgr-ident/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33888302,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gnuradio","mode","preamble","radio","sdr"],"created_at":"2026-06-04T04:02:46.187Z","updated_at":"2026-06-04T04:02:47.569Z","avatar_url":"https://github.com/Supermagnum.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gr-ident, a Radio Mode Identification Preamble for Gnuradio — Specification\n\n\u003e **AI DISCLAIMER**: This specification was developed with the assistance of AI (Claude by Anthropic).\n\u003e GNU Radio 4.x is designed with AI-assisted development in mind, making it particularly suitable\n\u003e for implementing this specification. The specification has not been reviewed by professional\n\u003e engineers. Use at your own risk.\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Installation](#installation)\n- [Documentation](#documentation)\n- [Design Goals](#design-goals)\n- [Preamble Structure](#preamble-structure)\n- [Modulation Profiles](#modulation-profiles)\n- [Golay(24,12) Protection](#golay2412-protection)\n- [12-Bit Field Layout](#12-bit-field-layout)\n- [Mode ID Table](#mode-id-table)\n  - [Reserved](#reserved)\n  - [Analog Modes](#analog-modes)\n  - [Digital Modes](#digital-modes)\n  - [Image and Television Modes](#image-and-television-modes)\n  - [Satellite Modes](#satellite-modes)\n  - [Experimental Range](#experimental-range)\n- [Receiver Behavior](#receiver-behavior)\n  - [Two-Layer Identification](#two-layer-identification)\n  - [Classifier Fallback Hierarchy](#classifier-fallback-hierarchy)\n- [Interaction with LDPC](#interaction-with-ldpc)\n- [Security Integration — gr-linux-crypto](#security-integration--gr-linux-crypto)\n- [GNU Radio 4.x OOT module](#gnu-radio-4x-oot-module)\n- [IQ Validation — radio-modulation-validator](#iq-validation--radio-modulation-validator)\n  - [Two-Layer Validation](#two-layer-validation)\n  - [Neural Network Classifiers](#neural-network-classifiers)\n  - [INT8 Quantisation and NPU Deployment](#int8-quantisation-and-npu-deployment)\n  - [Classifier Fallback at Runtime](#classifier-fallback-at-runtime)\n- [Future Work](#future-work)\n- [License](#license)\n\n---\n\n## Overview\n\nThis document specifies **gr-ident**, an open standard for a lightweight radio mode\nidentification preamble for use in amateur and experimental software-defined radio systems.\nThe specification is published freely, may be implemented by anyone without licensing\nrequirements, and is intended for open, interoperable use across receivers, transmitters,\nand software-defined radio tooling.\n\nThe preamble allows a receiver to identify the incoming signal mode — analog or digital —\nbefore committing to a demodulator, without relying on statistical signal classification\nmethods such as machine learning. Mode identification on the air is by **Golay-decoded\nmode ID** in the preamble, not by a neural network.\n\nAn **optional** neural-network classifier from\n[radio-modulation-validator](https://github.com/Supermagnum/radio-modulation-validator)\n(rmv) may run in parallel for validation or repeater fallback. It classifies **modulation\nfamily and order** (for example FM / NBFM_25 or FSK / DMR), not the full 9-bit gr-ident\nmode ID table. Preamble routing always takes precedence when a valid preamble is present.\nSee [Neural Network Classifiers](#neural-network-classifiers).\n\nThe preamble is self-contained, protected by forward error correction, and is entirely\ntransparent to any downstream LDPC decoder. It is designed to be decodable on modest\nCPU hardware with no GPU requirement.\n\nFor signals that carry a gr-ident preamble, identification is deterministic and\nmicrosecond-latency. For signals without a preamble (conventional legacy radios,\ninterference), an optional neural network classifier can identify the modulation family\nand order independently. The two layers are complementary — the preamble controls routing,\nthe classifier verifies it.\n\n---\n\n## Installation\n\ngr-ident is **not** in Debian/Ubuntu apt. Use apt for dependencies, then **compile** and\n**install** from a git checkout. Full tester workflows are in [`TESTING.md`](TESTING.md).\n\n### 1. Dependencies (apt)\n\n```bash\nsudo apt update\nsudo apt install -y \\\n  python3 python3-zmq \\\n  meson ninja-build build-essential \\\n  cmake pkg-config g++-14 \\\n  libzmq3-dev\n```\n\nIf `g++-14` is unavailable on your release, omit it (CMake falls back to `g++` from\n`build-essential`; C++23 is required for the GNU Radio 4 plugin).\n\nInstall [GNU Radio 4.x](https://wiki.gnuradio.org/) separately (not an apt package here).\nThese docs use prefix `/opt/gnuradio4-gcc`.\n\n### 2. Source\n\n```bash\ngit clone https://github.com/Supermagnum/gr-ident.git\ncd gr-ident\n```\n\n### 3. Configure\n\n```bash\ncmake -B build-gr4 \\\n  -DCMAKE_PREFIX_PATH=/opt/gnuradio4-gcc \\\n  -DCMAKE_INSTALL_PREFIX=/opt/gnuradio4-gcc\n```\n\n`CMAKE_INSTALL_PREFIX` defaults to the GNU Radio prefix when unset. Use the same path for\nboth variables so plugins, binaries, and Python land in one tree.\n\n### 4. Compile\n\n```bash\ncmake --build build-gr4 -j\"$(nproc)\"\n```\n\nOptional checks before install:\n\n```bash\nctest --test-dir build-gr4 --output-on-failure\nmeson setup build \u0026\u0026 meson test -C build\nPYTHONPATH=python python3 -m unittest discover -s python/tests -v\n```\n\n### 5. Install\n\n```bash\nsudo cmake --install build-gr4\n```\n\nThis installs into `CMAKE_INSTALL_PREFIX` (example layout):\n\n| Path under prefix | Contents |\n|---|---|\n| `lib/gnuradio-4/plugins/` | `GrIdentBlocksShared`, `GrIdentZmqBlocksShared` |\n| `include/gnuradio-4.0/` | Block headers |\n| `bin/` | `grident_receive_flowgraph`, `grident_run_flowgraph`, `grident_ptt_zmq_smoke`, `grident_iq_test.py`, `grident_validate.py` |\n| `share/gr-ident/python/` | Python `grident` package |\n| `share/gr-ident/gr-ident-env.sh` | Sets `PYTHONPATH` and `PATH` for installed tools |\n\nWithout `libzmq3-dev` at configure time, ZeroMQ plugins and `grident_ptt_zmq_smoke` are not built.\n\n### 6. Use the installation\n\n```bash\nsource /opt/gnuradio4-gcc/share/gr-ident/gr-ident-env.sh\ngrident_receive_flowgraph \\\n  python/tests/fixtures/common_modes/mode_110.cf32 110\n```\n\nAdd the `source` line to your shell profile for a permanent setup.\n\n### Develop from a build tree (no install)\n\nRun from the repository without step 5:\n\n```bash\nexport PYTHONPATH=\"$(pwd)/python\"\n./build-gr4/grident_receive_flowgraph \\\n  python/tests/fixtures/common_modes/mode_110.cf32 110\n```\n\n### Optional components\n\n| Component | When | Purpose |\n|---|---|---|\n| **ImageMagick** | `sudo apt install -y imagemagick` | Regenerate `docs/` waterfall PNGs |\n| **radio-modulation-validator** | Sibling clone or `PATH` | Signal-layer IQ validation ([rmv](https://github.com/Supermagnum/radio-modulation-validator)) |\n\nPreamble validation does not require rmv:\n\n```bash\ngrident_validate.py --preamble-only\n```\n(after sourcing `gr-ident-env.sh`, or `PYTHONPATH=python` from the source tree)\n\n---\n\n## Documentation\n\nGenerated test documentation, IQ capture details, waterfall plots, and regression results:\n\n- [docs/README.md](docs/README.md) — documentation index and plot parameters\n- [docs/test-results.md](docs/test-results.md) — unit test log and IQ roundtrip matrix\n- [docs/modulation-captures.md](docs/modulation-captures.md) — per-mode air interfaces, capture durations, and spectrograms\n- [docs/codechart.md](docs/codechart.md) — code and test function map (debug reference)\n- [blocklib/grident/blocks/README.md](blocklib/grident/blocks/README.md) — GNU Radio 4.x block build\n- [TESTING.md](TESTING.md) — tester onboarding and smoke tests\n- [docs/zeromq-protocol.md](docs/zeromq-protocol.md) — LinHT and gr-ident ZeroMQ wire formats and mode examples\n- [docs/gateway-integration.md](docs/gateway-integration.md) — VoIP gateway adapters, ZeroMQ, gr-linux-crypto\n- [docs/rmv-integration.md](docs/rmv-integration.md) — two-layer IQ validation via radio-modulation-validator\n- [docs/npu-deployment.md](docs/npu-deployment.md) — INT8 quantisation and SpacemiT NPU deployment\n- [apps/flowgraphs/zmq-distributed-demo.md](apps/flowgraphs/zmq-distributed-demo.md) — ZeroMQ distributed edges\n\nRegenerate with:\n\n```bash\nPYTHONPATH=python python3 python/grident/generate_docs.py\n```\n\n---\n\n## Design Goals\n\n- Identify the incoming signal mode before demodulation begins\n- Distinguish analog from digital signals at the earliest possible point\n- Protect the mode identifier with FEC decodable at low SNR on any CPU\n- Never interfere with or confuse downstream LDPC decoders\n- Support up to 500 assigned modes with room for growth\n- Remain simple enough to implement in GNU Radio 4.x without specialist hardware\n- Carry minimal but useful metadata alongside the mode ID\n- Complement (not replace) neural network signal classification for signals\n  without a preamble\n\n---\n\n## Preamble Structure\n\nThe preamble consists of a single Golay(24,12) codeword transmitted before the main\nframe payload. It occupies a fixed number of symbols at the start of each transmission\nand is consumed entirely before the downstream decoder sees any data.\n\n```\n+------------------+-----------------------------+\n|  PREAMBLE        |  MAIN FRAME PAYLOAD         |\n|  24 bits         |  (LDPC or other FEC)        |\n|  Golay(24,12)    |  Decoder starts here only   |\n+------------------+-----------------------------+\n```\n\nThe boundary between the preamble and the main frame is fixed and defined by the\nmodulation in use. The downstream decoder is started only after the preamble has been\nfully consumed and validated.\n\nA known synchronization sequence (correlatable without carrier lock or symbol timing\nrecovery) must precede the preamble to allow detection at negative SNR. Sync sequences\nand physical layer parameters are **modulation-specific** and defined in\n[Modulation Profiles](#modulation-profiles).\n\n---\n\n## Modulation Profiles\n\nEach mode uses a defined air interface for transmitting the sync sequence and 24-bit\nGolay-protected preamble. IQ test vectors and conforming implementations must use the\nprofile assigned to that mode ID.\n\nCommon physical layer parameters for narrowband digital profiles:\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Symbol rate | 4800 sym/s | ETSI TS 102 490, Yaesu C4FM |\n| 4-FSK deviations | ±648 Hz, ±1944 Hz | ETSI TS 102 490-1 |\n| Sample rate (test vectors) | 48000 Hz | 10 samples/symbol |\n\n### Profile: `nfm_125_4800`\n\nUsed by **NFM 12.5 kHz** (mode 20) and **EchoLink** (mode 110, FM gateway path).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Channel | 12.5 kHz NFM | ETSI EN 300 113 |\n| Data burst | 4800 sym/s CPFSK 4-FSK | ETSI TS 102 490 deviations |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_nfm)) |\n\n### Profile: `nfm_125_ctcss_4800`\n\nUsed by **NFM 12.5 kHz + CTCSS** (mode 30).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Base | `nfm_125_4800` | |\n| CTCSS tone | 88.5 Hz | EIA/TIA-603 |\n| CTCSS deviation | 500 Hz | Typical repeater practice |\n| Preamble | Clean 4-FSK burst (no CTCSS on preamble) | gr-ident rule |\n| Payload | CTCSS-only carrier for remainder of 3 s body | Simulates voice segment |\n\n### Profile: `nfm_125_dcs_4800`\n\nUsed by **NFM 12.5 kHz + DCS** (mode 40).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Base | `nfm_125_4800` | |\n| DCS rate | 134.4 bit/s | ETSI TS 103 236 |\n| DCS shift | ±134 Hz | ETSI TS 103 236 |\n| Test code | 023 | Standard test codeword |\n| Preamble | Clean 4-FSK burst (no DCS on preamble) | gr-ident rule |\n| Payload | DCS-only carrier for remainder of 3 s body | Simulates voice segment |\n\n### Profile: `c4fm_4800`\n\nUsed by **C4FM / Fusion** (mode 104).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | C4FM 4-FSK | Yaesu System Fusion |\n| Symbol rate | 4800 sym/s | |\n| Sync | 24-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_c4fm)) |\n\n### Profile: `dpmr_4800`\n\nUsed by **dPMR** (mode 108).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | 4-FSK | ETSI TS 102 490-1 |\n| Symbol rate | 4800 sym/s | |\n| Sync | 24-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_dpmr)) |\n\n### Profile: `psk31_3125`\n\nUsed by **PSK31** (mode 158).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | BPSK | PSK31 amateur digital mode |\n| Symbol rate | 31.25 baud | PSK31 standard |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_psk31)) |\n\n### Profile: `rtty_50`\n\nUsed by **RTTY** (mode 159).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | 2-FSK | ITA2 radioteletype |\n| Symbol rate | 50 baud | Common amateur RTTY rate |\n| Frequency shift | 170 Hz (mark +85 Hz, space -85 Hz) | ITA2 audio shift |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_rtty)) |\n\n### Profile: `dmr_4800`\n\nUsed by **DMR** modes 100-102, 106, and 109 (test-vector placeholder).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | 4-FSK | ETSI TS 102 361 |\n| Symbol rate | 4800 sym/s | |\n| Sync | 24-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_dmr)) |\n\n### Profile: `nxdn_4800`\n\nUsed by **NXDN** (mode 107).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | 4-FSK | NXDN common air interface |\n| Symbol rate | 4800 sym/s | |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_nxdn)) |\n\n### Profile: `m17_4800`\n\nUsed by **M17** modes 120 and 121.\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | 4-FSK | M17 open digital voice |\n| Symbol rate | 4800 sym/s | |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_m17)) |\n\n### Profile: `dstar_4800`\n\nUsed by **D-STAR** (mode 103) and **D-STAR Reflector** (mode 115). Test vectors use\n4-FSK at 4800 sym/s as a correlatable placeholder for gateway paths.\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | 4-FSK (test vectors) | D-STAR digital voice |\n| Symbol rate | 4800 sym/s | |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_dstar)) |\n\n### Profile: `ax25_1200`\n\nUsed by **AX.25** (mode 150) and **APRS** (mode 151).\n\n| Parameter | Value | Reference |\n|---|---|---|\n| Modulation | Bell 202 AFSK | AX.25 amateur packet |\n| Symbol rate | 1200 baud | |\n| Sync | 16-bit sequence | gr-ident assigned (see [Sync Sequences](docs/sync-sequences.md#sync_ax25)) |\n\nMode ID to profile mapping for assigned modes is implemented in\n`python/grident/modulation/registry.py`.\n\nThe gr-ident preamble is always a clean data burst at the start of a\ntransmission. For NFM modes with CTCSS or DCS, test vectors append a\nsquelch-tone payload after the preamble for the remainder of the **3 second**\nmodulated body; decoders must not apply CTCSS/DCS compensation to the preamble\nsymbols themselves.\n\nAll IQ test vectors use this layout:\n\n```\n[ 1 s silence ] [ 3 s modulated body ] [ 1 s silence ]\n```\n\nThe modulated body begins with the sync + Golay preamble burst. The remainder\nis profile-specific payload (CTCSS/DCS tone, idle carrier, RTTY mark, and so on).\n\n---\n\n## Golay(24,12) Protection\n\nThe 12 data bits of the preamble field are encoded using a Golay(24,12) perfect binary\nlinear code, producing 24 bits for transmission.\n\nProperties:\n\n- **Data bits**: 12\n- **Encoded bits**: 24\n- **Minimum Hamming distance**: 8\n- **Error correction**: up to 3 bit errors per codeword\n- **Error detection**: up to 7 bit errors per codeword\n- **Decoding complexity**: trivial on any CPU, microsecond latency\n- **No interaction** with downstream LDPC or convolutional decoders\n\nThe generator polynomial and encoding/decoding procedure follow the standard\nGolay(24,12) definition as used in, for example, the M17 protocol LICH channel.\n\n---\n\n## 12-Bit Field Layout\n\nThe 12 data bits protected by the Golay codeword are allocated as follows:\n\n```\nBit 11        Bit 10        Bit 9              Bits 8–0\n+-----------+-----------+------------------+------------------+\n| Analog /  | Encrypted /| Metadata present | Mode ID          |\n| Digital   | Open       | (optional ext.)  | (9 bits, 0–511)  |\n+-----------+-----------+------------------+------------------+\n```\n\n| Field | Bits | Description |\n|---|---|---|\n| Mode ID | 9 (bits 0–8) | Identifies the specific mode (0–511) |\n| Metadata present | 1 (bit 9) | 0 = primary preamble only; 1 = secondary metadata codeword follows on air |\n| Encrypted / Open | 1 (bit 10) | 0 = open / unencrypted, 1 = encrypted content |\n| Analog / Digital | 1 (bit 11) | 0 = analog mode, 1 = digital mode |\n\nThe **Analog / Digital flag** (bit 11) is the most significant bit, allowing a receiver\nto make the analog/digital routing decision immediately from the MSB alone, without\nneeding to decode the full Mode ID. This is the minimum useful action a receiver can\ntake upon preamble detection.\n\nThe **Encrypted / Open flag** (bit 10) signals whether the payload content is encrypted.\nNote that encryption may be subject to regulatory restrictions depending on jurisdiction\nand frequency band. Users are responsible for compliance with applicable regulations.\n\n---\n\n## Optional Secondary Metadata Field\n\nWhen bit 9 (**Metadata present**) is set in the primary preamble field, a second\nGolay(24,12) codeword is transmitted immediately after the primary codeword (still\nbefore the main payload). The on-air order is:\n\n```\n[ sync ] [ primary Golay 24 bits ] [ secondary Golay 24 bits ] [ payload ... ]\n```\n\nThe 12 data bits of the secondary field are allocated as follows:\n\n```\nBit 11–8       Bit 7–4        Bit 3–0\n+-------------+-------------+--------------+\n| Bandwidth   | Codec param | Callsign     |\n| code        | (4 bits)    | nibble       |\n+-------------+-------------+--------------+\n```\n\n| Field | Bits | Description |\n|---|---|---|\n| Bandwidth code | 4 (bits 11–8) | Channel bandwidth hint (see table below) |\n| Codec param | 4 (bits 7–4) | Mode-specific sub-codec or rate index (0–15) |\n| Callsign nibble | 4 (bits 3–0) | Upper four bits of CRC-16-CCITT of callsign (0 if none) |\n\n| Bandwidth code | Meaning |\n|---|---|\n| 0 | Unspecified |\n| 1 | 6.25 kHz |\n| 2 | 8.33 kHz |\n| 3 | 12.5 kHz |\n| 4 | 20 kHz |\n| 5 | 25 kHz |\n| 6 | WFM / wide |\n| 7–15 | Reserved |\n\nImplementations: `python/grident/metadata_field.py`, `blocklib/grident/lib/metadata_field.cc`,\nGNU Radio 4.x blocks `MetadataEncode` and `MetadataDecode`.\n\nReceivers that do not implement the secondary field must still decode the primary\npreamble when bit 9 = 0. When bit 9 = 1, conforming receivers should decode both\ncodewords before routing to the payload demodulator.\n\n---\n\n## Mode ID Table\n\n### Reserved\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 0 | 0x000 | Null / unassigned |\n| 511 | 0x1FF | Test / loopback |\n\n---\n\n### Analog Modes\n*(Bit 11 = 0)*\n\n#### AM Variants\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 1 | 0x001 | AM — Amplitude Modulation (standard) |\n| 2 | 0x002 | AM-DSB — Double Sideband |\n| 3 | 0x003 | AM-SSB — Single Sideband (generic) |\n| 4 | 0x004 | USB — Upper Sideband |\n| 5 | 0x005 | LSB — Lower Sideband |\n| 6 | 0x006 | DSB-SC — Double Sideband Suppressed Carrier |\n\n#### FM — Wide\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 10 | 0x00A | WFM — Wideband FM, broadcast (200 kHz) |\n| 11 | 0x00B | WFM Stereo — Wideband FM with stereo pilot |\n| 12 | 0x00C | FM 25 kHz — Standard FM, 25 kHz channel spacing |\n| 13 | 0x00D | FM 20 kHz — FM, 20 kHz channel spacing |\n\n#### NFM — Narrowband FM, no tone squelch\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 20 | 0x014 | NFM 12.5 kHz — Standard narrowband FM |\n| 21 | 0x015 | NFM 8.33 kHz — Aviation narrowband FM |\n| 22 | 0x016 | NFM 6.25 kHz — Very narrow FM |\n\n#### NFM — with CTCSS (Continuous Tone-Coded Squelch System)\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 30 | 0x01E | NFM 12.5 kHz + CTCSS |\n| 31 | 0x01F | NFM 8.33 kHz + CTCSS |\n| 32 | 0x020 | NFM 6.25 kHz + CTCSS |\n| 33 | 0x021 | FM 25 kHz + CTCSS |\n| 34 | 0x022 | FM 20 kHz + CTCSS |\n\n#### NFM — with DCS (Digital-Coded Squelch)\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 40 | 0x028 | NFM 12.5 kHz + DCS |\n| 41 | 0x029 | NFM 8.33 kHz + DCS |\n| 42 | 0x02A | NFM 6.25 kHz + DCS |\n| 43 | 0x02B | FM 25 kHz + DCS |\n| 44 | 0x02C | FM 20 kHz + DCS |\n\n#### NFM — with CTCSS and DCS combined\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 50 | 0x032 | NFM 12.5 kHz + CTCSS + DCS |\n| 51 | 0x033 | NFM 8.33 kHz + CTCSS + DCS |\n| 52 | 0x034 | NFM 6.25 kHz + CTCSS + DCS |\n\n#### Other Analog\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 60 | 0x03C | CW — Continuous Wave (Morse) |\n| 61 | 0x03D | CW Narrow |\n| 62 | 0x03E | NBAM — Narrowband AM |\n| 63 | 0x03F | ECSS — Exalted Carrier Selectable Sideband |\n\n---\n\n### Digital Modes\n*(Bit 11 = 1)*\n\n#### Voice — Proprietary Codec\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 100 | 0x064 | DMR — Digital Mobile Radio (ETSI TS 102 361) |\n| 101 | 0x065 | DMR Tier II — Conventional |\n| 102 | 0x066 | DMR Tier III — Trunked |\n| 103 | 0x067 | D-STAR — Digital Smart Technologies for Amateur Radio |\n| 104 | 0x068 | C4FM / Fusion — Yaesu System Fusion |\n| 105 | 0x069 | P25 Phase 1 — APCO Project 25 (C4FM) |\n| 106 | 0x06A | P25 Phase 2 — APCO Project 25 (TDMA) |\n| 107 | 0x06B | NXDN — Icom / Kenwood narrowband digital |\n| 108 | 0x06C | dPMR — Digital Private Mobile Radio |\n| 109 | 0x06D | TETRA — Terrestrial Trunked Radio |\n\n#### Voice Linking / VoIP Gateways\n\nThese mode IDs identify internet voice-linking services. They apply when the transmission\ncarries linked voice — for example after RF demodulation at a repeater or gateway, or when\nrouting voice through an IP bridge rather than a distinct over-the-air codec.\n\ngr-ident **identifies** the link type on the RF preamble; it does **not** implement EchoLink,\nIRLP, AllStar, Mumble, Wires-X, or D-STAR reflector protocols. A separate **gateway adapter**\nbridges decoded mode IDs and audio/PTT to the VoIP stack. See\n[Gateway integration reference](docs/gateway-integration.md) for ZeroMQ wiring and\n[gr-linux-crypto](https://github.com/Supermagnum/gr-linux-crypto) hooks.\n\n| Mode ID | Hex | Description | gr-ident RF profile (typical) | Gateway software |\n|---:|---|---|---|---|\n| 110 | 0x06E | EchoLink — node, conference, or repeater link | `nfm_125_4800` | [SvxLink](https://github.com/sm0svx/svxlink) |\n| 111 | 0x06F | IRLP — Internet Radio Linking Project | Operator-defined (often NFM) | IRLP node |\n| 112 | 0x070 | AllStar Link — Asterisk / app_rpt | Operator-defined (often NFM) | [ASL3](https://github.com/allstarlink/asl3) |\n| 113 | 0x071 | Mumble — open-source VoIP (e.g. KA-Node) | Operator-defined | Mumble / [QRadioLink](https://qradiolink.org/) |\n| 114 | 0x072 | Wires-X — Yaesu internet linking (link node) | `c4fm_4800` if digital RF | Yaesu Wires-X / MMDVM bridges |\n| 115 | 0x073 | D-STAR Reflector — DCS / REF / XRF | D-STAR sync (`sync_dstar`) | [xlxd](https://github.com/n7tae/new-xlxd) / [urfd](https://github.com/n7tae/urfd) |\n\n**Integration buses (gr-ident side):**\n\n- **Receive:** subscribe to preamble JSON on `tcp://127.0.0.1:5560` (topic `grident`) after\n  Golay decode; route on `mode_id`; if `encrypted` is true, invoke gr-linux-crypto before\n  forwarding audio to the gateway ([ZeroMQ protocol](docs/zeromq-protocol.md)).\n- **Transmit:** publish LinHT PMT `SOT`/`EOT` on `ipc:///tmp/ptt_msg` or gr-ident multipart\n  PTT on `:5561`; GR `PreambleOnPtt` inserts the burst for the configured linking mode ID.\n\nThere are no mature GNU Radio OOT modules that speak these VoIP protocols end-to-end.\nDocumented patterns use **UDP/ALSA/USRP audio** between GNU Radio and SvxLink, AllStar, or\nMumble, with gr-ident handling RF identification only.\n\n#### Voice — Open Codec\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 120 | 0x078 | M17 — Open digital voice, Codec2 3200 |\n| 121 | 0x079 | M17 — Codec2 1600 |\n| 122 | 0x07A | FreeDV 700D |\n| 123 | 0x07B | FreeDV 1600 |\n| 124 | 0x07C | FreeDV 2020 |\n| 125 | 0x07D | Codec2 2400 bps (raw, no framing) |\n| 126 | 0x07E | Codec2 1200 bps (raw, no framing) |\n| 127 | 0x07F | Opus (raw, no framing) |\n\n#### Data / Packet\n\nSlot-synchronized weak-signal modes (FT8, FT4, JS8Call, and similar) are intentionally\nomitted. Their fixed transmit windows leave no room for a gr-ident preamble without\nbreaking protocol timing.\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 150 | 0x096 | AX.25 — Amateur packet radio |\n| 151 | 0x097 | APRS — Automatic Packet Reporting System |\n| 152 | 0x098 | VARA HF |\n| 153 | 0x099 | VARA FM |\n| 154 | 0x09A | Winlink |\n| 158 | 0x09E | PSK31 |\n| 159 | 0x09F | RTTY — Radioteletype |\n\n#### Broadband / Multi-carrier\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 180 | 0x0B4 | OFDM — Generic OFDM (parameters in metadata) |\n| 181 | 0x0B5 | COFDM — Coded OFDM |\n\n---\n\n### Image and Television Modes\n*(Bit 11 = 1)*\n\n#### SSTV — Slow Scan Television (Analog Image)\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 210 | 0x0D2 | SSTV — Martin M1 |\n| 211 | 0x0D3 | SSTV — Martin M2 |\n| 212 | 0x0D4 | SSTV — Scottie S1 |\n| 213 | 0x0D5 | SSTV — Scottie S2 |\n| 214 | 0x0D6 | SSTV — Scottie DX |\n| 215 | 0x0D7 | SSTV — Robot 36 |\n| 216 | 0x0D8 | SSTV — Robot 72 |\n| 217 | 0x0D9 | SSTV — PD 90 |\n| 218 | 0x0DA | SSTV — PD 120 |\n| 219 | 0x0DB | SSTV — PD 160 |\n| 220 | 0x0DC | SSTV — PD 180 |\n| 221 | 0x0DD | SSTV — PD 240 |\n| 222 | 0x0DE | SSTV — Wraase SC2-120 |\n| 223 | 0x0DF | SSTV — Wraase SC2-180 |\n\n#### DSSTV / Digital Image\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 230 | 0x0E6 | DSSTV — Digital Slow Scan Television |\n| 231 | 0x0E7 | EasyPal — HF digital image (DRM-based) |\n| 232 | 0x0E8 | FSSTV — Fast Scan ATV (narrowband digital) |\n| 233 | 0x0E9 | FAX — HF radiofax / weatherfax |\n\n#### ATV — Amateur Television\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 240 | 0x0F0 | ATV — Analog amateur television (wideband) |\n| 241 | 0x0F1 | ATV — NTSC |\n| 242 | 0x0F2 | ATV — PAL |\n| 243 | 0x0F3 | DATV — Digital ATV (DVB-S) |\n| 244 | 0x0F4 | DATV — Digital ATV (DVB-S2) |\n| 245 | 0x0F5 | DATV — Digital ATV (DVB-T) |\n\n---\n\n### Satellite Modes\n*(Bit 11 = 1)*\n\nModes in this section cover amateur satellite **uplink** use — signals a station transmits\nthrough a transponder. Receive-only downlinks (telemetry beacons, weather imaging,\nnavigation, ADS-B, and similar) are intentionally omitted; they cannot be influenced by\nthe transmitting station and are out of scope for gr-ident.\n\n#### Satellite Voice and Data\n\n| Mode ID | Hex | Description |\n|---|---|---|\n| 260 | 0x104 | Linear transponder — SSB/CW uplink/downlink |\n| 261 | 0x105 | FM Satellite — Standard FM voice repeater |\n| 262 | 0x106 | FM Satellite + CTCSS — FM with access tone |\n| 264 | 0x108 | Es'hail-2 / QO-100 — NB transponder SSB |\n| 265 | 0x109 | Es'hail-2 / QO-100 — WB transponder DATV |\n\n---\n\n### Experimental Range\n\nMode IDs 300–498 are reserved for experimental and user-defined modes. Assignments\nwithin this range are recorded in the public\n[experimental mode registry](docs/experimental-mode-registry.md)\n(`registry/experimental-modes.json`).\n\n| Range | Hex Range | Description |\n|---|---|---|\n| 300 | 0x12C | [Sleipnir](https://github.com/Supermagnum/gr-sleipnir) — 8-carrier QPSK digital voice (see [registry](docs/experimental-mode-registry.md)) |\n| 301–498 | 0x12D–0x1F2 | Experimental / user-defined (unassigned) |\n| 499 | 0x1F3 | Experimental range upper boundary marker |\n| 500–510 | 0x1F4–0x1FE | Reserved for future standardization |\n| 511 | 0x1FF | Test / loopback |\n\n---\n\n## Receiver Behavior\n\n### Two-Layer Identification\n\nA conforming receiver combines two independent identification methods. The preamble\ncontrols routing — the classifier verifies it. Neither depends on the other.\n\n```\nSignal received\n        │\n        ▼  (microseconds — always runs)\nSync sequence detected\n        │\n        ▼\nGolay(24,12) decode\n        │\n   ─────┴─────\n   │           │\npass        \u003e 3 bit errors\n   │           │\n   ▼           ▼\nExtract     Discard / squelch\nbit 11          │\n(A/D flag)      └──► classifier-only path (if available)\n   │\n   ▼\nRoute to analog or digital demodulator bank  ← immediate, never blocked\n   │\n   ▼  (milliseconds — advisory, asynchronous)\nExtract mode_id (bits 0–8)\nExtract encrypted flag (bit 10)\n        │\n        ▼\nNeural network classifier cross-checks signal\n  → agrees: high confidence, proceed normally\n  → disagrees: log discrepancy, route on preamble\n  → classifier unavailable: preamble routing only\n```\n\nFor a conforming receiver the minimum viable action is **step 5 alone** — the\nanalog/digital routing decision from bit 11. Steps 6–9 provide full mode\ndiscrimination. The classifier adds independent verification but is never required.\n\nThe decision tree in full:\n\n1. Detect synchronization sequence via correlation (modulation-specific)\n2. Receive 24 preamble bits\n3. Apply Golay(24,12) decoding and error correction\n4. If decoding fails (more than 3 bit errors): fall back to squelch / discard\n5. Extract bit 11 (Analog / Digital flag) — route immediately\n   - If 0: route to analog demodulator bank\n   - If 1: route to digital demodulator bank\n6. Extract bits 0–8 (Mode ID)\n7. Look up Mode ID in local mode table\n8. If mode is known: activate the corresponding demodulator\n9. If mode is unknown: optionally alert user; do not pass noise to audio output\n10. Extract bit 10 (Encrypted / Open flag) — invoke gr-linux-crypto if set\n11. Pass remaining frame data to the downstream decoder (LDPC or other)\n12. Classifier cross-check result (asynchronous, advisory):\n    - Log result to audit trail\n    - If mismatch: flag for operator review; do not interrupt audio in progress\n    - Apply corrected routing to the next transmission if mismatch is confirmed\n\n### Classifier Fallback Hierarchy\n\nThe neural network classifier operates at one of four levels depending on\navailable hardware. The system degrades gracefully at every level. The repeater\nworks correctly at all four levels — the classifier is always advisory, never\na hard dependency.\n\n| Mode | Preamble | Classifier | Backend | Latency |\n|---|---|---|---|---|\n| **FULL** | yes | yes | NPU (SpacemiT A100) | ~15 µs per chunk |\n| **DEGRADED** | yes | yes | CPU INT8 ONNX | ~5 ms per chunk |\n| **MINIMAL** | yes | no | None | Preamble only |\n| **FALLBACK** | no | no | None | Default mode policy |\n\n**Detection order at startup** (`python/grident/rmv_integration/backend.py`):\n\n1. Check for SpacemiT NPU kernel driver (`/sys/class/misc/spacemit-npu`)\n2. Attempt to load SpacemiT NPU runtime library (`spacemit_npu`)\n3. If NPU unavailable: check for INT8 ONNX models (`*_int8.onnx`) and onnxruntime\n4. If CPU classifier unavailable: run preamble-only (MINIMAL mode)\n5. Publish `identification_mode` on ZeroMQ status socket at startup and on any change\n\n**Classifier timeout policy:** if CPU inference takes longer than 100 ms on any\nsingle chunk, the result is discarded for that transmission. If timeouts exceed\n3 in any 60-second window, the CPU classifier is automatically disabled and the\nsystem falls back to MINIMAL mode. The mode change is published on the ZMQ\nstatus socket.\n\n**Seven rules that must never be violated:**\n\n1. Preamble controls routing. Classifier verifies routing.\n2. Never wait for the classifier before routing a signal.\n3. If classifier disagrees with preamble, route on preamble and log the discrepancy.\n4. Never interrupt audio output based on a classifier result.\n5. Apply corrected routing only to the next transmission.\n6. Publish `identification_mode` on ZMQ status at startup and on any mode change.\n7. The repeater works correctly in all four modes: FULL, DEGRADED, MINIMAL, FALLBACK.\n\n---\n\n## Interaction with LDPC\n\nThe preamble is designed to be fully transparent to any downstream LDPC decoder.\nThe following rules ensure this:\n\n- The preamble occupies a fixed, known number of bits (24) before the LDPC frame\n- The LDPC decoder is not started until the preamble boundary has been passed\n- The preamble uses Golay(24,12), which is entirely independent of LDPC\n- No preamble bit pattern can form a valid LDPC codeword start, provided the\n  synchronization sequence correctly delimits the boundary\n- The preamble carries no data that the LDPC decoder needs to reference\n\nImplementations must ensure the LDPC decoder input pointer starts at the correct\noffset after the preamble. The preamble should be stripped from the bitstream before\nit is passed to any downstream decoder.\n\n---\n\n## Security Integration — gr-linux-crypto\n\nThe preamble specification is designed to work alongside\n[gr-linux-crypto](https://github.com/Supermagnum/gr-linux-crypto), an out-of-tree GNU Radio\nmodule providing Linux-specific cryptographic infrastructure. When the **Encrypted / Open**\nflag (bit 10) is set in the preamble, the payload that follows is encrypted and the receiver\nmust have access to the appropriate key material before demodulation is useful.\n\n### How the Encrypted Flag Interacts with gr-linux-crypto\n\nWhen bit 10 is set to 1:\n\n1. The receiver identifies the mode from the Mode ID as normal\n2. The receiver knows the payload is encrypted before attempting to decode it\n3. Key material must be retrieved — from the Linux kernel keyring, a Nitrokey hardware\n   security module, or an OpenPGP card — before the payload is passed to the demodulator\n4. The decrypted payload is then passed to the appropriate demodulator for the identified mode\n\nThis allows a receiver to cleanly refuse to attempt demodulation of encrypted content\nit has no key for, rather than producing noise or a failed decode.\n\n### Supported Key Sources (via gr-linux-crypto)\n\n| Key Source | Description |\n|---|---|\n| Linux kernel keyring | Keys stored securely in the kernel, accessed via `keyctl` |\n| Nitrokey (password safe slot) | Keys stored on hardware token, never leave the device |\n| OpenPGP card / Nitrokey Pro | Private key operations performed on-card |\n| GnuPG agent | Keys managed by the GnuPG agent with pinentry PIN protection |\n\n### Supported Cryptographic Operations\n\n| Operation | Algorithm | Use |\n|---|---|---|\n| Payload encryption | AES-256-GCM | Authenticated encryption of frame payload |\n| Payload encryption | ChaCha20-Poly1305 | Battery-friendly authenticated encryption |\n| Key agreement | Brainpool ECDH (P-256r1 / P-384r1 / P-512r1) | Session key establishment |\n| Digital signature | Brainpool ECDSA | Callsign authentication; transmission signing |\n| Digital signature | Ed25519 (via gr-nacl) | Open alternative signature scheme |\n| Key derivation | HKDF (RFC 5869) | Deriving session keys from ECDH shared secret |\n| Multi-recipient | Brainpool ECIES (up to 25 recipients) | Encrypting session key for multiple receivers |\n\n### Regulatory Note\n\nAmateur radio regulations in most jurisdictions prohibit encryption of on-air content.\nThe **Encrypted / Open** flag and associated gr-linux-crypto support are provided for:\n\n- Experimental and research use on appropriate frequencies\n- Digital signature authentication (bit 10 = 0; signatures do not encrypt content)\n- Jurisdictions and frequency allocations where encryption is lawfully permitted\n\nUsers are solely responsible for ensuring their use complies with applicable regulations.\n\n### Digital Signatures Without Encryption\n\nIt is valid and useful to set bit 10 = 0 (open/unencrypted) while still using\ngr-linux-crypto for **digital signatures**. In this case:\n\n- The payload is transmitted in the clear and decodable by any receiver\n- A signature frame (Ed25519 or Brainpool ECDSA, 64–72 bytes) is appended at the\n  end of the transmission\n- Receivers with signature verification capability can verify callsign authenticity\n- Receivers without signature support simply ignore the trailing signature frame\n\nThis provides progressive enhancement: authentication without breaking compatibility.\n\n---\n\n## GNU Radio 4.x OOT module\n\nThe reference out-of-tree module lives under `blocklib/grident/`. CMake builds:\n\n| Plugin | Contents |\n|---|---|\n| `GrIdentBlocks` | Golay, preamble/metadata codec blocks, `PreambleOnPtt` (PTT-gated TX) |\n| `GrIdentZmqBlocks` | ZeroMQ PUSH/PULL IQ edges, preamble JSON PUB, TX/PTT SUB (requires libzmq) |\n\nBuild:\n\n```bash\ncmake -B build-gr4 -DCMAKE_PREFIX_PATH=/opt/gnuradio4-gcc\ncmake --build build-gr4\n```\n\nZeroMQ blocks support distributed flowgraphs (IQ between hosts), publishing decoded\npreamble fields as JSON on receive, and **TX/PTT control** on transmit (`ZmqTxControlSub`\ninto `PreambleOnPtt`). The default TX control profile matches the\n[LinHT Handheld Transceiver](https://github.com/M17-Project/LinHT-utils) PMT `SOT`/`EOT`\nmessages on `ipc:///tmp/ptt_msg`; use `profile=grident` for standalone multipart JSON/text.\nSee [`docs/zeromq-protocol.md`](docs/zeromq-protocol.md) for wire formats and source references.\n\nIQ-level detection without GNU Radio remains in `python/grident/iq_decode.py` and\n`blocklib/grident/lib/preamble_detect.cc`.\n\n---\n\n## IQ Validation — radio-modulation-validator\n\nThe `python/grident/rmv_integration/` subsystem connects gr-ident to\n[radio-modulation-validator](https://github.com/Supermagnum/radio-modulation-validator) (rmv),\nan independent neural network classifier trained on public RF datasets. This enables\ntwo-layer validation of committed IQ test vectors and live signal cross-checking at runtime.\n\n### Two-Layer Validation\n\nCommitted IQ test vectors can be checked with `grident-validate`:\n\n```bash\n# Preamble only — no external dependencies\nPYTHONPATH=python python3 apps/grident_validate.py --preamble-only\n\n# Full two-layer validation — requires rmv installed\nPYTHONPATH=python python3 apps/grident_validate.py \\\n  --fixtures python/tests/fixtures/common_modes/\n```\n\n**Layer 1 — Preamble (Golay roundtrip):** verifies the gr-ident specification itself.\nGolay(24,12) encode → decode, field extraction, comparison against fixture codewords,\nand error-injection tests (1–3 bit errors corrected, 4+ detected). Runs with no\nexternal dependencies.\n\n**Layer 2 — Signal (rmv classifier):** verifies that the IQ test vectors contain the\nmodulation claimed by each mode ID. The `.cf32` fixture file for each mode is classified\nby rmv and the result is compared against the expected modulation family (FM/FSK/PSK/AM/QAM)\nand order (NBFM_25/DMR/M17/etc.).\n\nIf rmv is not installed, preamble checks still run and signal validation is skipped\nwith a clear warning. See [`docs/rmv-integration.md`](docs/rmv-integration.md) and\n[`TESTING.md`](TESTING.md).\n\n### Neural Network Classifiers\n\nrmv provides two ResidualCNN classifiers trained on public RF datasets:\n\n| Classifier | Classes | Accuracy | Training data |\n|---|---|---|---|\n| Family | 6 (FM/FSK/PSK/QAM/AM/PAM) | 91.84% | RadioML 2016.10A + CSPB.ML.2018R2 + synthetic |\n| Order | 43 (NBFM/DMR/M17/YSF/NXDN/dPMR/…) | 70.48% | Same + protocol-accurate 4FSK synthetic |\n\nThe 43 order classes include protocol-specific modes generated from published standards:\n\n| Mode | Symbol rate | Deviation | Pulse shaping | Standard |\n|---|---|---|---|---|\n| DMR | 4800 baud | ±1944 Hz | RC α=0.2 | ETSI TS 102 361-1 |\n| M17 | 4800 baud | ±2400 Hz | RRC β=0.5 | M17 Project spec v1.0 |\n| YSF | 4800 baud | ±2400 Hz | Gaussian BT=0.5 | TIA-102 / Yaesu |\n| NXDN | 2400 baud | ±1050 Hz | RC α=0.2 | ICOM/Kenwood |\n| dPMR | 2400 baud | ±1050 Hz | RC α=0.2 | ETSI TS 102 490 |\n\nTraining data is generated independently from gr-ident blocks — only GNU Radio\nbuilt-ins and numpy/scipy are used, keeping the validation boundary clean.\n\nThe models output **modulation family and order**, not a gr-ident mode ID (0–511).\nPreamble routing always takes precedence when a valid preamble is present.\n\n**gr-ident mode IDs checked against the classifier in this repo**\n\nSignal-layer validation runs only on modes with committed IQ fixtures (eight modes\ntoday). Each fixture is compared to the expected family/order in\n`python/grident/rmv_integration/mode_map.py`:\n\n| Mode ID | Name | Expected family | Expected order |\n|---:|---|---|---|\n| 20 | NFM 12.5 kHz | FM | NBFM_25 |\n| 30 | NFM 12.5 kHz + CTCSS | FM | NBFM_25 |\n| 40 | NFM 12.5 kHz + DCS | FM | NBFM_25 |\n| 104 | C4FM / Fusion | FSK | YSF |\n| 108 | dPMR | FSK | dPMR |\n| 110 | EchoLink | FSK | CPFSK |\n| 158 | PSK31 | PSK | BPSK |\n| 159 | RTTY | FSK | 2FSK |\n\nAdditional mode IDs are mapped in `mode_map.py` for future fixtures or live cross-check\n(for example DMR 100–102, D-STAR 103, M17 120–121, Sleipnir 300). Mode IDs 0, 60, 109,\nand 511 are excluded from signal validation.\n\n**Known classifier ambiguities** (family match still passes; order may disagree):\n\n| Mode ID | Issue |\n|---:|---|\n| 107 | NXDN and dPMR share identical 4-FSK parameters |\n| 21 | Aviation AM vs NFM 8.33 kHz depends on fixture context |\n\n### INT8 Quantisation and NPU Deployment\n\nThe classifiers are available in three formats:\n\n| Format | Size | Inference latency | Use |\n|---|---|---|---|\n| FP32 ONNX (`*.onnx`) | 2.7 MB | ~15 ms CPU | Reference, full accuracy |\n| INT8 ONNX (`*_int8.onnx`) | ~700 KB | ~5 ms CPU | Default CPU deployment |\n| NPU binary (`*.nb`) | ~700 KB | ~15 µs NPU | SpacemiT K3 A100 cores |\n\nINT8 quantisation accuracy:\n\n| Model | FP32 vs INT8 agreement | Method |\n|---|---|---|\n| family_classifier | 99.90% | Static QDQ |\n| order_classifier | 99.61% | Dynamic INT8 fallback |\n\nOn the SpacemiT K3 with A100 NPU cores (60 TOPS INT8, RVV 1.0):\n\n- Both classifiers in sequence: ~30 µs total\n- One 1024-sample chunk at 48 kHz = 21.3 ms of signal\n- The classifier runs **710× faster than real time**\n\nTo generate NPU binaries on the K3 (requires SpacemiT NPU SDK):\n\n```bash\nrmv export-quantised --synthetic datasets/synthetic/synthetic.npz\nrmv export-npu --calibration datasets/synthetic/synthetic.npz\n```\n\nSee [`docs/npu-deployment.md`](docs/npu-deployment.md) for full deployment instructions\nincluding cross-compilation notes.\n\n### Classifier Fallback at Runtime\n\nThe `python/grident/rmv_integration/` modules implement the live receive path for\nrepeater deployments. At startup, the system detects the best available backend and\npublishes the result on the ZeroMQ status socket:\n\n```json\n{\n  \"classifier_backend\": \"npu\",\n  \"gr_ident_available\": true,\n  \"identification_mode\": \"full\",\n  \"rmv_available\": true,\n  \"classifier_timeout_ms\": 10\n}\n```\n\nThe four operating modes and fallback rules are described in full under\n[Classifier Fallback Hierarchy](#classifier-fallback-hierarchy) above.\n\nRuntime modules:\n\n| Module | Purpose |\n|---|---|\n| `backend.py` | NPU and CPU detection at startup |\n| `runtime.py` | `RuntimeStatus`, `initialise_runtime()`, ZMQ status |\n| `identify.py` | Async live identification with timeout and cross-check |\n| `validator.py` | Offline validation against rmv (used by `grident-validate`) |\n| `mode_map.py` | Mode ID → rmv family/order mapping |\n| `preamble_check.py` | Golay roundtrip checks |\n| `report.py` | Markdown report generator |\n| `cli.py` | `grident-validate` command |\n\n---\n\n## Future Work\n\n- Additional modulation profiles and normative sync sequences for remaining mode IDs\n- Hosted web UI for the experimental mode registry (JSON registry exists in-repo)\n- GNU Radio 4.x streaming blocks for BPSK / 2-FSK profiles and full in-process flowgraph examples\n- End-to-end gr-linux-crypto demo flowgraph (design reference:\n  [`apps/flowgraphs/gr-linux-crypto-demo.md`](apps/flowgraphs/gr-linux-crypto-demo.md))\n- Formal assignment of mode IDs for additional regional and emerging modes\n- P25-specific 4FSK synthetic training data (9600 baud CQPSK) to improve P25 order-level classification\n- SpacemiT NPU `.nb` model files once SDK is available on the development machine\n- Static QDQ quantisation for the order classifier (currently using dynamic INT8 fallback)\n\n---\n\n## License\n\ngr-ident is an **open standard**. This specification is released into the public domain.\nNo rights reserved. Implementers are free to use, modify, and distribute this specification\nwithout restriction. Attribution is appreciated but not required.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsupermagnum%2Fgr-ident","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsupermagnum%2Fgr-ident","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsupermagnum%2Fgr-ident/lists"}