Chamber test tools: chamber_tmux.sh, chamber_log.py, mark.sh¶
Lives on: the follower Pi 5, in ~/capstone/rfdf/. Source in this repo under
agentcore/rfdf/.
What they do¶
Built for the anechoic chamber test (see the test plan post): the array sits on a turntable, the HackRF transmits from a fixed spot, and we need every Kraken bearing tagged with the turntable angle and transmit power at that moment.
| Script | Job |
|---|---|
chamber_tmux.sh |
Opens one screen with everything needed for the test |
chamber_log.py |
Logs every Kraken sample, tagged with the current test point |
mark.sh |
Tells the logger "the table is now at this angle, at this gain" |
Walkthrough: one screen for the whole test¶
~/capstone/rfdf/chamber_tmux.sh
opens a tmux session called chamber with five labeled panes:
+---------------------------------------------+
| LOG chamber_log.py (press Enter) |
+----------------------+----------------------+
| MARK ./mark.sh | HACKRF transmitter |
+----------------------+----------------------+
| STATUS rfdf.sh up | DOA app log |
+----------------------+----------------------+
STATUS runs rfdf.sh up automatically. LOG and MARK have their commands
typed in but not started, so nothing logs until the stack is confirmed OK.
Because it's tmux, everything keeps running on the Pi 5 if the laptop's SSH drops. Running the same command again reattaches to the same screen.
Walkthrough: marking a test point¶
./mark.sh 90 30 # table at 90°, HackRF gain 30
./mark.sh 90 30 redo # optional note
mark.sh just writes the angle, gain and note to a small state file, and appends them with
a timestamp to chamber_marks.txt:
echo "$*" > "$LOGS/chamber_state"
echo "$(date +%T) $*" >> "$LOGS/chamber_marks.txt"
Walkthrough: the logger¶
chamber_log.py polls the Kraken five times a second (the same way as the
DOA app's poller), and for every new sample writes one CSV row tagged with
whatever was last marked:
if t_ms is not None and t_ms != last_t:
last_t = t_ms
angle, gain, note = read_state()
w.writerow([t_ms, int(time.time() * 1000), brg, conf, rssi, angle, gain, note])
Once a second it prints a summary for the current point: sample count, average bearing ± spread, confidence and RSSI. That's how the operator knows when a point is done (the bearing has settled and there are ~100 samples, about 45 s).
Averaging bearings needs care for the same reason as heading: the average of 359° and 1° is 0°, not 180°. So each bearing is treated as a point on a circle and the average is taken there:
z = sum(cmath.exp(1j * math.radians(d)) for d in degs) / len(degs)
mean = math.degrees(cmath.phase(z)) % 360
r = min(abs(z), 1.0)
std = math.degrees(math.sqrt(max(0.0, -2 * math.log(r)))) if r > 1e-9 else 180.0
r is how tightly the bearings cluster (1 = all identical, 0 = scattered all round); the
spread is the circular standard deviation computed from it.
The logger only reads from the Kraken, so it doesn't take samples away from the DOA app; both can run at once.