weaklink-modem 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivica Matic (9A3ICE)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.1
2
+ Name: weaklink-modem
3
+ Version: 0.6.1
4
+ Summary: N-FSK streaming modem for HF SSB and wider channels: Reed-Solomon framing, soft Viterbi, round-robin block repetition, byte-pipe semantics.
5
+ License: MIT
6
+ Author: ivica3730k
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Telecommunications Industry
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Communications :: Ham Radio
20
+ Classifier: Topic :: Multimedia :: Sound/Audio
21
+ Requires-Dist: numpy (>=2.0,<3.0)
22
+ Requires-Dist: reedsolo (>=1.7.0,<2.0.0)
23
+ Requires-Dist: sounddevice (>=0.5,<0.6)
24
+ Requires-Dist: soundfile (>=0.12,<0.13)
25
+ Project-URL: Bug Tracker, https://github.com/ivica3730k/weaklink-9a3ice/issues
26
+ Project-URL: Homepage, https://github.com/ivica3730k/weaklink-9a3ice
27
+ Project-URL: Repository, https://github.com/ivica3730k/weaklink-9a3ice
28
+ Description-Content-Type: text/markdown
29
+
30
+ <!-- generated from README.md at tag v0.6.1 by scripts/build_pypi_readme.py; do not hand-edit -->
31
+
32
+ # weaklink
33
+
34
+ Streaming digital modem: bytes on stdin → audio → bytes on stdout. Works
35
+ with `tail -f`; no memory buffering, no wait-for-EOF.
36
+
37
+ ![alt text](https://github.com/ivica3730k/weaklink-9a3ice/raw/v0.6.1/image.png)
38
+
39
+ Distribution: `weaklink-modem` (PyPI + release binaries).
40
+
41
+ ---
42
+
43
+ ## Install
44
+
45
+ Portable Linux binary:
46
+
47
+ ```bash
48
+ sudo apt install libportaudio2 libsndfile1
49
+ curl -L -O https://github.com/ivica3730k/weaklink-9a3ice/releases/latest/download/weaklink-modem-linux-x86_64-latest
50
+ chmod +x weaklink-modem-linux-x86_64-latest
51
+ ```
52
+
53
+ Debian / Ubuntu `.deb`:
54
+
55
+ ```bash
56
+ curl -L -O https://github.com/ivica3730k/weaklink-9a3ice/releases/latest/download/weaklink-modem_amd64-latest.deb
57
+ sudo dpkg -i weaklink-modem_amd64-latest.deb
58
+ ```
59
+
60
+ From source:
61
+
62
+ ```bash
63
+ poetry install
64
+ poetry run weaklink-modem --version
65
+ ```
66
+
67
+ PyPI:
68
+
69
+ ```bash
70
+ pip install weaklink-modem
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Quickstart
76
+
77
+ ```bash
78
+ # WAV roundtrip
79
+ echo -n "hello weaklink" | weaklink-modem tx --modem-wav /tmp/hello.wav
80
+ weaklink-modem rx --modem-wav /tmp/hello.wav
81
+
82
+ # Live speaker → mic
83
+ weaklink-modem rx > out.txt &
84
+ echo -n "over the room" | weaklink-modem tx
85
+
86
+ # Long-lived stream
87
+ tail -f /var/log/syslog | weaklink-modem tx --modem-baud 300
88
+ ```
89
+
90
+ Both sides must use the same `--modem-baud` (no handshake).
91
+
92
+ ---
93
+
94
+ ## Presets
95
+
96
+ Three baud rates. Every preset carries 13 B of payload per RS block
97
+ (RS(16,8) + CRC-32), so message sizes map identically across bauds.
98
+
99
+ | Baud | CLI (both tx / rx) | 4-FSK tones (Hz) | Bandwidth | Default repeats | Measured best SNR | Min live tx (13 B payload) |
100
+ |---:|---|---|---:|---:|---:|---:|
101
+ | 45 | `--modem-baud 45` | 1200 / 1400 / 1600 / 1800 | 600 Hz | 4× | ≈ −14 dB | 28 s |
102
+ | 300 | `--modem-baud 300` | 1050 / 1350 / 1650 / 1950 | 900 Hz | 2× | ≈ −5 dB | 2.4 s |
103
+ | 1200 | `--modem-baud 1200` | 500 / 1700 / 2900 / 4100 | 3600 Hz | 2× | ≈ +2 dB | 1.0 s |
104
+
105
+ SNR is measured with AWGN normalised to a 3 kHz reference band — a
106
+ cross-baud comparison convention, not a physical channel filter.
107
+
108
+ Override `--modem-block-repeats N` on both sides for more copies. Each
109
+ doubling buys ~2–3 dB via soft-LLR combining, at proportional air time.
110
+
111
+ ---
112
+
113
+ ## Debugging live audio
114
+
115
+ `weaklink-modem rx --modem-debug > out.txt` writes diagnostics to
116
+ `log.txt` (stdout stays clean for piping). Watch for:
117
+
118
+ - `audio: peak +X dBFS` below −40 dBFS → wrong mic or gain too low.
119
+ - `RS corrected` — outer code saved a block.
120
+ - `N slot(s) failed CRC/RS` — unrecoverable, data lost.
121
+ - macOS mic AGC / voice-isolation destroys tones; disable in System Settings.
122
+
123
+ ---
124
+
125
+ ## Full SNR sweep
126
+
127
+ Every baud × num_tones × RS × repeats combo is measured in
128
+ [`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.6.1/results.md). Re-run `poetry run weaklink-modem-benchmark` to
129
+ refresh.
130
+
131
+ ---
132
+
133
+ ## Supported audio backends
134
+
135
+ | Backend | Linux | macOS | Windows | Notes |
136
+ |---|:---:|:---:|:---:|---|
137
+ | sounddevice / PortAudio | ✓ | ✓ | ✓* | Default. Index (`5`) or name substring (`USB`). WASAPI / CoreAudio / ALSA under the hood. |
138
+ | Pulse / PipeWire subprocess | ✓ | — | — | `paplay` / `parec`. Fires on `pulse:<id>`, `pulse:<name>`, a bare Pulse sink id resolvable by `pactl`, or a name PortAudio doesn't know (e.g. `virt.monitor`). |
139
+ | WAV via soundfile | ✓ | ✓ | ✓ | File I/O only, via `--modem-wav`. |
140
+
141
+ `*` Windows is untested — PortAudio supports it so it should work, but no CI on that platform yet.
142
+
143
+ ---
144
+
145
+ ## CLI reference
146
+
147
+ | Flag | Default | Description |
148
+ |------|---------|-------------|
149
+ | `--modem-baud N` | `300` | Symbol rate. Only `45`, `300`, `1200` supported. |
150
+ | `--modem-num-tones N` | `4` | N-FSK order: 2 / 4 / 8 / 16. Higher packs more bits per symbol at wider bandwidth and worse cliff. 2 halves throughput but fits narrow audio paths (e.g. FM voice via SignaLink). TX and RX must match. |
151
+ | `--modem-rs-data-bytes N` | preset | Reed-Solomon data bytes per block. |
152
+ | `--modem-rs-parity-bytes N` | preset | RS parity bytes. Corrects up to N/2 byte errors per block. |
153
+ | `--modem-no-rs-crc` | CRC on | Skip the CRC-32 inside each RS block. |
154
+ | `--modem-block-repeats N` | preset | N copies per block, each permuted differently; RX soft-combines LLRs. |
155
+ | `--modem-wav PATH` | live | WAV file instead of live audio. |
156
+ | `--modem-audio-output NAME` | OS default | tx audio target: sounddevice index, name substring, or Pulse sink. |
157
+ | `--modem-audio-input NAME` | OS default | rx audio source: same syntax; Pulse sources like `virt.monitor` supported. |
158
+ | `--modem-debug` | off | Verbose DEBUG chatter in the log file. |
159
+ | `--modem-log-file PATH` | `./log.txt` | Diagnostics land here. |
160
+
161
+ ---
162
+
163
+ ## How it works
164
+
165
+ ### TX signal chain
166
+
167
+ ```mermaid
168
+ flowchart LR
169
+ A[stdin bytes] --> B[Chunk<br/>rs_data − 3 B<br/>per block]
170
+ B --> C[Frame<br/>len · idx · payload · pad]
171
+ C --> D[RS + CRC-32]
172
+ D --> E[Conv encode<br/>K=7, r=1/2]
173
+ E --> F[Per-block<br/>interleaver<br/>32-cycle PN]
174
+ F --> G[Bits → 4-FSK<br/>symbols]
175
+ G --> H[CPFSK<br/>modulator]
176
+ H --> I[+ preamble<br/>+ pilot]
177
+ I --> J[audio out]
178
+ ```
179
+
180
+ ### RX signal chain
181
+
182
+ ```mermaid
183
+ flowchart LR
184
+ A[audio in] --> B[Non-coherent<br/>4-FSK demod]
185
+ B --> C[Coarse FFT<br/>LO offset]
186
+ C --> D[Preamble<br/>correlator]
187
+ D --> E[Peak detect<br/>+ non-max<br/>suppression]
188
+ E --> F[Per-preamble<br/>fine offset]
189
+ F --> G[Slot soft LLRs]
190
+ G --> H[Deinterleave<br/>seed brute-force]
191
+ H --> I[Soft Viterbi]
192
+ I --> J{RS + CRC<br/>ok?}
193
+ J -- yes --> K[Assemble<br/>by block_index]
194
+ J -- no --> L[Buffer LLRs;<br/>combine across<br/>R copies]
195
+ L --> I
196
+ K --> M[Strip zero-pad<br/>via length]
197
+ M --> N[stdout bytes]
198
+ ```
199
+
200
+ Every slot is bracketed by a preamble, so any single slot decodes
201
+ standalone. Spurious mid-stream peaks get dropped. Message boundaries
202
+ between separate tx sessions are inferred from non-block-length spans
203
+ between preambles — one rx pipe can watch many tx sessions in a row.
204
+
205
+ ### Wire format
206
+
207
+ ```
208
+ One tx session (live audio):
209
+
210
+ ┌────────┬─────┬────────┬─────┬────────┬─────┬─────┬────────┬─────┬────────┐
211
+ │ pilot │ pre │ slot 0 │ pre │ slot 1 │ pre │ ... │slot N-1│ pre │ pilot │
212
+ └────────┴─────┴────────┴─────┴────────┴─────┴─────┴────────┴─────┴────────┘
213
+
214
+ One RS block, data area (before conv + interleave + FSK):
215
+
216
+ ┌── 1B ──┬── 2B ────┬──── rs_data − 3 B ────┬── 4B CRC ──┬── rs_parity B ──┐
217
+ │ length │block_idx │ payload (zero-padded) │ CRC-32 │ RS parity │
218
+ └────────┴──────────┴───────────────────────┴────────────┴─────────────────┘
219
+ ```
220
+
221
+ `block_idx` is 2 bytes → one tx session is bounded at 65 535 slots.
222
+
223
+ ---
224
+
225
+ ## Testing
226
+
227
+ ```bash
228
+ poetry run pytest -q # ~2 min, full suite
229
+ ```
230
+
231
+ Every batch-decode test has an e2e-streaming companion that drives audio
232
+ through the same `_StreamingRxPump` the CLI uses.
233
+
234
+ ---
235
+
236
+ ## Glossary
237
+
238
+ - **N-FSK / CPFSK** — N continuous-phase tones, log₂(N) bits per symbol. Default N=4.
239
+ - **Preamble** — Fixed 32-symbol PN sequence bracketing every slot; RX locks timing / frequency / amplitude from it.
240
+ - **Slot** — Preamble + one RS-encoded block.
241
+ - **Block** — RS-encoded chunk carrying header + payload.
242
+ - **RS(n,k)** — Reed-Solomon outer code. k data + parity → n wire bytes; corrects (n-k)/2 byte errors.
243
+ - **CRC-32** — Catches errors past RS correction.
244
+ - **Convolutional code (K=7, r=1/2) + soft Viterbi** — Inner FEC and its decoder, driven by per-bit LLRs.
245
+ - **LLR** — Log-likelihood ratio; a soft (real-valued) confidence per bit instead of a hard 0/1.
246
+ - **Interleaver** — Bit shuffle so bursts become isolated errors. Ours changes every block (32-permutation cycle).
247
+ - **Non-coherent demod** — Tone detection by energy; ~3 dB behind coherent.
248
+ - **LO offset** — Radio frequency error; we correct up to ±500 Hz.
249
+ - **Pilot** — Short random N-FSK burst before / after every live TX.
250
+ - **SNR (dB)** — Signal-to-noise ratio. Negative = noise louder than signal.
251
+ - **AWGN** — Additive white Gaussian noise; the standard clean-channel noise model used by the benchmark.
252
+ - **Shannon limit** — Theoretical lowest SNR at which a given data rate can be decoded error-free. Every FEC decoder sits some dB above it — that gap is what "Gap" columns report.
253
+ - **Best SNR / cliff** — Lowest SNR at which decode still works for a given config. Below it, everything breaks.
254
+ - **Nyquist theorem** — A signal is only recoverable if sampled above twice its highest frequency. In practice: tones must sit below sample_rate/2, and N-FSK tone spacing must be at least `1/T_symbol` for non-coherent orthogonality.
255
+
256
+ ---
257
+
258
+ ## Roadmap
259
+
260
+ - **Coherent detection** — Costas-loop demod, ~3 dB gain. Big DSP lift.
261
+ - **LDPC** — Closes ~2–4 dB of the Shannon gap. Needs a proper construction.
262
+
263
+ ---
264
+
265
+ ## License
266
+
267
+ MIT. See `LICENSE`.
268
+
269
+ Reed-Solomon via [`reedsolo`](https://github.com/tomerfiliba-org/reedsolomon).
270
+ Convolutional code uses the standard NASA/CCSDS (171, 133) generator
271
+ polynomials. Audio via [`sounddevice`](https://github.com/spatialaudio/python-sounddevice)
272
+ and [`soundfile`](https://github.com/bastibe/python-soundfile).
273
+
@@ -0,0 +1,88 @@
1
+ [tool.poetry]
2
+ name = "weaklink-modem"
3
+ version = "0.6.1"
4
+ description = "N-FSK streaming modem for HF SSB and wider channels: Reed-Solomon framing, soft Viterbi, round-robin block repetition, byte-pipe semantics."
5
+ authors = ["ivica3730k"]
6
+ license = "MIT"
7
+ readme = "readme-pypi.md"
8
+ packages = [{ include = "weaklink/modem" }]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Telecommunications Industry",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: POSIX :: Linux",
14
+ "Operating System :: MacOS",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Communications :: Ham Radio",
20
+ "Topic :: Multimedia :: Sound/Audio",
21
+ ]
22
+
23
+ [tool.poetry.urls]
24
+ Homepage = "https://github.com/ivica3730k/weaklink-9a3ice"
25
+ Repository = "https://github.com/ivica3730k/weaklink-9a3ice"
26
+ "Bug Tracker" = "https://github.com/ivica3730k/weaklink-9a3ice/issues"
27
+
28
+ [tool.poetry.dependencies]
29
+ python = "^3.10"
30
+ reedsolo = "^1.7.0"
31
+ numpy = "^2.0"
32
+ soundfile = "^0.12"
33
+ sounddevice = "^0.5"
34
+
35
+ [tool.poetry.group.dev.dependencies]
36
+ black = "26.3.1"
37
+ pre-commit = "4.6.0"
38
+ pytest = "9.0.3"
39
+ mypy = "^2.1.0"
40
+ pylint = "^4.0.5"
41
+ setuptools = "82.0.1"
42
+ pytest-xdist = "^3.8.0"
43
+
44
+ [tool.poetry.scripts]
45
+ weaklink-modem = "weaklink.modem.cli:main"
46
+ weaklink-modem-benchmark = "weaklink.modem.benchmark:main"
47
+
48
+ [build-system]
49
+ requires = ["poetry-core>=1.0.0"]
50
+ build-backend = "poetry.core.masonry.api"
51
+
52
+ [tool.black]
53
+ line-length = 120
54
+ target-version = ["py312"]
55
+ include = '\.pyi?$'
56
+ exclude = '''
57
+ /(
58
+ \.git
59
+ | \.venv
60
+ | build
61
+ | venv
62
+ | dist
63
+ )/
64
+ '''
65
+
66
+ [tool.pytest.ini_options]
67
+ addopts = "-n auto"
68
+
69
+ [tool.ruff]
70
+ line-length = 120
71
+ target-version = "py312"
72
+
73
+ [tool.ruff.lint]
74
+ # F = pyflakes (unused imports F401, undefined names F821, unused vars, ...)
75
+ # I = isort-compatible import ordering
76
+ select = ["F", "I001"]
77
+
78
+ [tool.ruff.lint.isort]
79
+ known-first-party = ["weaklink"]
80
+
81
+ [tool.mypy]
82
+ python_version = "3.12"
83
+ files = ["weaklink", "tests"]
84
+ ignore_missing_imports = true
85
+ check_untyped_defs = true
86
+ warn_redundant_casts = true
87
+ warn_unused_ignores = true
88
+ show_error_codes = true
@@ -0,0 +1,243 @@
1
+ <!-- generated from README.md at tag v0.6.1 by scripts/build_pypi_readme.py; do not hand-edit -->
2
+
3
+ # weaklink
4
+
5
+ Streaming digital modem: bytes on stdin → audio → bytes on stdout. Works
6
+ with `tail -f`; no memory buffering, no wait-for-EOF.
7
+
8
+ ![alt text](https://github.com/ivica3730k/weaklink-9a3ice/raw/v0.6.1/image.png)
9
+
10
+ Distribution: `weaklink-modem` (PyPI + release binaries).
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ Portable Linux binary:
17
+
18
+ ```bash
19
+ sudo apt install libportaudio2 libsndfile1
20
+ curl -L -O https://github.com/ivica3730k/weaklink-9a3ice/releases/latest/download/weaklink-modem-linux-x86_64-latest
21
+ chmod +x weaklink-modem-linux-x86_64-latest
22
+ ```
23
+
24
+ Debian / Ubuntu `.deb`:
25
+
26
+ ```bash
27
+ curl -L -O https://github.com/ivica3730k/weaklink-9a3ice/releases/latest/download/weaklink-modem_amd64-latest.deb
28
+ sudo dpkg -i weaklink-modem_amd64-latest.deb
29
+ ```
30
+
31
+ From source:
32
+
33
+ ```bash
34
+ poetry install
35
+ poetry run weaklink-modem --version
36
+ ```
37
+
38
+ PyPI:
39
+
40
+ ```bash
41
+ pip install weaklink-modem
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Quickstart
47
+
48
+ ```bash
49
+ # WAV roundtrip
50
+ echo -n "hello weaklink" | weaklink-modem tx --modem-wav /tmp/hello.wav
51
+ weaklink-modem rx --modem-wav /tmp/hello.wav
52
+
53
+ # Live speaker → mic
54
+ weaklink-modem rx > out.txt &
55
+ echo -n "over the room" | weaklink-modem tx
56
+
57
+ # Long-lived stream
58
+ tail -f /var/log/syslog | weaklink-modem tx --modem-baud 300
59
+ ```
60
+
61
+ Both sides must use the same `--modem-baud` (no handshake).
62
+
63
+ ---
64
+
65
+ ## Presets
66
+
67
+ Three baud rates. Every preset carries 13 B of payload per RS block
68
+ (RS(16,8) + CRC-32), so message sizes map identically across bauds.
69
+
70
+ | Baud | CLI (both tx / rx) | 4-FSK tones (Hz) | Bandwidth | Default repeats | Measured best SNR | Min live tx (13 B payload) |
71
+ |---:|---|---|---:|---:|---:|---:|
72
+ | 45 | `--modem-baud 45` | 1200 / 1400 / 1600 / 1800 | 600 Hz | 4× | ≈ −14 dB | 28 s |
73
+ | 300 | `--modem-baud 300` | 1050 / 1350 / 1650 / 1950 | 900 Hz | 2× | ≈ −5 dB | 2.4 s |
74
+ | 1200 | `--modem-baud 1200` | 500 / 1700 / 2900 / 4100 | 3600 Hz | 2× | ≈ +2 dB | 1.0 s |
75
+
76
+ SNR is measured with AWGN normalised to a 3 kHz reference band — a
77
+ cross-baud comparison convention, not a physical channel filter.
78
+
79
+ Override `--modem-block-repeats N` on both sides for more copies. Each
80
+ doubling buys ~2–3 dB via soft-LLR combining, at proportional air time.
81
+
82
+ ---
83
+
84
+ ## Debugging live audio
85
+
86
+ `weaklink-modem rx --modem-debug > out.txt` writes diagnostics to
87
+ `log.txt` (stdout stays clean for piping). Watch for:
88
+
89
+ - `audio: peak +X dBFS` below −40 dBFS → wrong mic or gain too low.
90
+ - `RS corrected` — outer code saved a block.
91
+ - `N slot(s) failed CRC/RS` — unrecoverable, data lost.
92
+ - macOS mic AGC / voice-isolation destroys tones; disable in System Settings.
93
+
94
+ ---
95
+
96
+ ## Full SNR sweep
97
+
98
+ Every baud × num_tones × RS × repeats combo is measured in
99
+ [`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.6.1/results.md). Re-run `poetry run weaklink-modem-benchmark` to
100
+ refresh.
101
+
102
+ ---
103
+
104
+ ## Supported audio backends
105
+
106
+ | Backend | Linux | macOS | Windows | Notes |
107
+ |---|:---:|:---:|:---:|---|
108
+ | sounddevice / PortAudio | ✓ | ✓ | ✓* | Default. Index (`5`) or name substring (`USB`). WASAPI / CoreAudio / ALSA under the hood. |
109
+ | Pulse / PipeWire subprocess | ✓ | — | — | `paplay` / `parec`. Fires on `pulse:<id>`, `pulse:<name>`, a bare Pulse sink id resolvable by `pactl`, or a name PortAudio doesn't know (e.g. `virt.monitor`). |
110
+ | WAV via soundfile | ✓ | ✓ | ✓ | File I/O only, via `--modem-wav`. |
111
+
112
+ `*` Windows is untested — PortAudio supports it so it should work, but no CI on that platform yet.
113
+
114
+ ---
115
+
116
+ ## CLI reference
117
+
118
+ | Flag | Default | Description |
119
+ |------|---------|-------------|
120
+ | `--modem-baud N` | `300` | Symbol rate. Only `45`, `300`, `1200` supported. |
121
+ | `--modem-num-tones N` | `4` | N-FSK order: 2 / 4 / 8 / 16. Higher packs more bits per symbol at wider bandwidth and worse cliff. 2 halves throughput but fits narrow audio paths (e.g. FM voice via SignaLink). TX and RX must match. |
122
+ | `--modem-rs-data-bytes N` | preset | Reed-Solomon data bytes per block. |
123
+ | `--modem-rs-parity-bytes N` | preset | RS parity bytes. Corrects up to N/2 byte errors per block. |
124
+ | `--modem-no-rs-crc` | CRC on | Skip the CRC-32 inside each RS block. |
125
+ | `--modem-block-repeats N` | preset | N copies per block, each permuted differently; RX soft-combines LLRs. |
126
+ | `--modem-wav PATH` | live | WAV file instead of live audio. |
127
+ | `--modem-audio-output NAME` | OS default | tx audio target: sounddevice index, name substring, or Pulse sink. |
128
+ | `--modem-audio-input NAME` | OS default | rx audio source: same syntax; Pulse sources like `virt.monitor` supported. |
129
+ | `--modem-debug` | off | Verbose DEBUG chatter in the log file. |
130
+ | `--modem-log-file PATH` | `./log.txt` | Diagnostics land here. |
131
+
132
+ ---
133
+
134
+ ## How it works
135
+
136
+ ### TX signal chain
137
+
138
+ ```mermaid
139
+ flowchart LR
140
+ A[stdin bytes] --> B[Chunk<br/>rs_data − 3 B<br/>per block]
141
+ B --> C[Frame<br/>len · idx · payload · pad]
142
+ C --> D[RS + CRC-32]
143
+ D --> E[Conv encode<br/>K=7, r=1/2]
144
+ E --> F[Per-block<br/>interleaver<br/>32-cycle PN]
145
+ F --> G[Bits → 4-FSK<br/>symbols]
146
+ G --> H[CPFSK<br/>modulator]
147
+ H --> I[+ preamble<br/>+ pilot]
148
+ I --> J[audio out]
149
+ ```
150
+
151
+ ### RX signal chain
152
+
153
+ ```mermaid
154
+ flowchart LR
155
+ A[audio in] --> B[Non-coherent<br/>4-FSK demod]
156
+ B --> C[Coarse FFT<br/>LO offset]
157
+ C --> D[Preamble<br/>correlator]
158
+ D --> E[Peak detect<br/>+ non-max<br/>suppression]
159
+ E --> F[Per-preamble<br/>fine offset]
160
+ F --> G[Slot soft LLRs]
161
+ G --> H[Deinterleave<br/>seed brute-force]
162
+ H --> I[Soft Viterbi]
163
+ I --> J{RS + CRC<br/>ok?}
164
+ J -- yes --> K[Assemble<br/>by block_index]
165
+ J -- no --> L[Buffer LLRs;<br/>combine across<br/>R copies]
166
+ L --> I
167
+ K --> M[Strip zero-pad<br/>via length]
168
+ M --> N[stdout bytes]
169
+ ```
170
+
171
+ Every slot is bracketed by a preamble, so any single slot decodes
172
+ standalone. Spurious mid-stream peaks get dropped. Message boundaries
173
+ between separate tx sessions are inferred from non-block-length spans
174
+ between preambles — one rx pipe can watch many tx sessions in a row.
175
+
176
+ ### Wire format
177
+
178
+ ```
179
+ One tx session (live audio):
180
+
181
+ ┌────────┬─────┬────────┬─────┬────────┬─────┬─────┬────────┬─────┬────────┐
182
+ │ pilot │ pre │ slot 0 │ pre │ slot 1 │ pre │ ... │slot N-1│ pre │ pilot │
183
+ └────────┴─────┴────────┴─────┴────────┴─────┴─────┴────────┴─────┴────────┘
184
+
185
+ One RS block, data area (before conv + interleave + FSK):
186
+
187
+ ┌── 1B ──┬── 2B ────┬──── rs_data − 3 B ────┬── 4B CRC ──┬── rs_parity B ──┐
188
+ │ length │block_idx │ payload (zero-padded) │ CRC-32 │ RS parity │
189
+ └────────┴──────────┴───────────────────────┴────────────┴─────────────────┘
190
+ ```
191
+
192
+ `block_idx` is 2 bytes → one tx session is bounded at 65 535 slots.
193
+
194
+ ---
195
+
196
+ ## Testing
197
+
198
+ ```bash
199
+ poetry run pytest -q # ~2 min, full suite
200
+ ```
201
+
202
+ Every batch-decode test has an e2e-streaming companion that drives audio
203
+ through the same `_StreamingRxPump` the CLI uses.
204
+
205
+ ---
206
+
207
+ ## Glossary
208
+
209
+ - **N-FSK / CPFSK** — N continuous-phase tones, log₂(N) bits per symbol. Default N=4.
210
+ - **Preamble** — Fixed 32-symbol PN sequence bracketing every slot; RX locks timing / frequency / amplitude from it.
211
+ - **Slot** — Preamble + one RS-encoded block.
212
+ - **Block** — RS-encoded chunk carrying header + payload.
213
+ - **RS(n,k)** — Reed-Solomon outer code. k data + parity → n wire bytes; corrects (n-k)/2 byte errors.
214
+ - **CRC-32** — Catches errors past RS correction.
215
+ - **Convolutional code (K=7, r=1/2) + soft Viterbi** — Inner FEC and its decoder, driven by per-bit LLRs.
216
+ - **LLR** — Log-likelihood ratio; a soft (real-valued) confidence per bit instead of a hard 0/1.
217
+ - **Interleaver** — Bit shuffle so bursts become isolated errors. Ours changes every block (32-permutation cycle).
218
+ - **Non-coherent demod** — Tone detection by energy; ~3 dB behind coherent.
219
+ - **LO offset** — Radio frequency error; we correct up to ±500 Hz.
220
+ - **Pilot** — Short random N-FSK burst before / after every live TX.
221
+ - **SNR (dB)** — Signal-to-noise ratio. Negative = noise louder than signal.
222
+ - **AWGN** — Additive white Gaussian noise; the standard clean-channel noise model used by the benchmark.
223
+ - **Shannon limit** — Theoretical lowest SNR at which a given data rate can be decoded error-free. Every FEC decoder sits some dB above it — that gap is what "Gap" columns report.
224
+ - **Best SNR / cliff** — Lowest SNR at which decode still works for a given config. Below it, everything breaks.
225
+ - **Nyquist theorem** — A signal is only recoverable if sampled above twice its highest frequency. In practice: tones must sit below sample_rate/2, and N-FSK tone spacing must be at least `1/T_symbol` for non-coherent orthogonality.
226
+
227
+ ---
228
+
229
+ ## Roadmap
230
+
231
+ - **Coherent detection** — Costas-loop demod, ~3 dB gain. Big DSP lift.
232
+ - **LDPC** — Closes ~2–4 dB of the Shannon gap. Needs a proper construction.
233
+
234
+ ---
235
+
236
+ ## License
237
+
238
+ MIT. See `LICENSE`.
239
+
240
+ Reed-Solomon via [`reedsolo`](https://github.com/tomerfiliba-org/reedsolomon).
241
+ Convolutional code uses the standard NASA/CCSDS (171, 133) generator
242
+ polynomials. Audio via [`sounddevice`](https://github.com/spatialaudio/python-sounddevice)
243
+ and [`soundfile`](https://github.com/bastibe/python-soundfile).
@@ -0,0 +1,31 @@
1
+ """Weaklink modem: N-FSK + convolutional FEC + soft Viterbi, audio I/O."""
2
+
3
+ from weaklink.modem.api import ModemOptions, build_config, rx, tx
4
+ from weaklink.modem.codec import ModemConfig
5
+ from weaklink.modem.constants import BAUD_PRESETS
6
+ from weaklink.modem.exceptions import (
7
+ ConfigError,
8
+ EncodeError,
9
+ NyquistError,
10
+ PTTError,
11
+ WeaklinkError,
12
+ )
13
+ from weaklink.modem.waveform import WaveformConfig
14
+
15
+ __all__ = [
16
+ # Public API
17
+ "tx",
18
+ "rx",
19
+ "build_config",
20
+ "BAUD_PRESETS",
21
+ # Config objects
22
+ "ModemOptions",
23
+ "ModemConfig",
24
+ "WaveformConfig",
25
+ # Exceptions
26
+ "WeaklinkError",
27
+ "ConfigError",
28
+ "NyquistError",
29
+ "EncodeError",
30
+ "PTTError",
31
+ ]