weaklink-modem 0.7.2__tar.gz → 0.7.4__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.
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/PKG-INFO +17 -47
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/pyproject.toml +2 -2
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/readme-pypi.md +15 -45
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/__init__.py +1 -1
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/benchmark.py +1 -1
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/cli.py +2 -2
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/codec.py +3 -3
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/constants.py +1 -1
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/exceptions.py +1 -1
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/streaming.py +1 -1
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/waveform.py +7 -7
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/LICENSE +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/api.py +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/audio.py +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/fec.py +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/interleaver.py +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/ptt.py +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/py.typed +0 -0
- {weaklink_modem-0.7.2 → weaklink_modem-0.7.4}/src/weaklink/modem/rs.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: weaklink-modem
|
|
3
|
-
Version: 0.7.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.7.4
|
|
4
|
+
Summary: MFSK streaming modem for HF SSB and wider channels: Reed-Solomon framing, soft Viterbi, round-robin block repetition, byte-pipe semantics.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: ivica3730k
|
|
7
7
|
Requires-Python: >=3.10,<4.0
|
|
@@ -27,7 +27,7 @@ Project-URL: Homepage, https://github.com/ivica3730k/weaklink-9a3ice
|
|
|
27
27
|
Project-URL: Repository, https://github.com/ivica3730k/weaklink-9a3ice
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
|
-
<!-- generated from README.md at tag v0.7.
|
|
30
|
+
<!-- generated from README.md at tag v0.7.4 by scripts/build_pypi_readme.py; do not hand-edit -->
|
|
31
31
|
|
|
32
32
|
# weaklink
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ with `tail -f`; no memory buffering, no wait-for-EOF. RS + convolutional
|
|
|
37
37
|
soft-LLR combining. Modes: **OOK (1-tone)** through **16-FSK**, at
|
|
38
38
|
**45 / 300 / 1200 baud**.
|
|
39
39
|
|
|
40
|
-

|
|
41
41
|
|
|
42
42
|
Distribution: `weaklink-modem` (PyPI + release binaries).
|
|
43
43
|
|
|
@@ -121,7 +121,7 @@ cross-baud comparison convention, not a physical channel filter.
|
|
|
121
121
|
|
|
122
122
|
Doubling `--modem-block-repeats` buys ~2–3 dB via soft-LLR combining
|
|
123
123
|
at proportional air time. Full sweep of every combo we test is in
|
|
124
|
-
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.
|
|
124
|
+
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.4/results.md).
|
|
125
125
|
|
|
126
126
|
### OOK / 1-tone mode
|
|
127
127
|
|
|
@@ -131,52 +131,21 @@ bandwidth of any mode (no tone stack — just the carrier and its
|
|
|
131
131
|
modulation sidelobes). Same 1 bit/symbol as 2-FSK but a few dB worse
|
|
132
132
|
in AWGN — the trade you make for the narrow spectrum. Cliff at
|
|
133
133
|
45 baud with `block_repeats=4`: ≈ −14 dB, matching 4-FSK at the same
|
|
134
|
-
settings. See [`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.
|
|
134
|
+
settings. See [`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.4/results.md) for the per-baud numbers.
|
|
135
135
|
|
|
136
|
-
### One tone at a time (constant envelope, any
|
|
136
|
+
### One tone at a time (constant envelope, any M)
|
|
137
137
|
|
|
138
138
|
Every mode in this modem is **single-tone-at-a-time CPFSK**.
|
|
139
139
|
`--modem-num-tones 16` means "16 possible frequencies to pick from
|
|
140
140
|
per symbol", **not** "16 frequencies playing simultaneously". The
|
|
141
141
|
transmitter emits exactly one sinusoid at any instant, hopping between
|
|
142
142
|
frequencies at the symbol clock. Constant envelope (PAPR = 3 dB, the
|
|
143
|
-
peak-to-RMS of a pure sine) regardless of
|
|
144
|
-
|
|
145
|
-
Time-frequency view of a 4-FSK burst — each column is one symbol,
|
|
146
|
-
each row is one of the 4 available frequencies:
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
freq
|
|
150
|
-
↑
|
|
151
|
-
F₃ │ . ▓ . . . ▓ . .
|
|
152
|
-
F₂ │ . . . ▓ . . . .
|
|
153
|
-
F₁ │ ▓ . . . . . ▓ .
|
|
154
|
-
F₀ │ . . ▓ . ▓ . . ▓
|
|
155
|
-
└────────────────────────→ time
|
|
156
|
-
s₀ s₁ s₂ s₃ s₄ s₅ s₆ s₇
|
|
157
|
-
|
|
158
|
-
Exactly one ▓ per column: at any instant, one tone at full amplitude.
|
|
159
|
-
Over 8 symbols the modem has visited all 4 slots (that's what you see
|
|
160
|
-
on an SDR waterfall over time), but never more than one at once.
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
vs. what parallel multi-tone (OFDM-style; **not** this modem) would do:
|
|
164
|
-
|
|
165
|
-
```
|
|
166
|
-
freq
|
|
167
|
-
↑
|
|
168
|
-
F₃ │ ▓ . ▓ ▓ . ▓ . ▓
|
|
169
|
-
F₂ │ . ▓ ▓ . ▓ . ▓ ▓ Multiple ▓ per column: N tones
|
|
170
|
-
F₁ │ ▓ ▓ . ▓ . ▓ ▓ . summed at each instant. PAPR grows
|
|
171
|
-
F₀ │ ▓ . ▓ . ▓ ▓ . ▓ with N (~10·log₁₀(N)).
|
|
172
|
-
└────────────────────────→ time
|
|
173
|
-
s₀ s₁ s₂ s₃ s₄ s₅ s₆ s₇
|
|
174
|
-
```
|
|
143
|
+
peak-to-RMS of a pure sine) regardless of M.
|
|
175
144
|
|
|
176
145
|
Why single-tone:
|
|
177
146
|
|
|
178
147
|
- **All transmit power in one tone at a time** — maximum per-symbol SNR, no `1/N` power split across a stack.
|
|
179
|
-
- **Higher
|
|
148
|
+
- **Higher M buys log₂(M) bits/symbol** (more throughput) without any PAPR cost. 16-FSK carries 4× the bits of 2-FSK at the same baud, same peak power.
|
|
180
149
|
|
|
181
150
|
On an SDR waterfall you'll see all N frequency slots "lit up" during a long transmission — that's the display integrating over time, showing every slot the modem visited. Zoom the FFT window below one symbol duration (~3.3 ms at 300 baud) and you'll see the transmitter chasing one tone across the slots instead.
|
|
182
151
|
|
|
@@ -197,7 +166,7 @@ On an SDR waterfall you'll see all N frequency slots "lit up" during a long tran
|
|
|
197
166
|
## Full SNR sweep
|
|
198
167
|
|
|
199
168
|
Every baud × num_tones × RS × repeats combo is measured in
|
|
200
|
-
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.
|
|
169
|
+
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.4/results.md). Re-run `poetry run weaklink-modem-benchmark` to
|
|
201
170
|
refresh.
|
|
202
171
|
|
|
203
172
|
---
|
|
@@ -219,7 +188,7 @@ refresh.
|
|
|
219
188
|
| Flag | Default | Description |
|
|
220
189
|
|------|---------|-------------|
|
|
221
190
|
| `--modem-baud N` | `300` | Symbol rate. Only `45`, `300`, `1200` supported. |
|
|
222
|
-
| `--modem-num-tones N` | `4` |
|
|
191
|
+
| `--modem-num-tones N` | `4` | MFSK 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. |
|
|
223
192
|
| `--modem-rs-data-bytes N` | preset | Reed-Solomon data bytes per block. |
|
|
224
193
|
| `--modem-rs-parity-bytes N` | preset | RS parity bytes. Corrects up to N/2 byte errors per block. |
|
|
225
194
|
| `--modem-no-rs-crc` | CRC on | Skip the CRC-32 inside each RS block. |
|
|
@@ -283,7 +252,7 @@ One tx session (live audio):
|
|
|
283
252
|
│ pilot │ pre │ slot 0 │ pre │ slot 1 │ pre │ ... │slot N-1│ pre │ pilot │
|
|
284
253
|
└────────┴─────┴────────┴─────┴────────┴─────┴─────┴────────┴─────┴────────┘
|
|
285
254
|
|
|
286
|
-
One RS block, data area (before conv + interleave +
|
|
255
|
+
One RS block, data area (before conv + interleave + MFSK):
|
|
287
256
|
|
|
288
257
|
┌── 1B ──┬── 2B ────┬──── rs_data − 3 B ────┬── 4B CRC ──┬── rs_parity B ──┐
|
|
289
258
|
│ length │block_idx │ payload (zero-padded) │ CRC-32 │ RS parity │
|
|
@@ -307,10 +276,11 @@ through the same `_StreamingRxPump` the CLI uses.
|
|
|
307
276
|
|
|
308
277
|
## Glossary
|
|
309
278
|
|
|
310
|
-
- **
|
|
279
|
+
- **MFSK** — M-ary Frequency Shift Keying. M possible tone frequencies; each symbol picks one and emits it → log₂(M) bits/symbol. Exactly one tone on the air at any instant (never a stack). Default M=4.
|
|
280
|
+
- **CPFSK** — Continuous-Phase FSK. Frequency changes between symbols with no phase discontinuity, so the envelope stays clean at symbol boundaries. This modem's MFSK is CPFSK.
|
|
311
281
|
- **Single-tone-at-a-time** — The whole family from OOK through 16-FSK emits at most one sinusoid at any instant. Envelope stays constant (PAPR = 3 dB, same as a pure sine) regardless of `--modem-num-tones`.
|
|
312
282
|
- **OOK** — On-off keying. `num_tones=1` mode: single carrier, symbol 0 = silence, symbol 1 = tone. 1 bit/symbol like 2-FSK but at the narrowest possible bandwidth (only the carrier + modulation sidelobes). Pays a few dB vs 2-FSK in AWGN.
|
|
313
|
-
- **PAPR** — Peak-to-average power ratio. 0 dB = pure DC; 3 dB = pure sine; ~10·log₁₀(
|
|
283
|
+
- **PAPR** — Peak-to-average power ratio. 0 dB = pure DC; 3 dB = pure sine; ~10·log₁₀(M) dB = M tones summed with random phases. This modem stays at 3 dB.
|
|
314
284
|
- **Preamble** — Fixed 32-symbol PN sequence bracketing every slot; RX locks timing / frequency / amplitude from it.
|
|
315
285
|
- **Slot** — Preamble + one RS-encoded block.
|
|
316
286
|
- **Block** — RS-encoded chunk carrying header + payload.
|
|
@@ -321,12 +291,12 @@ through the same `_StreamingRxPump` the CLI uses.
|
|
|
321
291
|
- **Interleaver** — Bit shuffle so bursts become isolated errors. Ours changes every block (32-permutation cycle).
|
|
322
292
|
- **Non-coherent demod** — Tone detection by energy; ~3 dB behind coherent.
|
|
323
293
|
- **LO offset** — Radio frequency error; we correct up to ±500 Hz.
|
|
324
|
-
- **Pilot** — Short random
|
|
294
|
+
- **Pilot** — Short random MFSK burst before / after every live TX.
|
|
325
295
|
- **SNR (dB)** — Signal-to-noise ratio. Negative = noise louder than signal.
|
|
326
296
|
- **AWGN** — Additive white Gaussian noise; the standard clean-channel noise model used by the benchmark.
|
|
327
297
|
- **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.
|
|
328
298
|
- **Best SNR / cliff** — Lowest SNR at which decode still works for a given config. Below it, everything breaks.
|
|
329
|
-
- **Nyquist theorem** — A signal is only recoverable if sampled above twice its highest frequency. In practice: tones must sit below sample_rate/2, and
|
|
299
|
+
- **Nyquist theorem** — A signal is only recoverable if sampled above twice its highest frequency. In practice: tones must sit below sample_rate/2, and MFSK tone spacing must be at least `1/T_symbol` for non-coherent orthogonality.
|
|
330
300
|
|
|
331
301
|
---
|
|
332
302
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "weaklink-modem"
|
|
3
|
-
version = "0.7.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.7.4"
|
|
4
|
+
description = "MFSK streaming modem for HF SSB and wider channels: Reed-Solomon framing, soft Viterbi, round-robin block repetition, byte-pipe semantics."
|
|
5
5
|
authors = ["ivica3730k"]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "readme-pypi.md"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated from README.md at tag v0.7.
|
|
1
|
+
<!-- generated from README.md at tag v0.7.4 by scripts/build_pypi_readme.py; do not hand-edit -->
|
|
2
2
|
|
|
3
3
|
# weaklink
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ with `tail -f`; no memory buffering, no wait-for-EOF. RS + convolutional
|
|
|
8
8
|
soft-LLR combining. Modes: **OOK (1-tone)** through **16-FSK**, at
|
|
9
9
|
**45 / 300 / 1200 baud**.
|
|
10
10
|
|
|
11
|
-

|
|
12
12
|
|
|
13
13
|
Distribution: `weaklink-modem` (PyPI + release binaries).
|
|
14
14
|
|
|
@@ -92,7 +92,7 @@ cross-baud comparison convention, not a physical channel filter.
|
|
|
92
92
|
|
|
93
93
|
Doubling `--modem-block-repeats` buys ~2–3 dB via soft-LLR combining
|
|
94
94
|
at proportional air time. Full sweep of every combo we test is in
|
|
95
|
-
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.
|
|
95
|
+
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.4/results.md).
|
|
96
96
|
|
|
97
97
|
### OOK / 1-tone mode
|
|
98
98
|
|
|
@@ -102,52 +102,21 @@ bandwidth of any mode (no tone stack — just the carrier and its
|
|
|
102
102
|
modulation sidelobes). Same 1 bit/symbol as 2-FSK but a few dB worse
|
|
103
103
|
in AWGN — the trade you make for the narrow spectrum. Cliff at
|
|
104
104
|
45 baud with `block_repeats=4`: ≈ −14 dB, matching 4-FSK at the same
|
|
105
|
-
settings. See [`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.
|
|
105
|
+
settings. See [`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.4/results.md) for the per-baud numbers.
|
|
106
106
|
|
|
107
|
-
### One tone at a time (constant envelope, any
|
|
107
|
+
### One tone at a time (constant envelope, any M)
|
|
108
108
|
|
|
109
109
|
Every mode in this modem is **single-tone-at-a-time CPFSK**.
|
|
110
110
|
`--modem-num-tones 16` means "16 possible frequencies to pick from
|
|
111
111
|
per symbol", **not** "16 frequencies playing simultaneously". The
|
|
112
112
|
transmitter emits exactly one sinusoid at any instant, hopping between
|
|
113
113
|
frequencies at the symbol clock. Constant envelope (PAPR = 3 dB, the
|
|
114
|
-
peak-to-RMS of a pure sine) regardless of
|
|
115
|
-
|
|
116
|
-
Time-frequency view of a 4-FSK burst — each column is one symbol,
|
|
117
|
-
each row is one of the 4 available frequencies:
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
freq
|
|
121
|
-
↑
|
|
122
|
-
F₃ │ . ▓ . . . ▓ . .
|
|
123
|
-
F₂ │ . . . ▓ . . . .
|
|
124
|
-
F₁ │ ▓ . . . . . ▓ .
|
|
125
|
-
F₀ │ . . ▓ . ▓ . . ▓
|
|
126
|
-
└────────────────────────→ time
|
|
127
|
-
s₀ s₁ s₂ s₃ s₄ s₅ s₆ s₇
|
|
128
|
-
|
|
129
|
-
Exactly one ▓ per column: at any instant, one tone at full amplitude.
|
|
130
|
-
Over 8 symbols the modem has visited all 4 slots (that's what you see
|
|
131
|
-
on an SDR waterfall over time), but never more than one at once.
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
vs. what parallel multi-tone (OFDM-style; **not** this modem) would do:
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
freq
|
|
138
|
-
↑
|
|
139
|
-
F₃ │ ▓ . ▓ ▓ . ▓ . ▓
|
|
140
|
-
F₂ │ . ▓ ▓ . ▓ . ▓ ▓ Multiple ▓ per column: N tones
|
|
141
|
-
F₁ │ ▓ ▓ . ▓ . ▓ ▓ . summed at each instant. PAPR grows
|
|
142
|
-
F₀ │ ▓ . ▓ . ▓ ▓ . ▓ with N (~10·log₁₀(N)).
|
|
143
|
-
└────────────────────────→ time
|
|
144
|
-
s₀ s₁ s₂ s₃ s₄ s₅ s₆ s₇
|
|
145
|
-
```
|
|
114
|
+
peak-to-RMS of a pure sine) regardless of M.
|
|
146
115
|
|
|
147
116
|
Why single-tone:
|
|
148
117
|
|
|
149
118
|
- **All transmit power in one tone at a time** — maximum per-symbol SNR, no `1/N` power split across a stack.
|
|
150
|
-
- **Higher
|
|
119
|
+
- **Higher M buys log₂(M) bits/symbol** (more throughput) without any PAPR cost. 16-FSK carries 4× the bits of 2-FSK at the same baud, same peak power.
|
|
151
120
|
|
|
152
121
|
On an SDR waterfall you'll see all N frequency slots "lit up" during a long transmission — that's the display integrating over time, showing every slot the modem visited. Zoom the FFT window below one symbol duration (~3.3 ms at 300 baud) and you'll see the transmitter chasing one tone across the slots instead.
|
|
153
122
|
|
|
@@ -168,7 +137,7 @@ On an SDR waterfall you'll see all N frequency slots "lit up" during a long tran
|
|
|
168
137
|
## Full SNR sweep
|
|
169
138
|
|
|
170
139
|
Every baud × num_tones × RS × repeats combo is measured in
|
|
171
|
-
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.
|
|
140
|
+
[`results.md`](https://github.com/ivica3730k/weaklink-9a3ice/blob/v0.7.4/results.md). Re-run `poetry run weaklink-modem-benchmark` to
|
|
172
141
|
refresh.
|
|
173
142
|
|
|
174
143
|
---
|
|
@@ -190,7 +159,7 @@ refresh.
|
|
|
190
159
|
| Flag | Default | Description |
|
|
191
160
|
|------|---------|-------------|
|
|
192
161
|
| `--modem-baud N` | `300` | Symbol rate. Only `45`, `300`, `1200` supported. |
|
|
193
|
-
| `--modem-num-tones N` | `4` |
|
|
162
|
+
| `--modem-num-tones N` | `4` | MFSK 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. |
|
|
194
163
|
| `--modem-rs-data-bytes N` | preset | Reed-Solomon data bytes per block. |
|
|
195
164
|
| `--modem-rs-parity-bytes N` | preset | RS parity bytes. Corrects up to N/2 byte errors per block. |
|
|
196
165
|
| `--modem-no-rs-crc` | CRC on | Skip the CRC-32 inside each RS block. |
|
|
@@ -254,7 +223,7 @@ One tx session (live audio):
|
|
|
254
223
|
│ pilot │ pre │ slot 0 │ pre │ slot 1 │ pre │ ... │slot N-1│ pre │ pilot │
|
|
255
224
|
└────────┴─────┴────────┴─────┴────────┴─────┴─────┴────────┴─────┴────────┘
|
|
256
225
|
|
|
257
|
-
One RS block, data area (before conv + interleave +
|
|
226
|
+
One RS block, data area (before conv + interleave + MFSK):
|
|
258
227
|
|
|
259
228
|
┌── 1B ──┬── 2B ────┬──── rs_data − 3 B ────┬── 4B CRC ──┬── rs_parity B ──┐
|
|
260
229
|
│ length │block_idx │ payload (zero-padded) │ CRC-32 │ RS parity │
|
|
@@ -278,10 +247,11 @@ through the same `_StreamingRxPump` the CLI uses.
|
|
|
278
247
|
|
|
279
248
|
## Glossary
|
|
280
249
|
|
|
281
|
-
- **
|
|
250
|
+
- **MFSK** — M-ary Frequency Shift Keying. M possible tone frequencies; each symbol picks one and emits it → log₂(M) bits/symbol. Exactly one tone on the air at any instant (never a stack). Default M=4.
|
|
251
|
+
- **CPFSK** — Continuous-Phase FSK. Frequency changes between symbols with no phase discontinuity, so the envelope stays clean at symbol boundaries. This modem's MFSK is CPFSK.
|
|
282
252
|
- **Single-tone-at-a-time** — The whole family from OOK through 16-FSK emits at most one sinusoid at any instant. Envelope stays constant (PAPR = 3 dB, same as a pure sine) regardless of `--modem-num-tones`.
|
|
283
253
|
- **OOK** — On-off keying. `num_tones=1` mode: single carrier, symbol 0 = silence, symbol 1 = tone. 1 bit/symbol like 2-FSK but at the narrowest possible bandwidth (only the carrier + modulation sidelobes). Pays a few dB vs 2-FSK in AWGN.
|
|
284
|
-
- **PAPR** — Peak-to-average power ratio. 0 dB = pure DC; 3 dB = pure sine; ~10·log₁₀(
|
|
254
|
+
- **PAPR** — Peak-to-average power ratio. 0 dB = pure DC; 3 dB = pure sine; ~10·log₁₀(M) dB = M tones summed with random phases. This modem stays at 3 dB.
|
|
285
255
|
- **Preamble** — Fixed 32-symbol PN sequence bracketing every slot; RX locks timing / frequency / amplitude from it.
|
|
286
256
|
- **Slot** — Preamble + one RS-encoded block.
|
|
287
257
|
- **Block** — RS-encoded chunk carrying header + payload.
|
|
@@ -292,12 +262,12 @@ through the same `_StreamingRxPump` the CLI uses.
|
|
|
292
262
|
- **Interleaver** — Bit shuffle so bursts become isolated errors. Ours changes every block (32-permutation cycle).
|
|
293
263
|
- **Non-coherent demod** — Tone detection by energy; ~3 dB behind coherent.
|
|
294
264
|
- **LO offset** — Radio frequency error; we correct up to ±500 Hz.
|
|
295
|
-
- **Pilot** — Short random
|
|
265
|
+
- **Pilot** — Short random MFSK burst before / after every live TX.
|
|
296
266
|
- **SNR (dB)** — Signal-to-noise ratio. Negative = noise louder than signal.
|
|
297
267
|
- **AWGN** — Additive white Gaussian noise; the standard clean-channel noise model used by the benchmark.
|
|
298
268
|
- **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.
|
|
299
269
|
- **Best SNR / cliff** — Lowest SNR at which decode still works for a given config. Below it, everything breaks.
|
|
300
|
-
- **Nyquist theorem** — A signal is only recoverable if sampled above twice its highest frequency. In practice: tones must sit below sample_rate/2, and
|
|
270
|
+
- **Nyquist theorem** — A signal is only recoverable if sampled above twice its highest frequency. In practice: tones must sit below sample_rate/2, and MFSK tone spacing must be at least `1/T_symbol` for non-coherent orthogonality.
|
|
301
271
|
|
|
302
272
|
---
|
|
303
273
|
|
|
@@ -119,7 +119,7 @@ def _find_cliff(config: Config, *, trials: int, payload: bytes) -> Result:
|
|
|
119
119
|
shannon = shannon_snr_db(info_rate)
|
|
120
120
|
|
|
121
121
|
cliff: float | None = None
|
|
122
|
-
# OOK's cliff sits ~10 dB higher than
|
|
122
|
+
# OOK's cliff sits ~10 dB higher than MFSK, so the sweep needs a
|
|
123
123
|
# higher starting SNR to see it. Everything else still starts at
|
|
124
124
|
# +10 dB, matching the previous behaviour.
|
|
125
125
|
snr_db = 25.0 if config.num_tones == 1 else 10.0
|
|
@@ -64,7 +64,7 @@ def _add_modem_args(sub: argparse.ArgumentParser) -> None:
|
|
|
64
64
|
default=None,
|
|
65
65
|
dest="modem_num_tones",
|
|
66
66
|
choices=(1, 2, 4, 8, 16),
|
|
67
|
-
help="Number of
|
|
67
|
+
help="Number of MFSK tones. 4 (default) is standard; 8/16 pack "
|
|
68
68
|
"more bits per symbol at wider bandwidth and worse cliff. 2 "
|
|
69
69
|
"halves throughput but fits narrow audio paths. 1 selects OOK "
|
|
70
70
|
"(single carrier, on/off keying) -- narrowest bandwidth of any "
|
|
@@ -128,7 +128,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
128
128
|
except PackageNotFoundError:
|
|
129
129
|
version = "unknown"
|
|
130
130
|
|
|
131
|
-
parser = argparse.ArgumentParser(prog="weaklink-modem", description="Streaming
|
|
131
|
+
parser = argparse.ArgumentParser(prog="weaklink-modem", description="Streaming MFSK modem.")
|
|
132
132
|
parser.add_argument("--version", action="version", version=f"weaklink-modem {version}")
|
|
133
133
|
subparsers = parser.add_subparsers(dest="direction", required=True)
|
|
134
134
|
tx_parser = subparsers.add_parser("tx", help="Encode stdin bytes and transmit (or write to WAV).")
|
|
@@ -111,7 +111,7 @@ def _preamble_deterministic_sidelobe(num_tones: int) -> float:
|
|
|
111
111
|
max_score = max(max_score, abs(numerator / denominator))
|
|
112
112
|
return max_score
|
|
113
113
|
|
|
114
|
-
#
|
|
114
|
+
# MFSK correlator: score = (M * wanted - total) / ((M-1) * total).
|
|
115
115
|
for shift in range(1, length):
|
|
116
116
|
magnitudes = np.zeros((length, num_tones), dtype=np.float64)
|
|
117
117
|
# Positions inside the overlap carry the shifted preamble's tone.
|
|
@@ -820,7 +820,7 @@ def _find_preamble_peaks(
|
|
|
820
820
|
mag_col = magnitudes[:, 0]
|
|
821
821
|
raw = np.correlate(mag_col, tone_mask, mode="valid") - np.correlate(mag_col, silence_mask, mode="valid")
|
|
822
822
|
else:
|
|
823
|
-
#
|
|
823
|
+
# MFSK: for each tone k, mask marks where the preamble expects
|
|
824
824
|
# that tone; sum the wanted energy across all N tones.
|
|
825
825
|
wanted = np.zeros(max_offset + 1, dtype=np.float64)
|
|
826
826
|
for tone in range(num_tones):
|
|
@@ -846,7 +846,7 @@ def _find_preamble_peaks(
|
|
|
846
846
|
cutoff = float(np.quantile(abs_scores, 0.75))
|
|
847
847
|
noise_pool = scores[abs_scores <= cutoff]
|
|
848
848
|
else:
|
|
849
|
-
#
|
|
849
|
+
# MFSK noise floor: bottom (2/M) of scores (adaptive quantile).
|
|
850
850
|
# At M=2 sidelobes fill the lower half, so we go lower (Q0.25).
|
|
851
851
|
# At higher M sidelobes are tighter and the lower half is fine.
|
|
852
852
|
q = max(0.25, 1.0 - 2.0 / num_tones)
|
|
@@ -24,7 +24,7 @@ BAUD_PRESETS: dict[float, dict[str, float]] = {
|
|
|
24
24
|
|
|
25
25
|
#: Pilot padding on each side of live-tx: brings the audio sink out
|
|
26
26
|
#: of its IDLE state (~50 ms of wake-up latency) and gives the
|
|
27
|
-
#: coarse-offset FFT real
|
|
27
|
+
#: coarse-offset FFT real MFSK tone energy to lock onto.
|
|
28
28
|
LIVE_TX_PILOT_MIN_SECONDS: float = 0.2
|
|
29
29
|
|
|
30
30
|
#: Pilot must also exceed the preamble in symbol space so back-to-back
|
|
@@ -18,7 +18,7 @@ class ConfigError(WeaklinkError):
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class NyquistError(ConfigError):
|
|
21
|
-
"""
|
|
21
|
+
"""MFSK tone stack won't fit under sample_rate/2, or spacing is
|
|
22
22
|
below the non-coherent orthogonality floor."""
|
|
23
23
|
|
|
24
24
|
|
|
@@ -29,7 +29,7 @@ _log = logging.getLogger("weaklink.streaming")
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def pilot_signal(config: ModemConfig, duration_seconds: float) -> np.ndarray:
|
|
32
|
-
"""Random
|
|
32
|
+
"""Random MFSK symbols for ``duration_seconds``. All tones exercised
|
|
33
33
|
uniformly so the coarse-offset FFT locks cleanly."""
|
|
34
34
|
symbols_needed = max(1, int(round(duration_seconds * config.waveform.baud)))
|
|
35
35
|
rng = np.random.default_rng(0xC0DE)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""MFSK CPFSK modulator + non-coherent soft demodulator.
|
|
2
2
|
|
|
3
3
|
Continuous-phase for narrow spectrum, non-coherent I/Q magnitudes (~3 dB
|
|
4
4
|
worse than coherent but no carrier recovery). Gray-coded symbols so
|
|
@@ -25,7 +25,7 @@ NUM_TONES = 4
|
|
|
25
25
|
def _num_symbols(num_tones: int) -> int:
|
|
26
26
|
"""Size of the symbol alphabet. For ``num_tones=1`` we run OOK
|
|
27
27
|
(1 frequency, 2 symbols: silence + tone); otherwise symbols and
|
|
28
|
-
tones are 1:1 as in standard
|
|
28
|
+
tones are 1:1 as in standard MFSK."""
|
|
29
29
|
return 2 if num_tones == 1 else num_tones
|
|
30
30
|
|
|
31
31
|
|
|
@@ -70,7 +70,7 @@ class WaveformConfig:
|
|
|
70
70
|
amplitude: float = 0.25
|
|
71
71
|
"""Peak amplitude, well under 1.0 to leave headroom in WAV / audio devices."""
|
|
72
72
|
num_tones: int = 4
|
|
73
|
-
"""Number of tone frequencies. Standard
|
|
73
|
+
"""Number of tone frequencies. Standard MFSK for values in
|
|
74
74
|
{2, 4, 8, 16}: each tone is one symbol. ``num_tones=1`` selects
|
|
75
75
|
OOK -- one carrier, symbol 0 = silence, symbol 1 = tone --
|
|
76
76
|
which trades throughput for the narrowest bandwidth possible."""
|
|
@@ -83,7 +83,7 @@ class WaveformConfig:
|
|
|
83
83
|
def __post_init__(self) -> None:
|
|
84
84
|
if self.num_tones != 1 and (self.num_tones < 2 or (self.num_tones & (self.num_tones - 1)) != 0):
|
|
85
85
|
raise ConfigError(f"num_tones must be 1 or a power of 2 >= 2, got {self.num_tones}")
|
|
86
|
-
# OOK sits on a single carrier at ``center_hz``;
|
|
86
|
+
# OOK sits on a single carrier at ``center_hz``; MFSK spreads
|
|
87
87
|
# symmetrically around ``center_hz`` at ``tone_spacing_hz``.
|
|
88
88
|
if self.num_tones == 1:
|
|
89
89
|
relative_offsets: tuple[float, ...] = (0.0,)
|
|
@@ -114,7 +114,7 @@ class WaveformConfig:
|
|
|
114
114
|
|
|
115
115
|
@property
|
|
116
116
|
def num_symbols(self) -> int:
|
|
117
|
-
"""Symbol-alphabet size. Equals ``num_tones`` for standard
|
|
117
|
+
"""Symbol-alphabet size. Equals ``num_tones`` for standard MFSK;
|
|
118
118
|
equals 2 for OOK (``num_tones=1``)."""
|
|
119
119
|
return _num_symbols(self.num_tones)
|
|
120
120
|
|
|
@@ -142,7 +142,7 @@ def symbols_to_bits(symbols: np.ndarray, num_tones: int = NUM_TONES) -> bytes:
|
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
def modulate(symbols: np.ndarray, config: WaveformConfig) -> np.ndarray:
|
|
145
|
-
"""Vectorised modulator: continuous-phase
|
|
145
|
+
"""Vectorised modulator: continuous-phase MFSK for ``num_tones >= 2``;
|
|
146
146
|
on-off keying for ``num_tones == 1`` (symbol 0 = silence, symbol 1 =
|
|
147
147
|
continuous-phase sine at the carrier)."""
|
|
148
148
|
samples_per_symbol = config.samples_per_symbol
|
|
@@ -164,7 +164,7 @@ def modulate(symbols: np.ndarray, config: WaveformConfig) -> np.ndarray:
|
|
|
164
164
|
gate = symbols_int.astype(np.float64)[:, None]
|
|
165
165
|
return (config.amplitude * gate * carrier).ravel().astype(np.float32)
|
|
166
166
|
|
|
167
|
-
# Standard
|
|
167
|
+
# Standard MFSK: per-symbol frequency + continuous phase.
|
|
168
168
|
omega_per_symbol = 2.0 * np.pi * np.asarray(config.tones_hz, dtype=np.float64)[symbols_int] * dt
|
|
169
169
|
# Phase at the START of each symbol = 0 for i=0, cumsum(omega*sps) shifted for i>=1.
|
|
170
170
|
end_phases = np.cumsum(omega_per_symbol * samples_per_symbol)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|