Skip to content

time_scale, FM-real collision noise, squelch-open delay, opt-in scheduling priority + samoyed echo/flush pin - #17

Merged
M0LTE merged 10 commits into
mainfrom
feat/timescale-realism-priority
Jun 12, 2026
Merged

time_scale, FM-real collision noise, squelch-open delay, opt-in scheduling priority + samoyed echo/flush pin#17
M0LTE merged 10 commits into
mainfrom
feat/timescale-realism-priority

Conversation

@M0LTE

@M0LTE M0LTE commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Four simulator improvements plus a samoyed pin bump, all driven by packet.net's Packet.LinkBench rung-2 campaign (sustained AX.25 bulk-transfer testing over this sim). Each lands as its own commit with tests; make test (vet + full suite) green throughout, race-detector pass clean.

time_scale (config + -time-scale flag, default 1.0) — run the whole simulation N× faster than wall clock. The sim's only real-time dependencies are the rxFeeder pacing ticker, the txWatchdog tick/silence-window, and the composite-recorder ticker (everything else is sample-count-driven), so scaling those three runs the full audio pipeline — modem included — proportionally faster. A 10-minute sustained-transfer experiment becomes ~1 minute at ×10. Documented caveat (README + startup warning): the TNC children's own wall-clock behaviours (CSMA persist/slottime) don't scale, and hosts must scale their protocol timers to match — an accelerated-testing mode, not a calibrated CSMA sim. Test proves block sequences are byte-identical across scales.

collision_mode: noise implemented (was an accepted-but-stub mode) — a collision inside the capture margin now renders gaussian garble at an amplitude tracking the strongest signal, instead of pure digital silence. A real FM discriminator outputs loud heterodyne garble when comparable carriers beat — silence is unrealistically clean (perfectly detectable, can never false-sync a modem or hold DCD open). silence stays the default; capture/single-source behaviour byte-identical.

Per-link squelch_open_ms (default 0 = today) — mutes the first N ms of each transmission as heard by the receiving port, modelling RX squelch/carrier-detect opening — the thing KISS TXDELAY exists for, which the sim previously made invisible (instant audio delivery makes a too-short TXDELAY look fine when it wouldn't be on air). Silence is substituted rather than blocks dropped, so the mixer still sees the carrier for capture/collision decisions. Transmission boundary reuses the txWatchdog's exact idle→active rule so the two definitions can't drift.

-rt-priority (opt-in) — renices the router and its TNC children to −10 so the 10 ms pacing ticker doesn't glitch under shared host load (these tests run on a box that also builds/tests the host stack). EPERM logs a one-liner pointing at cap_add: [SYS_NICE] and continues. Deliberately niceness, not SCHED_FIFO (won't starve the host). README compose snippet included.

samoyed pin → packet-net/samoyed#1 — the previous build echoed the ACKMODE TX-complete acknowledgement at audio render time (~25× faster than real time; hosts measured 7 ms echoes for 350 ms-minimum frames), and kept transmitting frames a disconnected KISS client had queued (successive test sessions heard each other's leftovers). Both fixed upstream in the fork; this picks up the merge.

🤖 Generated with Claude Code

M0LTE and others added 9 commits June 12, 2026 01:29
…ll clock

The only real-time pacing in the router is a handful of wall-clock tickers
(rxFeeder block ticker, composite recorder ticker, txWatchdog tick + silence
window); everything else is sample-count-driven. Dividing those intervals by a
new top-level time_scale knob (config, or -time-scale on sim-router which
overrides it) runs the whole simulation N x faster — a 60 s protocol exchange
completes in 60/N s. The txSilenceWindow must scale too: silence detection has
to track the (faster) audio rate or tx_end events fire mid-transmission.

Documented caveat (README + config comment + startup warning): the TNC
children's own wall-clock behaviours (CSMA persist/slottime, internal
timeouts) do NOT scale, so time_scale > 1 is an accelerated-testing mode, not
a calibrated CSMA simulation, and KISS hosts must scale their own T1/T2 to
match.

Motivated by packet.net's Packet.LinkBench rung-2 campaign (long soak
exchanges dominated by wall-clock waiting).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ead of digital silence

A collision inside the capture margin used to produce pure digital silence —
unrealistically clean. A real FM discriminator outputs loud garble (the
heterodyne beat between two comparable carriers plus wideband noise) at an
amplitude comparable to the signals themselves, and that garble is exactly
what hammers a receiving modem's DCD and false-sync paths during real
collisions. The already-config-accepted "noise" mode now renders collisions
as gaussian noise whose RMS matches the strongest signal's post-loss level
(hot collision = loud garble, weak distant one = quiet garble), routed through
the existing AddNoise machinery so RNG locking and clamping stay in one place.

"silence" remains the default for backwards compatibility; "sum" stays a
stub. From packet.net's Packet.LinkBench rung-2 campaign (collision-recovery
results were implausibly good against the silence model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pening delay

Real FM receivers mute audio until squelch opens after a carrier appears (tens
of ms, plus discriminator settle) — the on-air reason KISS TXDELAY exists.
net-sim used to deliver a transmission's first samples instantly, making tiny
TXDELAY values look fine when they wouldn't be on air.

New optional per-link squelch_open_ms (0..500, default 0 = previous behaviour,
strictly validated): the linkQueue — the per-directed-link seam squelch
belongs to — delivers the first squelch_open_ms worth of blocks of each
transmission as silence instead of audio. Silence, not nothing: the carrier
stays visible to the mixer for capture/collision decisions; only the
receiver's audio is muted while the squelch opens.

Transmission boundary: push tags a block start-of-transmission after a
silenceWindow-sized idle gap — the txWatchdog's exact idle→active rule
(time-scaled txSilenceWindow), so inter-frame pauses inside one keyup don't
re-mute and the two boundary definitions can't drift apart. The tag rides on
the block, so the pop-side mute is counted in blocks (sim-time audio),
independent of drain timing and of time_scale.

From packet.net's Packet.LinkBench rung-2 campaign (TXDELAY sweep bottomed out
unrealistically at ~0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ren for jitter-free pacing

The 10 ms pacing tickers and the TNC children's software demodulators glitch
under shared host load: a lost tick gaps a receiver's audio and produces
decode failures that look like RF problems. New opt-in -rt-priority flag on
sim-router renices the router's own process and (via the new tnc.Child.Pid())
every spawned TNC child to -10. Plain niceness only, deliberately not
SCHED_FIFO — a real-time policy risks starving the host, and niceness is
enough for ticker jitter.

Best-effort by design: without CAP_SYS_NICE the kernel returns EPERM, which
earns one clear warning (pointing at --cap-add SYS_NICE / cap_add:
[SYS_NICE], snippet added to the README docker section) and the simulation
continues at normal priority. Default behaviour is untouched. Uses syscall
directly (golang.org/x/sys is not a dependency of this module).

From packet.net's Packet.LinkBench rung-2 campaign (bench results were noisy
whenever CI neighbours were busy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ket-net/samoyed#1)

The previous pin echoed the ACKMODE 0x0C acknowledgement when the frame's audio was rendered (which runs much faster than real time), not when it had been played out — hosts saw TX-complete echoes milliseconds after queueing on an idle channel. It also kept transmitting frames a disconnected KISS TCP client had queued, so successive test sessions against the same port heard each other's leftovers. Both fixed in packet-net/samoyed#1; this pin picks up that merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…im-router)

The renice option landed on cmd/sim-router only, but the container entrypoint runs sim-web, which embeds the router as a library — so the flag was unreachable in the shipped image. Thread it through sim-web's flag set and app state into router.Options.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sim user

docker --cap-add SYS_NICE only populates the container's bounding set; the non-root `sim` user's processes don't inherit it, so the renice EPERM'd even with the cap granted. setcap cap_sys_nice+ep on sim-web/sim-router gives the processes the capability directly — still gated on the container being granted the cap AND -rt-priority being passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…C denies negative nice)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed (wall-clock TX pacing) — measured, tracked upstream

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…everted

The disconnect flush (packet-net/samoyed#1, second commit) wedged the channel under repeated mid-transfer KISS disconnects — bisected with packet.net's LinkBench stress sequence; full evidence in #19. Reverted upstream (packet-net/samoyed#3); this pin carries the validated set: ACKMODE echo at PTT release (the 7 ms-echo fix) plus the tq lost-wakeup fix (packet-net/samoyed#2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@M0LTE
M0LTE merged commit 20ef85f into main Jun 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant