hamlib-tci-sidecar 0.1.0__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.
- hamlib_tci_sidecar-0.1.0/CLAUDE.md +391 -0
- hamlib_tci_sidecar-0.1.0/LICENSE +21 -0
- hamlib_tci_sidecar-0.1.0/MANIFEST.in +25 -0
- hamlib_tci_sidecar-0.1.0/PKG-INFO +265 -0
- hamlib_tci_sidecar-0.1.0/README.md +242 -0
- hamlib_tci_sidecar-0.1.0/hamlib_tci_sidecar.egg-info/PKG-INFO +265 -0
- hamlib_tci_sidecar-0.1.0/hamlib_tci_sidecar.egg-info/SOURCES.txt +16 -0
- hamlib_tci_sidecar-0.1.0/hamlib_tci_sidecar.egg-info/dependency_links.txt +1 -0
- hamlib_tci_sidecar-0.1.0/hamlib_tci_sidecar.egg-info/requires.txt +1 -0
- hamlib_tci_sidecar-0.1.0/hamlib_tci_sidecar.egg-info/top_level.txt +1 -0
- hamlib_tci_sidecar-0.1.0/js8_tx_test.py +183 -0
- hamlib_tci_sidecar-0.1.0/monitor_tx.py +195 -0
- hamlib_tci_sidecar-0.1.0/pyproject.toml +43 -0
- hamlib_tci_sidecar-0.1.0/requirements.txt +5 -0
- hamlib_tci_sidecar-0.1.0/setup.cfg +4 -0
- hamlib_tci_sidecar-0.1.0/tci-sidecar-linux.py +697 -0
- hamlib_tci_sidecar-0.1.0/test_audio_simple.py +94 -0
- hamlib_tci_sidecar-0.1.0/test_tx.py +178 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# CLAUDE.md — hamlib-tci-sidecar/
|
|
2
|
+
|
|
3
|
+
TCI audio bridge for connecting JS8Call, fldigi, WSJT-X, and other ham radio applications to the SunSDR2 Pro via ExpertSDR3.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Provides bidirectional audio + CAT control bridge between ham radio software and ExpertSDR3's TCI WebSocket interface and proxies audio to a sidecar (this program), which in turn makes it available via virtual audio devices for the ham radio software to use.
|
|
8
|
+
|
|
9
|
+
Single TCI connection from rigctld, external Python process handles audio via TCP socket connected to rigctld.
|
|
10
|
+
|
|
11
|
+
rigctld runs just like it does for any other radio unless you configure and run the sidecar. when you do that, it proxies audio. it does this because expertsdr3 will only send audio to the TCI client that requests PTT. there is no other architecture that solves the problem.
|
|
12
|
+
|
|
13
|
+
the architecture is extremely simple: rigctld connects to expertsdr3. if you want to do tci audio, the audio sidecar connects to rigctld. when the user wants to transmit audio from the sidecar, rigctld issues a PTT command with the tci option. various packets come from expertsdr3, such as TX_CHRONO and binary RX packets. these should all be forwarded to the sidecar. transmitted audio comes from the sidecar and should be forwarded by rigctld to expertsdr3. PTT is triggered by the user using CAT. rigctld does not handle audio, other than to proxy it between expertsdr3 and the sidecar.
|
|
14
|
+
|
|
15
|
+
- Clean separation: rigctld handles radio control, sidecar handles audio
|
|
16
|
+
- TX_CHRONO and all audio packets are routed to the sidecar for TX audio.
|
|
17
|
+
- sidecar is optional (can run rigctld standalone without audio)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
JS8Call → rigctld (localhost:4532) ← Audio Sidecar (socket 4534)
|
|
21
|
+
↓ ↑
|
|
22
|
+
TCI (localhost:50001) Virtual Audio
|
|
23
|
+
(tci-rx, tci-tx)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
the test system is 10.1.1.52. it is a linux machine with a connected sunsdr2 pro and is running expertsdr3 with tci enabled.
|
|
27
|
+
|
|
28
|
+
the hamlib system in ~/Dropbox/claude/Hamlib/ has been modified to support TCI v2.0. You must run rigctld from this directory and point LD_LIBRARY_PATH to the right library for it to work. the build system is questionable and doesn't always rebuild everything completely. when making changes to tci2.c in this tree, be certain both rigctld and the .so shared library were both actually rebuilt successfully before using them. the machine id for rigctld for TCI is 12. note that repeated disconnections and reconnections to expersdr3 cause audio to stop working. expersdr3 was run from the shell in a loop. if you kill all of the expertsdr3 processes, it will sleep for three seconds and then restart. you have to do this when you wedge the audio system with too many disconnects.
|
|
29
|
+
|
|
30
|
+
there are no options in expertsdr3 for configuring tci other than on/off and port number. it's turned on and on the standard port 50001. tci should start sending audio the moment you connect and ask it to. there are no buttons for the user to press. tci audio is routed to the radio for transmission automatically when the right tci commands are given to transmit. there are no options for the user to select. do not ask.
|
|
31
|
+
|
|
32
|
+
- the official TCI documentation is at ~/Dropbox/claude/Hamlib/TCI_Protocol.pdf
|
|
33
|
+
- eesdr-tci is a python library installed on this machine that successfully sends and receives audio via TCI
|
|
34
|
+
- there is working c++ code to use as an example at https://github.com/maksimus1210/TCI
|
|
35
|
+
|
|
36
|
+
JS8Call serves as an example of the program that will be used with this solution.
|
|
37
|
+
|
|
38
|
+
rigctld runs on port 4532 and JS8Call is configured to talk to that port.
|
|
39
|
+
|
|
40
|
+
JS8Call is configured to use tci-tx and tci-rx.monitor as audio devices.
|
|
41
|
+
|
|
42
|
+
there was already an aborted attempt to build this. it got too bogged down and broken and I gave up to start over with this project.
|
|
43
|
+
|
|
44
|
+
It is critical to constantly update this file with new knowledge that is learned. when your context compacts, you get stupid, forget things, and we have to repeat the same work. documenting it here constantly helps prevent that.
|
|
45
|
+
|
|
46
|
+
old work on this project is in ~/Dropbox/claude/rf-bench/projects/sunsdr/tci-audiopipe/. it doesn't work, but there might be some useful clues in there.
|
|
47
|
+
|
|
48
|
+
I want you to build and test this with your own test client, not js8call. we'll try js8call once things seem to be working. I want you to build your own client that will connect to rigctld, change frequencies and modes, and send and receive audio. I don't actually need to hear the audio, but I need audio support so you can verify that it's receiving audio and I need you to generate audio (a 1khz tone is perfectly fine) to transmit for testing.
|
|
49
|
+
|
|
50
|
+
by monitoring the tci traffic in rigctld and the sidecar, you can verify that audio is being correctly sent and received without me needing to check.
|
|
51
|
+
|
|
52
|
+
Here is what I want:
|
|
53
|
+
|
|
54
|
+
1. make sure rigctld is doing the basic functions correctly. set some frequencies and read them back. set some modes and read them back. set some splits and make sure they set correctly. when that's working, set the frequency to 14078 khz and the mode to usb digital with no split. trigger PTT and make certain the radio switches to PTT mode. then turn it off. when all of this works, this step is done.
|
|
55
|
+
|
|
56
|
+
2. get received audio working. the failed implementation at least did this much right. start rigctld and the sidecar, trigger received audio (you can steal that code from the other project), and monitor packet flow to make certain it works. note that the audio format resets every time and you have to set it yourself each time for the format of data you want. when you can reliably get audio 100% of the time into the sidecar, this step is complete.
|
|
57
|
+
|
|
58
|
+
3. complete the receive path code in the sidecar, then write a tci client that connects to the sidecar, sets a mode and frequency (14078 khz, usb digital) and triggers audio receive. then pipe that audio to a virtual sound device named tci-rx.monitor. make sure the tci client you wrote can "hear" audio from that interface. when all of this works, this step is done.
|
|
59
|
+
|
|
60
|
+
4. add TCI transmit to the sidecar. modify the client to transmit a 1 khz audio tone via tci. have it connect to rigctld, trigger RX, listen for received audio, then trigger tci transmit and send the 1 khz tone. verify that expertsdr3 sends TX_CHRONO. set the configuration for the TX data. verify that the 1khz is flowing into the virtual sound device (tci-tx), through the sidecar, through rigctld, and to expertsdr3. check the packet flow at every step. when TX_CHRONO and audio TX packets are flowing, this step is complete.
|
|
61
|
+
|
|
62
|
+
you may have to refactor the tci2.c code to make things work right. RX audio seemed to be working in the old broken version, but TX audio did not work. do not be afraid to make changes. but only one TCI connection to expertsdr3. no exceptions. rigctld is your proxy. rigctld is not to do any processing of data, it should only proxy the audio data when configured to do so.
|
|
63
|
+
|
|
64
|
+
expertsdr3 will automatically restart if you kill ALL of the expersdr3 processes. there are multiple. if it's not restarting within ten or fiften seconds after killing the processes, you missed at least one.
|
|
65
|
+
|
|
66
|
+
stop and ask me clarifying questions along the way. I want this built right. under no circumstances is anything to be pushed to github, pypi, or anywhere else. all code stays on 10.1.1.52.
|
|
67
|
+
|
|
68
|
+
you are free to install and use any tools you need to get the job done on 10.1.1.52, so long as you're not uploading or posting code to the internet.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## CURRENT STATUS (2026-06-07 16:00) - WORKING incl. JS8Call
|
|
73
|
+
|
|
74
|
+
RX audio + TX audio + CAT confirmed end-to-end with `tci-sidecar-linux.py`,
|
|
75
|
+
**including JS8Call's TX hitting the spectrum analyzer at 14.0795 MHz at
|
|
76
|
+
~-45 dBm** (50% drive, 50 mW per RFPOWER_METER) when JS8 transmits a
|
|
77
|
+
heartbeat.
|
|
78
|
+
|
|
79
|
+
### Last bug found (2026-06-07): TX audio amplification
|
|
80
|
+
|
|
81
|
+
After fixing the framing bugs in tci2.c, `test_tx.py` (8 kHz pacat with
|
|
82
|
+
amplitude 0.7 = peak ~22 937) was generating clean TX at -46 dBm. But
|
|
83
|
+
**JS8Call's actual TX produced 0 W** even though TX_CHRONO/TX_AUDIO were
|
|
84
|
+
flowing.
|
|
85
|
+
|
|
86
|
+
Root cause: JS8Call writes audio at peak ~3 350 (about -20 dBFS).
|
|
87
|
+
ExpertSDR3 silently ignores TX audio below some threshold and reports 0 W
|
|
88
|
+
out -- no error, just no RF. Confirmed by direct TCI tests bypassing
|
|
89
|
+
rigctld: pacat at 0.30 amplitude (peak ~9800) at 48 kHz mono via the same
|
|
90
|
+
PipeWire 48->8 kHz resample path produced -46 dBm; JS8Call at 0.10
|
|
91
|
+
amplitude on the same path produced nothing.
|
|
92
|
+
|
|
93
|
+
Fix: `tci-sidecar-linux.py` now applies a configurable gain (`--tx-gain-db`,
|
|
94
|
+
default **+20 dB = 10x**) with clipping protection before shipping the
|
|
95
|
+
TX_AUDIO frame. This brings JS8Call's audio peak from ~3 350 to ~32 767
|
|
96
|
+
(saturated) -- well above ExpertSDR3's threshold. test_tx.py audio also
|
|
97
|
+
clips harmlessly (peak ~32 767 either way). Result: -45 dBm clean output
|
|
98
|
+
during the full ~9-second JS8 frame.
|
|
99
|
+
|
|
100
|
+
`--rx-gain-db` is also available (default **0 dB = unity**). Use a
|
|
101
|
+
negative value to attenuate or a positive value to boost RX before it
|
|
102
|
+
hits the `tci-rx` PulseAudio sink. Both gains are linear post-conversion
|
|
103
|
+
to dB on input, with `np.clip(...)` saturation to int16 range.
|
|
104
|
+
|
|
105
|
+
### Things found along the way (2026-06-07)
|
|
106
|
+
|
|
107
|
+
* **Lots of orphaned `tci-rx`/`tci-tx` PipeWire sinks accumulate from
|
|
108
|
+
prior sidecar runs.** PipeWire's name-based sink lookup may pick a
|
|
109
|
+
SUSPENDED orphan instead of the live RUNNING sink, and JS8Call's TX
|
|
110
|
+
audio goes to a black hole. Mitigation: `systemctl --user restart
|
|
111
|
+
pipewire pipewire-pulse wireplumber` before starting the sidecar.
|
|
112
|
+
This is mentioned in the original CLAUDE.md but worth re-flagging.
|
|
113
|
+
|
|
114
|
+
* **JS8Call's `TX.SEND_MESSAGE` API call queues TX text but does NOT
|
|
115
|
+
auto-press the TX button on stock JS8Call.** `send_heartbeat(grid)`
|
|
116
|
+
*does* trigger an actual TX cycle on the next 15-second slot
|
|
117
|
+
boundary. When using js8net-legacy to drive JS8Call programmatically,
|
|
118
|
+
prefer `send_heartbeat()` over `send_message()` if you actually need
|
|
119
|
+
the radio to key.
|
|
120
|
+
|
|
121
|
+
* **PipeWire transparently resamples 48 kHz mono (JS8Call's output rate)
|
|
122
|
+
to 8 kHz mono (the sink's rate).** No special configuration needed in
|
|
123
|
+
the sidecar. But it does add a few hundred milliseconds of buffer
|
|
124
|
+
latency, which is what the `tx_buf` length growing from 1 152 to
|
|
125
|
+
16 000 samples in the sidecar log captures.
|
|
126
|
+
|
|
127
|
+
### Diagnostic tooling (2026-06-07)
|
|
128
|
+
|
|
129
|
+
`/tmp/diag_js8_v2.sh` on 10.1.1.52 -- runs in parallel:
|
|
130
|
+
1. SSA marker sweep at 14.079 MHz +/- 2.5 kHz, 0.5 s cadence (30 s window)
|
|
131
|
+
2. `parec` capture of `tci-tx.monitor` to a raw file (30 s)
|
|
132
|
+
3. `pactl list short sink-inputs` snapshot every 0.5 s
|
|
133
|
+
4. `pactl list short source-outputs` snapshot every 0.5 s
|
|
134
|
+
5. rigctld CAT poll for `t` (PTT) + `l RFPOWER_METER` every 0.25 s
|
|
135
|
+
6. Triggers JS8Call TX via the legacy js8net API (`send_heartbeat`)
|
|
136
|
+
|
|
137
|
+
Helper: `/tmp/analyze_tci_tx.py` parses the parec dump for peak/RMS/loud
|
|
138
|
+
chunks; `/tmp/ssa_inline.py` is the SSA driver script.
|
|
139
|
+
|
|
140
|
+
These let me prove four-way correlation on a single test:
|
|
141
|
+
- JS8 wrote audio (parec dump non-zero, peak ~3 350)
|
|
142
|
+
- Sidecar shipped TX_AUDIO frames (sidecar log delta)
|
|
143
|
+
- Radio entered TX (rigctld `t` returns 1)
|
|
144
|
+
- RF actually appeared on the antenna (SSA marker > -50 dBm AND
|
|
145
|
+
RFPOWER_METER > 0)
|
|
146
|
+
|
|
147
|
+
If only the first three are true, you have the JS8Call-quiet-audio bug
|
|
148
|
+
above.
|
|
149
|
+
|
|
150
|
+
### Old top-level summary (still valid)
|
|
151
|
+
|
|
152
|
+
### TX path bugs found and fixed today (2026-06-07)
|
|
153
|
+
|
|
154
|
+
These were both in `tci2_audio_poll_thread` and were the reason yesterday's
|
|
155
|
+
"23:20 - WORKING" report was wrong (TX_CHRONO/TX_AUDIO flowed but ExpertSDR3
|
|
156
|
+
emitted 0 W -- nothing on the antenna).
|
|
157
|
+
|
|
158
|
+
**Bug A -- TX-from-sidecar code only ran when WS poll() timed out.**
|
|
159
|
+
The sidecar TX path was inside `if (poll_ret == 0) { ... }`. With sensors
|
|
160
|
+
streaming continuously, poll() almost never times out -- so sidecar bytes
|
|
161
|
+
sat in the kernel TCP buffer until enough quiet to trigger the timeout
|
|
162
|
+
branch, then got drained as a single chunk and forwarded as ONE WebSocket
|
|
163
|
+
frame (mangled framing). Fix: pump the sidecar every iteration; only
|
|
164
|
+
`continue` on poll-timeout AFTER pumping.
|
|
165
|
+
|
|
166
|
+
**Bug B -- partial-recv was forwarded as a complete TCI frame.**
|
|
167
|
+
The sidecar sends complete TCI frames (64-byte header + audio body) via
|
|
168
|
+
`sendall`, but TCP is a byte stream -- a 1088-byte frame can split across
|
|
169
|
+
recv() calls. The old code forwarded each `recv()` chunk as one
|
|
170
|
+
WebSocket frame. ExpertSDR3 silently dropped malformed frames -- TX
|
|
171
|
+
engaged but emitted 0 W. Fix: accumulate into `priv->audio_buf`, parse
|
|
172
|
+
the TCI 64-byte header to learn the total frame size, only emit when a
|
|
173
|
+
complete frame is buffered.
|
|
174
|
+
|
|
175
|
+
### Diagnostic that nailed it
|
|
176
|
+
|
|
177
|
+
`l RFPOWER_METER` and `l SWR` via rigctld reported 0 W and SWR=1.0 during
|
|
178
|
+
PTT, while a parallel `tune:0,true;` (pure carrier, no modulator) DID
|
|
179
|
+
produce -40 dBm at the antenna. That isolated the failure to "TCI audio
|
|
180
|
+
modulation path" rather than antenna/PA/CAT. A direct websockets-based
|
|
181
|
+
TCI client (bypassing rigctld) successfully transmitted the 1 kHz tone
|
|
182
|
+
at -46 dBm -- proving the bug was in rigctld's audio forwarding, not in
|
|
183
|
+
ExpertSDR3, sample format, or hardware.
|
|
184
|
+
|
|
185
|
+
### Verified end-to-end (2026-06-07 07:45)
|
|
186
|
+
|
|
187
|
+
| Test | Result |
|
|
188
|
+
|------|--------|
|
|
189
|
+
| `parec --device=tci-rx.monitor` | RMS 403, peak 10881, ~98% nonzero |
|
|
190
|
+
| 16 CAT cycles freq/mode/split + restore | all read back correct, final 14078 PKTUSB no split |
|
|
191
|
+
| TX 1 kHz tone, SSA at 14.079 MHz | **-46 dBm sustained for ~5 s during PTT, noise floor otherwise** |
|
|
192
|
+
| `l RFPOWER_METER` during TX | non-zero (was 0 before the fix) |
|
|
193
|
+
|
|
194
|
+
### TCI protocol notes (corrected)
|
|
195
|
+
|
|
196
|
+
The PDF shows uppercase commands (`START;`, `TRX:`, etc.) but the TCI
|
|
197
|
+
server emits and accepts **lowercase** (`start;`, `trx:`, `ready;`). The
|
|
198
|
+
parser is case-insensitive in both directions; either works on the wire.
|
|
199
|
+
|
|
200
|
+
`TRX:0,true;` alone does NOT cause RF emission -- the radio enters TX
|
|
201
|
+
state but needs an audio source. In CW it waits for keyer/CW_MACROS;
|
|
202
|
+
in SSB modes it pulls from whatever input is selected. For TCI-driven
|
|
203
|
+
SSB digital, send `TRX:0,true,tci;` AND stream `TX_AUDIO_STREAM` frames
|
|
204
|
+
in response to each `TX_CHRONO` request. `tune:0,true;` is a separate
|
|
205
|
+
ATU/PA-tuning path that emits a pure carrier independently of audio --
|
|
206
|
+
useful as a "is the radio actually able to TX?" sanity check.
|
|
207
|
+
The "rigctld crashing" report from 22:40 was a misdiagnosis (see "Sonnet's
|
|
208
|
+
red herrings" below). The real bugs were race conditions exposed by
|
|
209
|
+
sequential CAT reconnects. Fixed in `~/Dropbox/claude/Hamlib/rigs/dummy/tci2.c`.
|
|
210
|
+
|
|
211
|
+
### Real bugs and their fixes
|
|
212
|
+
|
|
213
|
+
**Bug 1 -- bind(4534) "Address already in use" on every CAT reconnect.**
|
|
214
|
+
`tci2_audio_init()` is called from `tci2_process_message()` on every TCI
|
|
215
|
+
READY, which fires every time `rig_open()` runs -- and rigctld calls
|
|
216
|
+
`rig_open()` for *each* CAT client. The original code re-created the
|
|
217
|
+
listening socket each time and failed because the previous one was still
|
|
218
|
+
bound. Fix: idempotent guard at top of `tci2_audio_init()`; if
|
|
219
|
+
`audio_listen_fd >= 0`, jump past the bind/listen and just (re)start the
|
|
220
|
+
audio reader thread.
|
|
221
|
+
|
|
222
|
+
**Bug 2 -- RPRT -5 on the FIRST CAT command after open.**
|
|
223
|
+
The audio reader thread and the CAT thread were both reading the same
|
|
224
|
+
WebSocket. When ExpertSDR3 emitted the response to a CAT query, whichever
|
|
225
|
+
thread won the race consumed it. If the audio thread won, the CAT thread
|
|
226
|
+
timed out (`tci2_recv_until` returned `-RIG_ETIMEOUT` -> -5). Fix:
|
|
227
|
+
single-reader architecture. The audio thread is now the sole reader;
|
|
228
|
+
text frames go into a 64-slot ring buffer (`text_q`) protected by
|
|
229
|
+
`q_mutex`+`q_cond`; binary frames forward to the sidecar. CAT thread's
|
|
230
|
+
`tci2_recv_until` pops from the queue when `reader_running` is set,
|
|
231
|
+
otherwise reads the socket directly (used during `tci2_open` before the
|
|
232
|
+
reader thread starts).
|
|
233
|
+
|
|
234
|
+
**Bug 3 -- TCI2_RECV_MAX too small.**
|
|
235
|
+
With sensor messages (RX_SENSORS_ENABLE/TX_SENSORS_ENABLE @ 200 ms each =
|
|
236
|
+
~10 frames/sec) piling up in the queue between CAT commands, the
|
|
237
|
+
old limit of 32 was exhausted before scanning past stale state to find
|
|
238
|
+
the matching reply. Bumped to 256.
|
|
239
|
+
|
|
240
|
+
**Bug 4 -- audio thread polled stale FD across rig_close.**
|
|
241
|
+
`rig_close` calls `port_close` which closes the WebSocket FD, but the
|
|
242
|
+
old `tci2_close` deliberately kept the audio thread alive. After close,
|
|
243
|
+
the thread spun on `POLLNVAL` because `pfd.fd = rp->fd` was now closed.
|
|
244
|
+
Fix: `tci2_close` now joins the audio thread; `tci2_audio_init`'s
|
|
245
|
+
idempotent path restarts the thread on the new WebSocket FD. Listen
|
|
246
|
+
socket (and sidecar connection if any) persist across CAT churn.
|
|
247
|
+
|
|
248
|
+
**Bug 5 -- `tci2_send` not thread-safe.**
|
|
249
|
+
The audio thread now calls `tci2_send` (AUDIO_START, etc.) concurrently
|
|
250
|
+
with the CAT thread. `tci2_send` now acquires `priv->ws_mutex` itself
|
|
251
|
+
instead of relying on callers; all WebSocket writes are serialized.
|
|
252
|
+
|
|
253
|
+
### Sonnet's red herrings (do NOT chase these again)
|
|
254
|
+
|
|
255
|
+
Sonnet's earlier report claimed:
|
|
256
|
+
- "FD_SETSIZE >= 1024" -- false. WS fd is always 3-5 in practice.
|
|
257
|
+
`select()` was fine.
|
|
258
|
+
- "rigctld crashing" -- false. rigctld was alive throughout; the
|
|
259
|
+
symptoms were RPRT -5 on the first CAT command, which Sonnet read
|
|
260
|
+
as a crash and chased into the wrong fix.
|
|
261
|
+
- "ExpertSDR3 fragmenting WebSocket frames" -- false. Sonnet's diagnostic
|
|
262
|
+
Python script had buggy frame parsing (didn't track FIN bits, lengths
|
|
263
|
+
off by one) and produced phantom opcodes 11/15. ExpertSDR3 sends
|
|
264
|
+
conventional FIN=1 frames.
|
|
265
|
+
|
|
266
|
+
### Architecture (current)
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
ExpertSDR3 (TCI v2.0, port 50001)
|
|
270
|
+
|
|
|
271
|
+
| one WebSocket
|
|
272
|
+
|
|
|
273
|
+
+-------- rigctld ---------+
|
|
274
|
+
| tci2_audio_poll_thread | <-- SOLE WS reader. Demuxes:
|
|
275
|
+
| (background) | binary -> audio_sidecar_fd (port 4534)
|
|
276
|
+
| reader_running=1 | text -> text_q + tci2_process_message
|
|
277
|
+
+--------------------------+
|
|
278
|
+
| CAT thread | <-- pops text_q in tci2_recv_until
|
|
279
|
+
| port 4532 (e.g. JS8) | writes via tci2_send (ws_mutex)
|
|
280
|
+
+--------------------------+
|
|
281
|
+
|
|
|
282
|
+
v
|
|
283
|
+
port 4534 (audio sidecar TCP socket; one client)
|
|
284
|
+
|
|
|
285
|
+
v
|
|
286
|
+
tci-sidecar-linux.py <-> tci-rx / tci-tx (PulseAudio sinks)
|
|
287
|
+
|
|
|
288
|
+
v
|
|
289
|
+
JS8Call / fldigi / WSJT-X / ...
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Verified end-to-end (2026-06-06 23:20)
|
|
293
|
+
|
|
294
|
+
- 5 sequential nc reconnects, `f` query: all return 14078055. No bind
|
|
295
|
+
errors, no -5.
|
|
296
|
+
- `test_audio_simple.py`: 20 RX_AUDIO frames, RMS ~320, peaks ~1100,
|
|
297
|
+
one frame with peak 13587 (real radio signal).
|
|
298
|
+
- 200-frame audio test: 15.7 fps stable.
|
|
299
|
+
- `test_tx.py --duration 5`: PTT on, TX_CHRONO requests + TX_AUDIO
|
|
300
|
+
responses flowing in sidecar log, PTT off.
|
|
301
|
+
|
|
302
|
+
### Build & deploy
|
|
303
|
+
|
|
304
|
+
The Hamlib build system silently uses stale convenience archives. To
|
|
305
|
+
get a clean rebuild that actually picks up tci2.c changes:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
cd ~/Dropbox/claude/Hamlib
|
|
309
|
+
|
|
310
|
+
# Force fresh tci2.o
|
|
311
|
+
make -C rigs/dummy tci2.lo
|
|
312
|
+
|
|
313
|
+
# Force fresh dummy convenience archive (this is the step the
|
|
314
|
+
# top-level make often skips)
|
|
315
|
+
rm -f rigs/dummy/.libs/libhamlib-dummy.a rigs/dummy/libhamlib-dummy.la
|
|
316
|
+
( cd rigs/dummy && make libhamlib-dummy.la )
|
|
317
|
+
|
|
318
|
+
# Force fresh shared library
|
|
319
|
+
rm -f src/libhamlib.la src/.libs/libhamlib.so.5*
|
|
320
|
+
( cd src && make libhamlib.la )
|
|
321
|
+
|
|
322
|
+
# Relink rigctld
|
|
323
|
+
touch tests/rigctld.c && make -C tests rigctld
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
To verify the new code is actually in the .so:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
strings src/.libs/libhamlib.so.5.0.0 | \
|
|
330
|
+
grep "audio listen socket already up" # must print
|
|
331
|
+
strings src/.libs/libhamlib.so.5.0.0 | \
|
|
332
|
+
grep "keeping audio thread alive" # must NOT print
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
To deploy to 10.1.1.52 (Dropbox sync is too slow for an active debug loop):
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
scp ~/Dropbox/claude/Hamlib/src/.libs/libhamlib.so.5.0.0 \
|
|
339
|
+
~/Dropbox/claude/Hamlib/tests/.libs/rigctld \
|
|
340
|
+
10.1.1.52:/tmp/
|
|
341
|
+
ssh 10.1.1.52 'pkill -9 rigctld; sleep 1; \
|
|
342
|
+
export LD_LIBRARY_PATH=/tmp; \
|
|
343
|
+
nohup /tmp/rigctld -m 12 -r 127.0.0.1:50001 -t 4532 \
|
|
344
|
+
-C audio_port=4534 -vvvv \
|
|
345
|
+
</dev/null >/tmp/rigctld.out 2>/tmp/rigctld.err & disown'
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
(Verbose output goes to stderr, not stdout. `-vvvv` gives 366+ lines
|
|
349
|
+
on startup; if you see only "Opened rig model 12, 'TCI 2.0'" you forgot
|
|
350
|
+
the redirection split.)
|
|
351
|
+
|
|
352
|
+
### Critical code locations (post-fix line numbers)
|
|
353
|
+
|
|
354
|
+
`~/Dropbox/claude/Hamlib/rigs/dummy/tci2.c`:
|
|
355
|
+
- ~70: `TCI2_RECV_MAX 256`, `TCI2_TEXT_Q_DEPTH 64`
|
|
356
|
+
- ~700: `tci2_send` (now locks ws_mutex internally)
|
|
357
|
+
- ~1175: `tci2_text_q_push / pop / drain`
|
|
358
|
+
- ~1240: `tci2_recv_until` (queue path when reader_running, direct otherwise)
|
|
359
|
+
- ~1485: `tci2_audio_init` (idempotent + restartable)
|
|
360
|
+
- ~1620: `tci2_audio_poll_thread` (sole-reader, queues text, accepts sidecar)
|
|
361
|
+
- ~1845: `tci2_close` (now joins audio thread; listen sock persists)
|
|
362
|
+
|
|
363
|
+
### Diagnostic commands
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
# ExpertSDR3 lifecycle
|
|
367
|
+
pgrep -a Expert; pgrep -a SdrApplication
|
|
368
|
+
pkill -9 ExpertSDR3; pkill -9 SdrApplication # auto-restarts in 10-15s
|
|
369
|
+
|
|
370
|
+
# rigctld
|
|
371
|
+
pgrep -a rigctld
|
|
372
|
+
ps -Lp $(pgrep rigctld) # thread count: should be 4
|
|
373
|
+
ss -tlnp | grep -E '(4532|4534|50001)' # listen sockets
|
|
374
|
+
ss -tn | grep -E ':50001' # WS ESTAB
|
|
375
|
+
|
|
376
|
+
# Audio sockets / orphaned PA sinks
|
|
377
|
+
pactl list sinks short | grep tci # tci-rx, tci-tx (one each ideal)
|
|
378
|
+
systemctl --user restart pipewire pipewire-pulse wireplumber # nukes orphans
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Important notes (carried over)
|
|
382
|
+
|
|
383
|
+
- **Only ONE TCI connection allowed** to ExpertSDR3. rigctld owns it; the
|
|
384
|
+
sidecar talks to rigctld over port 4534, never to ExpertSDR3 directly.
|
|
385
|
+
- **Process kills**: use specific names (`pkill -f tci_sidecar`), not
|
|
386
|
+
`pkill python3` (would kill unrelated services).
|
|
387
|
+
- **Test scripts** (all in `tci-sidecar-linux/`):
|
|
388
|
+
- `test_audio_simple.py` -- 20-frame RX sanity check, no PA needed
|
|
389
|
+
- `test_tx.py --duration N` -- 1 kHz tone TX via WAV
|
|
390
|
+
- `monitor_tx.py --duration 10 --threshold -50` -- SSA verifies RF
|
|
391
|
+
- `tci-sidecar-linux.py` -- full sidecar (creates tci-rx / tci-tx PA sinks)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeff Francis, N0GQ
|
|
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,25 @@
|
|
|
1
|
+
# Include the script (also in script-files) plus the docs and helper tools
|
|
2
|
+
include README.md
|
|
3
|
+
include LICENSE
|
|
4
|
+
include CLAUDE.md
|
|
5
|
+
include requirements.txt
|
|
6
|
+
include tci-sidecar-linux.py
|
|
7
|
+
|
|
8
|
+
# Helper / verification scripts that aren't installed as console scripts
|
|
9
|
+
# but are useful for someone reading the source.
|
|
10
|
+
include test_audio_simple.py
|
|
11
|
+
include test_tx.py
|
|
12
|
+
include monitor_tx.py
|
|
13
|
+
include js8_tx_test.py
|
|
14
|
+
|
|
15
|
+
# Things that should never end up in a release sdist or wheel
|
|
16
|
+
prune old
|
|
17
|
+
prune archive
|
|
18
|
+
prune __pycache__
|
|
19
|
+
prune build
|
|
20
|
+
prune dist
|
|
21
|
+
exclude windows.md
|
|
22
|
+
exclude osx.md
|
|
23
|
+
global-exclude *.py[cod]
|
|
24
|
+
global-exclude *.swp
|
|
25
|
+
global-exclude .DS_Store
|