Skip to content

Commit cfaa935

Browse files
rafael2kclaude
andcommitted
arq: drop an unreachable pattern correlation; refuse to truncate callsigns
Three connect-path cleanups, no wire change. 1. expect_pattern_ack no longer includes ACCEPTING. No pattern can arrive in that state: the caller answers an ACCEPT with either a coded DATAC16 confirm or a first MFSK DATA burst, never a pattern — send_ack() emits one only once the session is CONNECTED. So the correlator ran for the whole ~18 s ACCEPT window, and per the measurement already recorded at that site it costs about half the RX sample budget (3.5k samp/s consumed against 8k arriving, capture ring growing to ~400 kB). It was burning that precisely while the answerer had to decode the frames that complete the connect. Note this is NOT visible in connect_bench: the sim has no DSP, so it cannot show RX budget. The justification is that the branch is unreachable plus the existing DIAG measurement of what the correlator costs; confirming the gain needs the live path (make DEBUG_IO=1). 2. Callsign encoders refuse over-long input instead of truncating. Truncating an arithmetic code does not shorten the string — it decodes to a DIFFERENT one, so the peer answers a call from a station that does not exist, or drops one that does. Both encode_callsign_payload (CALL/ACCEPT) and encode_callsign_only_payload (CQ) had this. They now return -1, which build_call_accept already propagates, and send_call_accept says why rather than leaving a session retrying against silence. Tests: a round-trip over realistic callsigns including SSIDs (PU2UIT-2, DL9ABC-15) and a refusal test verified to fail against the old truncating code. 3. Dead code: startup_deadline_ms was written at three sites and read nowhere; ARQ_CONNECT_GRACE_SLOTS and ARQ_CONNECT_BUSY_EXT_S were never referenced. Left alone deliberately, because it is user-facing and the call is not mine: `startup_max_s` is a documented mercury.ini knob, clamped 2..60 and written back by cfg_write, whose ONLY effect was to set the unread field above. It is now inert — an operator can tune it and nothing happens. It should either be implemented or removed. Full C suite green, go test -count=1 green (203 s). The arq.c:754 format-truncation warning is pre-existing (present on the unmodified tree). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f22dee4 commit cfaa935

6 files changed

Lines changed: 78 additions & 16 deletions

File tree

datalink_arq/arq.c

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,13 +1107,20 @@ bool arq_get_runtime_snapshot(arq_runtime_snapshot_t *snapshot)
11071107
* guard then flushing bursts that were still arriving. Trunk, which has no
11081108
* pattern ACK, holds 7.99k samp/s on the same bench.
11091109
*
1110-
* WAIT_ACK is by definition the only in-session state where the peer's
1111-
* pattern ACK is due (see ARQ_DFLOW_WAIT_ACK), and ACCEPTING is the
1112-
* handshake equivalent. Everywhere else -- above all IDLE_IRS, where the
1113-
* receiver is busy demodulating a 13.5 s burst -- there is nothing to
1114-
* detect and the CPU is needed elsewhere. */
1110+
* WAIT_ACK is by definition the only state where a pattern ACK is due
1111+
* (see ARQ_DFLOW_WAIT_ACK). Everywhere else -- above all IDLE_IRS, where
1112+
* the receiver is busy demodulating a 13.5 s burst -- there is nothing to
1113+
* detect and the CPU is needed elsewhere.
1114+
*
1115+
* ACCEPTING used to be included as "the handshake equivalent". It is not:
1116+
* no pattern can arrive there. The caller answers an ACCEPT with either a
1117+
* coded DATAC16 confirm (send_call_accept -> ARQ_DFLOW_ACK_TX with
1118+
* pending_connect_confirm, arq_fsm.c) or a first MFSK DATA burst -- never a
1119+
* pattern, which send_ack() emits only once the session is CONNECTED. So
1120+
* the correlator ran for the whole ~18 s ACCEPT window and spent roughly
1121+
* half the RX budget precisely while the answerer had to decode the frames
1122+
* that complete the connect. */
11151123
snapshot->expect_pattern_ack =
1116-
(g_sess.conn_state == ARQ_CONN_ACCEPTING) ||
11171124
(g_sess.conn_state == ARQ_CONN_CONNECTED &&
11181125
g_sess.dflow_state == ARQ_DFLOW_WAIT_ACK);
11191126
snapshot->trx = trx;

datalink_arq/arq_fsm.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,14 @@ static void send_call_accept(arq_session_t *sess, bool is_accept)
461461
my_call, sess->remote_call, bw_hz);
462462
if (n > 0)
463463
send_frame(PACKET_TYPE_ARQ_CALL, sess->control_mode, (size_t)n, frame, 0);
464+
else
465+
/* Almost always an over-long callsign: the 10-byte SRC slot holds ~14
466+
* characters at ~5.25 bits each. The encoder refuses rather than
467+
* truncating (a truncated arithmetic code decodes to a different
468+
* string), so say why — otherwise this is a CALL that never goes out
469+
* and a session that retries against silence. */
470+
HLOGW(LOG_COMP, "%s not sent: cannot encode callsign '%s' (too long?)",
471+
is_accept ? "ACCEPT" : "CALL", my_call);
464472
}
465473

466474
static void send_ctrl_frame(arq_session_t *sess, arq_subtype_t subtype)
@@ -788,7 +796,6 @@ static void fsm_listening(arq_session_t *sess, const arq_event_t *ev)
788796
{
789797
sess->role = ARQ_ROLE_CALLEE;
790798
reset_session_data_state(sess);
791-
sess->startup_deadline_ms = time_now_ms() + (ARQ_STARTUP_MAX_S * 1000ULL);
792799
if (g_cbs.notify_connected)
793800
g_cbs.notify_connected(sess->remote_call, sess->local_call);
794801
if (g_timing)
@@ -815,8 +822,6 @@ static void fsm_calling(arq_session_t *sess, const arq_event_t *ev)
815822
bool has_tx_backlog = session_tx_backlog(sess) > 0;
816823
sess->role = ARQ_ROLE_CALLER;
817824
reset_session_data_state(sess); /* discard stale retransmit buf; MFSK-start */
818-
sess->startup_deadline_ms =
819-
time_now_ms() + (ARQ_STARTUP_MAX_S * 1000ULL);
820825
if (g_cbs.notify_connected)
821826
g_cbs.notify_connected(sess->remote_call, sess->local_call);
822827
if (g_timing)
@@ -889,8 +894,6 @@ static void fsm_accepting(arq_session_t *sess, const arq_event_t *ev)
889894
case ARQ_EV_RX_ACK:
890895
sess->role = ARQ_ROLE_CALLEE;
891896
reset_session_data_state(sess); /* discard stale retransmit buf; MFSK-start */
892-
sess->startup_deadline_ms =
893-
time_now_ms() + (ARQ_STARTUP_MAX_S * 1000ULL);
894897
if (g_cbs.notify_connected)
895898
g_cbs.notify_connected(sess->remote_call, sess->local_call);
896899
if (g_timing)

datalink_arq/arq_fsm.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ typedef struct
195195
/* --- Retry/timeout bookkeeping --- */
196196
int tx_retries_left; /* retries remaining for current frame */
197197
uint64_t state_enter_ms; /* when current conn_state was entered */
198-
uint64_t startup_deadline_ms; /* end of control-mode-only startup */
199198

200199
/* --- Peer state observed from frames --- */
201200
bool peer_has_data; /* peer's HAS_DATA flag in last frame */

datalink_arq/arq_protocol.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,14 @@ static int encode_callsign_payload(const char *src, const char *dst,
440440
if (enc_len <= 0)
441441
return -1;
442442

443+
/* Refuse rather than truncate. A truncated arithmetic code is not a
444+
* shortened callsign — it decodes to a DIFFERENT string at the far end,
445+
* so the peer answers a call from a station that does not exist, or
446+
* silently drops one that does. Failing here makes the operator's
447+
* over-long callsign visible instead of turning it into a wrong one. */
443448
size_t src_cap = out_cap - ARQ_CONNECT_DST_CRC_SIZE;
444449
if ((size_t)enc_len > src_cap)
445-
enc_len = (int)src_cap;
450+
return -1;
446451
memcpy(out + ARQ_CONNECT_DST_CRC_SIZE, tmp, (size_t)enc_len);
447452
return (int)(ARQ_CONNECT_DST_CRC_SIZE + (size_t)enc_len);
448453
}
@@ -489,8 +494,10 @@ static int encode_callsign_only_payload(const char *src, uint8_t *out, size_t ou
489494
if (enc_len <= 0)
490495
return -1;
491496

497+
/* Same reasoning as encode_callsign_payload: truncation corrupts, it does
498+
* not shorten. */
492499
if ((size_t)enc_len > out_cap)
493-
enc_len = (int)out_cap;
500+
return -1;
494501
memcpy(out, tmp, (size_t)enc_len);
495502
return enc_len;
496503
}

datalink_arq/arq_protocol.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,6 @@ extern _Atomic float arq_callint_override_s;
245245
#define ARQ_ACCEPT_RETRY_SLOTS atomic_load(&arq_accept_retry_slots)
246246
#define ARQ_DATA_RETRY_SLOTS atomic_load(&arq_data_retry_slots)
247247
#define ARQ_DISCONNECT_RETRY_SLOTS atomic_load(&arq_disconnect_retry_slots)
248-
#define ARQ_CONNECT_GRACE_SLOTS 2 /* extra wait slots for ACCEPT */
249-
#define ARQ_CONNECT_BUSY_EXT_S 2 /* busy-extension guard after CALL */
250248
#define ARQ_STARTUP_MAX_S_DEFAULT 10 /* control-mode-only startup window */
251249
extern _Atomic int arq_startup_max_s;
252250
#define ARQ_STARTUP_MAX_S atomic_load(&arq_startup_max_s)

tests/datalink_arq/test_arq_protocol.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,52 @@ void test_mode_timing_datac16_unaffected_by_override(void)
315315
TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.0f, t->retry_interval_s);
316316
}
317317

318+
/* ---- callsign packing: round-trip, and refusal instead of corruption ---- */
319+
320+
/* The SRC callsign is arithmetic-coded over a 38-symbol alphabet (A-Z, 0-9,
321+
* '-', EOF) with a uniform model, i.e. ~5.25 bits/char, into a 10-byte slot.
322+
* Realistic callsigns must survive a round trip unchanged. */
323+
void test_callsign_roundtrip_realistic(void)
324+
{
325+
static const char *calls[] = {
326+
"PU2UIT", "PU2UIT-2", "A0AAA", "ZL1ANY", "W1AW", "DL9ABC-15", "PY2-XYZ"
327+
};
328+
for (unsigned i = 0; i < sizeof calls / sizeof calls[0]; i++)
329+
{
330+
uint8_t frame[ARQ_CONTROL_FRAME_SIZE];
331+
int n = arq_protocol_build_call(frame, sizeof frame, 0x2A,
332+
calls[i], "DSTCALL", 2300);
333+
char msg[96];
334+
snprintf(msg, sizeof msg, "build_call failed for '%s'", calls[i]);
335+
TEST_ASSERT_GREATER_THAN_MESSAGE(0, n, msg);
336+
337+
uint8_t sid = 0; int bw = 0;
338+
char src[CALLSIGN_MAX_SIZE] = {0}, dst[CALLSIGN_MAX_SIZE] = {0};
339+
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
340+
arq_protocol_parse_call(frame, (size_t)n, &sid, src, dst, &bw), msg);
341+
TEST_ASSERT_EQUAL_STRING_MESSAGE(calls[i], src, msg);
342+
TEST_ASSERT_EQUAL_UINT8_MESSAGE(0x2A, sid, msg);
343+
}
344+
}
345+
346+
/* A callsign too long for the slot must be REFUSED, not truncated.
347+
*
348+
* Truncating an arithmetic code does not shorten the string — it decodes to a
349+
* DIFFERENT one, so the peer either answers a call from a station that does
350+
* not exist or drops one that does. The old code truncated and reported
351+
* success. */
352+
void test_callsign_too_long_is_refused_not_truncated(void)
353+
{
354+
uint8_t frame[ARQ_CONTROL_FRAME_SIZE];
355+
const char *huge = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
356+
357+
int n = arq_protocol_build_call(frame, sizeof frame, 0x2A,
358+
huge, "DSTCALL", 2300);
359+
TEST_ASSERT_LESS_OR_EQUAL_INT_MESSAGE(0, n,
360+
"over-long callsign was accepted — it will decode to a different "
361+
"station at the far end");
362+
}
363+
318364
int main(void)
319365
{
320366
UNITY_BEGIN();
@@ -348,5 +394,7 @@ int main(void)
348394
RUN_TEST(test_call_interval_override);
349395
RUN_TEST(test_call_interval_reset);
350396
RUN_TEST(test_mode_timing_datac16_unaffected_by_override);
397+
RUN_TEST(test_callsign_roundtrip_realistic);
398+
RUN_TEST(test_callsign_too_long_is_refused_not_truncated);
351399
return UNITY_END();
352400
}

0 commit comments

Comments
 (0)