Skip to content

Commit 8391879

Browse files
rafael2kclaude
andcommitted
arq/fsm: IRS mirrors the ISS delivery-driven ladder
The IRS payload decoder tracked peer_tx_mode = the last-DECODED mode (arq_modem_preferred_rx_mode() is hardcoded to DATAC16, so select_payload_rx_mode's first branch is dead). When the ISS climbed the ladder (e.g. MFSK->DATAC15) the IRS's dual decoder had no slot for the new mode, missed the first burst of every climb, never ACKed it, and the ISS retried and fell back -- the transfer oscillated at the MFSK floor and crawled. The IRS observes the same per-frame outcomes the sender climbs on, so it now mirrors the same delivery-driven ladder: a clean new frame climbs, a duplicate (sender retried and stepped down) steps down, and a full IDLE_IRS idle-hold with no RX steps down toward the floor (reset-on-miss, so a lost ACK that left us climbed above the sender re-rendezvous at the floor). Keeps the payload decoder on the mode the peer's NEXT burst will use, with no on-wire mode negotiation. ladder_step() is factored out of record_tx_outcome so both ends apply identical rules. Tests: test_irs_mirror_climbs_with_peer / _steps_down_on_duplicate / _resets_toward_floor_on_silence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9de8e52 commit 8391879

3 files changed

Lines changed: 207 additions & 35 deletions

File tree

datalink_arq/arq_fsm.c

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ void arq_fsm_init(arq_session_t *sess)
115115
sess->speed_level = 0;
116116
sess->tx_success_count = 0;
117117
sess->fast_ramp = true;
118+
sess->rx_speed_level = 0;
119+
sess->rx_success_count = 0;
120+
sess->rx_fast_ramp = true;
118121
}
119122

120123
int arq_fsm_timeout_ms(const arq_session_t *sess, uint64_t now)
@@ -151,6 +154,9 @@ static void reset_session_data_state(arq_session_t *sess)
151154
sess->speed_level = 0;
152155
sess->tx_success_count = 0;
153156
sess->fast_ramp = true;
157+
sess->rx_speed_level = 0;
158+
sess->rx_success_count = 0;
159+
sess->rx_fast_ramp = true;
154160
sess->payload_mode = arq_mode_ladder[0]; /* MFSK floor */
155161
sess->peer_tx_mode = arq_mode_ladder[0];
156162
sess->pending_disconnect = false;
@@ -196,6 +202,9 @@ static void sess_enter(arq_session_t *sess, arq_conn_state_t new_state,
196202
{
197203
sess->dflow_state = ARQ_DFLOW_IDLE_ISS;
198204
sess->peer_tx_mode = sess->initial_payload_mode;
205+
sess->rx_speed_level = 0; /* mirror back to the floor for a fresh session */
206+
sess->rx_success_count = 0;
207+
sess->rx_fast_ramp = true;
199208
sess->tx_frame_present = false;
200209
sess->tx_frame_len = 0;
201210
sess->tx_frame_retx = false;
@@ -285,46 +294,72 @@ static void apply_speed_level(arq_session_t *sess)
285294
clamp_payload_mode_to_bandwidth(arq_mode_ladder[sess->speed_level]);
286295
}
287296

288-
/** Record the outcome of the retained TX frame once its fate is known.
289-
* Delivery-driven, no SNR/OLLA/reverse-hold: clean == the frame was delivered
290-
* with no retransmission; else it needed at least one retry. Any retry steps
291-
* the ladder DOWN one rung immediately (toward MFSK) for reliability. A run
292-
* of clean deliveries steps it UP: the initial fast ramp climbs one rung per
293-
* clean delivery until the first retry, after which it settles to
294-
* ARQ_LADDER_UP_SUCCESSES clean deliveries per step. */
295-
static void record_tx_outcome(arq_session_t *sess, bool clean)
297+
/** Apply one delivery-driven ladder step to a (level, success_count, fast_ramp)
298+
* triple. Shared by the ISS (TX outcomes) and the IRS mode mirror (RX
299+
* outcomes) so both ends climb/drop by exactly the same rule and stay locked
300+
* in step without any on-wire mode negotiation.
301+
*
302+
* clean == the frame was delivered/received first try: a run of clean outcomes
303+
* climbs — the fast initial ramp climbs one rung per clean outcome until the
304+
* first miss, after which it settles to ARQ_LADDER_UP_SUCCESSES clean outcomes
305+
* per step. A miss/retry steps DOWN one rung immediately (toward the MFSK
306+
* floor) and ends the fast ramp, so a deep fade parks at the floor with no
307+
* over-climb oscillation. Returns the signed level change (for logging). */
308+
static int ladder_step(int *level, int *success_count, bool *fast_ramp, bool clean)
296309
{
310+
int before = *level;
297311
if (!clean)
298312
{
299-
sess->fast_ramp = false; /* first retry ends the fast initial ramp */
300-
if (sess->speed_level > 0)
313+
*fast_ramp = false;
314+
if (*level > 0)
315+
(*level)--;
316+
*success_count = 0;
317+
}
318+
else
319+
{
320+
(*success_count)++;
321+
int need = *fast_ramp ? 1 : ARQ_LADDER_UP_SUCCESSES;
322+
if (*success_count >= need && *level < ARQ_LADDER_LEVELS - 1)
301323
{
302-
sess->speed_level--;
303-
HLOGD(LOG_COMP, "Ladder step-down to %d (retry)", sess->speed_level);
324+
(*level)++;
325+
*success_count = 0;
304326
}
305-
sess->tx_success_count = 0;
306-
apply_speed_level(sess);
307-
return;
308327
}
328+
return *level - before;
329+
}
309330

310-
sess->tx_success_count++;
311-
/* Step-up cadence (the single tuning knob, confirmed at OTA): the fast
312-
* initial ramp climbs one rung per clean delivery until the first retry,
313-
* after which it settles to ARQ_LADDER_UP_SUCCESSES clean deliveries per
314-
* step. A retry always steps down one rung, so a deep fade parks at the
315-
* MFSK floor (no over-climb oscillation) while a clean link ramps quickly. */
316-
int need = sess->fast_ramp ? 1 : ARQ_LADDER_UP_SUCCESSES;
317-
if (sess->tx_success_count >= need &&
318-
sess->speed_level < ARQ_LADDER_LEVELS - 1)
319-
{
320-
sess->speed_level++;
321-
sess->tx_success_count = 0;
322-
HLOGD(LOG_COMP, "Ladder step-up to %d (%d clean%s)",
323-
sess->speed_level, need, sess->fast_ramp ? ", fast ramp" : "");
324-
}
331+
/** Record the outcome of the retained TX frame once its fate is known.
332+
* Delivery-driven, no SNR/OLLA/reverse-hold: clean == the frame was delivered
333+
* with no retransmission; else it needed at least one retry. */
334+
static void record_tx_outcome(arq_session_t *sess, bool clean)
335+
{
336+
int delta = ladder_step(&sess->speed_level, &sess->tx_success_count,
337+
&sess->fast_ramp, clean);
338+
if (delta < 0)
339+
HLOGD(LOG_COMP, "Ladder step-down to %d (retry)", sess->speed_level);
340+
else if (delta > 0)
341+
HLOGD(LOG_COMP, "Ladder step-up to %d", sess->speed_level);
325342
apply_speed_level(sess);
326343
}
327344

345+
/** IRS: mirror the peer's (ISS) ladder from the outcome of a received DATA
346+
* frame so our payload decoder is already on the mode the peer's NEXT burst
347+
* will use. clean_new == a new in-order frame decoded first try (mirrors the
348+
* sender's clean delivery); a duplicate (our ACK was lost, the sender retried
349+
* and stepped down) mirrors the sender's step-down. Keeps peer_tx_mode ==
350+
* arq_mode_ladder[rx_speed_level]. */
351+
static void irs_mirror_peer_ladder(arq_session_t *sess, bool clean_new)
352+
{
353+
int delta = ladder_step(&sess->rx_speed_level, &sess->rx_success_count,
354+
&sess->rx_fast_ramp, clean_new);
355+
sess->peer_tx_mode =
356+
clamp_payload_mode_to_bandwidth(arq_mode_ladder[sess->rx_speed_level]);
357+
if (delta != 0)
358+
HLOGD(LOG_COMP, "IRS RX-mode mirror %s to level %d (mode=%d)",
359+
delta > 0 ? "climb" : "step-down",
360+
sess->rx_speed_level, sess->peer_tx_mode);
361+
}
362+
328363
/* IRS: arm the ACK deadline after a received DATA frame. Stop-and-wait: one
329364
* frame per burst, so we always wait the channel guard before emitting the
330365
* pattern ACK (lets the ISS relay switch TX->RX before our tones arrive). */
@@ -1000,8 +1035,12 @@ static void fsm_connected(arq_session_t *sess, const arq_event_t *ev)
10001035
static void irs_receive_data(arq_session_t *sess, const arq_event_t *ev)
10011036
{
10021037
update_local_snr(sess, ev);
1003-
sess->peer_tx_mode = ev->mode;
10041038
bool new_frame = deliver_rx_checked(sess, ev);
1039+
/* Follow the sender's delivery-driven ladder: a new in-order frame is a
1040+
* clean delivery on its side (climb); a duplicate means it retried and
1041+
* stepped down. This sets peer_tx_mode to the mode of the peer's NEXT
1042+
* burst so the payload decoder is already there when it arrives. */
1043+
irs_mirror_peer_ladder(sess, new_frame);
10051044
if (new_frame && g_timing)
10061045
arq_timing_record_data_rx(g_timing, (int)ev->seq,
10071046
(int)ev->data_bytes, sess->local_snr_x10);
@@ -1209,7 +1248,6 @@ static void fsm_dflow(arq_session_t *sess, const arq_event_t *ev)
12091248
else if (ev->id == ARQ_EV_RX_DATA)
12101249
{
12111250
update_local_snr(sess, ev);
1212-
sess->peer_tx_mode = ev->mode;
12131251
sess->last_rx_ms = time_now_ms();
12141252

12151253
if (ev->seq == sess->rx_expected)
@@ -1239,6 +1277,7 @@ static void fsm_dflow(arq_session_t *sess, const arq_event_t *ev)
12391277
"RX_DATA in WAIT_ACK (dup seq=%d expected=%d) — re-TX our seq=%d",
12401278
(int)ev->seq, (int)sess->rx_expected, (int)sess->tx_frame_seq);
12411279
deliver_rx_checked(sess, ev); /* logs dup; no delivery */
1280+
irs_mirror_peer_ladder(sess, false); /* peer retried → mirror step-down */
12421281
sess->tx_frame_retx = true;
12431282
dflow_enter(sess, ARQ_DFLOW_DATA_TX, UINT64_MAX, ARQ_EV_TIMER_RETRY);
12441283
send_data_burst(sess);
@@ -1323,6 +1362,16 @@ static void fsm_dflow(arq_session_t *sess, const arq_event_t *ev)
13231362
}
13241363
else
13251364
{
1365+
/* Reset-on-miss: a full idle hold passed with no DATA. If a
1366+
* lost ACK left our RX-mode mirror climbed ABOVE the sender
1367+
* (which stepped down on its retry), we can no longer decode
1368+
* its bursts — silently stalling. Step the mirror down one
1369+
* rung toward the floor so the two ends re-rendezvous; the
1370+
* MFSK floor is the guaranteed common ground. At faster modes
1371+
* frames arrive well within the hold, so this only fires on a
1372+
* genuine stall (at the floor a step-down is a harmless no-op). */
1373+
if (sess->rx_speed_level > 0)
1374+
irs_mirror_peer_ladder(sess, false);
13261375
enter_idle_irs(sess); /* re-arm the idle hold */
13271376
}
13281377
}

datalink_arq/arq_fsm.h

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,22 @@ typedef struct
175175
bool fast_ramp; /* faster initial climb: 1 rung per clean
176176
* delivery until the first retry, then
177177
* ARQ_LADDER_UP_SUCCESSES-per-step */
178-
int peer_tx_mode; /* mode peer last TX'd in = my payload
179-
* RX decoder mode when IRS; updated
180-
* from ev->mode on every DATA frame */
178+
int peer_tx_mode; /* my payload RX decoder mode when IRS =
179+
* arq_mode_ladder[rx_speed_level]; the
180+
* mode the peer's NEXT DATA burst uses */
181+
182+
/* --- IRS-side mirror of the peer's (ISS) delivery-driven ladder --------
183+
* The IRS observes the same per-frame outcomes the sender climbs on (a
184+
* clean new frame == a clean delivery; a duplicate == a sender retry), so
185+
* applying the identical ladder rule keeps rx_speed_level == the sender's
186+
* speed_level with no on-wire mode negotiation. Without it the decoder can
187+
* only ever track a mode it has ALREADY decoded, so it misses the first
188+
* burst of every mode the sender climbs to (stalling the transfer at the
189+
* MFSK floor). A prolonged RX gap (a lost ACK left us above the sender)
190+
* steps this back down toward the floor so the two ends re-rendezvous. */
191+
int rx_speed_level; /* mirror of the peer's ladder index */
192+
int rx_success_count; /* clean receives toward a mirror step-up*/
193+
bool rx_fast_ramp; /* mirror of the peer's fast initial ramp*/
181194

182195
/* --- Retry/timeout bookkeeping --- */
183196
int tx_retries_left; /* retries remaining for current frame */

tests/datalink_arq/test_arq_fsm.c

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,113 @@ void test_timeout_ms_idle(void)
664664
TEST_ASSERT_GREATER_THAN(60000, ms);
665665
}
666666

667+
/* ---- IRS payload-mode mirror: follow the peer's delivery-driven ladder ----
668+
*
669+
* The IRS's payload decoder must be on the mode the peer's NEXT burst will use,
670+
* NOT the last one it decoded — otherwise it misses the first burst of every
671+
* mode the sender climbs to and the transfer stalls at the MFSK floor (the
672+
* -x sock regression). Since the IRS observes the same per-frame outcomes the
673+
* sender climbs on, it mirrors the same ladder. */
674+
675+
/* LISTENING -> ACCEPTING -> CONNECTED as the answerer: IRS role, IDLE_IRS. */
676+
static void goto_connected_irs(void)
677+
{
678+
enter_accepting();
679+
arq_event_t ev = make_event(ARQ_EV_RX_ACK);
680+
ev.session_id = 0x42;
681+
arq_fsm_dispatch(&sess, &ev);
682+
TEST_ASSERT_EQUAL_INT(ARQ_CONN_CONNECTED, sess.conn_state);
683+
TEST_ASSERT_EQUAL_INT(ARQ_DFLOW_IDLE_IRS, sess.dflow_state);
684+
}
685+
686+
/* In-order DATA frame carrying `n` payload bytes. HAS_DATA keeps us the IRS
687+
* across frames (the sender has more to send). mode is set to what the peer
688+
* actually sent, but the mirror deliberately ignores it (anticipation, not
689+
* follow-the-decoded-mode). */
690+
static arq_event_t make_data_event(uint8_t seq, int mode, size_t n)
691+
{
692+
arq_event_t ev = make_event(ARQ_EV_RX_DATA);
693+
ev.session_id = 0x42;
694+
ev.seq = seq;
695+
ev.mode = mode;
696+
ev.rx_flags = ARQ_FLAG_HAS_DATA;
697+
ev.data_bytes = n;
698+
ev.payload_len = n;
699+
for (size_t i = 0; i < n && i < sizeof(ev.payload); i++)
700+
ev.payload[i] = (uint8_t)(seq * 17 + i);
701+
return ev;
702+
}
703+
704+
/* Run the ACK_TX cycle (guard timer -> pattern ACK -> TX complete) back to
705+
* IDLE_IRS, so the next DATA frame is received in the same state a real IRS is. */
706+
static void complete_ack_tx(void)
707+
{
708+
arq_event_t ev = make_event(ARQ_EV_TIMER_ACK);
709+
arq_fsm_dispatch(&sess, &ev);
710+
ev = make_event(ARQ_EV_TX_COMPLETE);
711+
arq_fsm_dispatch(&sess, &ev);
712+
TEST_ASSERT_EQUAL_INT(ARQ_DFLOW_IDLE_IRS, sess.dflow_state);
713+
}
714+
715+
/* Each clean in-order frame climbs the sender one rung (fast initial ramp);
716+
* the IRS mirror climbs in lock step so peer_tx_mode names the NEXT burst's
717+
* mode before it arrives. */
718+
void test_irs_mirror_climbs_with_peer(void)
719+
{
720+
goto_connected_irs();
721+
TEST_ASSERT_EQUAL_INT(MERCURY_MODE_MFSK, sess.peer_tx_mode);
722+
723+
arq_event_t ev = make_data_event(0, MERCURY_MODE_MFSK, 90);
724+
arq_fsm_dispatch(&sess, &ev);
725+
TEST_ASSERT_EQUAL_INT(FREEDV_MODE_DATAC15, sess.peer_tx_mode); /* level 1 */
726+
complete_ack_tx();
727+
728+
ev = make_data_event(1, FREEDV_MODE_DATAC15, 30);
729+
arq_fsm_dispatch(&sess, &ev);
730+
TEST_ASSERT_EQUAL_INT(FREEDV_MODE_DATAC4, sess.peer_tx_mode); /* level 2 */
731+
complete_ack_tx();
732+
733+
ev = make_data_event(2, FREEDV_MODE_DATAC4, 54);
734+
arq_fsm_dispatch(&sess, &ev);
735+
TEST_ASSERT_EQUAL_INT(FREEDV_MODE_DATAC3, sess.peer_tx_mode); /* level 3 */
736+
}
737+
738+
/* A duplicate frame means our ACK was lost and the sender retried, stepping ITS
739+
* ladder down — the mirror must step down too so we can decode the retransmit. */
740+
void test_irs_mirror_steps_down_on_duplicate(void)
741+
{
742+
goto_connected_irs();
743+
arq_event_t ev = make_data_event(0, MERCURY_MODE_MFSK, 90);
744+
arq_fsm_dispatch(&sess, &ev);
745+
complete_ack_tx();
746+
ev = make_data_event(1, FREEDV_MODE_DATAC15, 30);
747+
arq_fsm_dispatch(&sess, &ev);
748+
complete_ack_tx();
749+
TEST_ASSERT_EQUAL_INT(FREEDV_MODE_DATAC4, sess.peer_tx_mode); /* climbed to level 2 */
750+
751+
/* Duplicate of an already-delivered seq (rx_expected has advanced past it). */
752+
ev = make_data_event(0, MERCURY_MODE_MFSK, 90);
753+
arq_fsm_dispatch(&sess, &ev);
754+
TEST_ASSERT_EQUAL_INT(FREEDV_MODE_DATAC15, sess.peer_tx_mode); /* stepped down to level 1 */
755+
}
756+
757+
/* Reset-on-miss: a full idle hold with no DATA (a lost ACK left us climbed above
758+
* the sender) steps the mirror down toward the floor so the two ends re-sync. */
759+
void test_irs_mirror_resets_toward_floor_on_silence(void)
760+
{
761+
goto_connected_irs();
762+
arq_event_t ev = make_data_event(0, MERCURY_MODE_MFSK, 90);
763+
arq_fsm_dispatch(&sess, &ev);
764+
complete_ack_tx();
765+
TEST_ASSERT_EQUAL_INT(FREEDV_MODE_DATAC15, sess.peer_tx_mode); /* climbed to level 1 */
766+
767+
/* Idle-hold fires with no reverse backlog and a recent RX (not dead yet). */
768+
fake_tx_backlog_fake.return_val = 0;
769+
ev = make_event(ARQ_EV_TIMER_PEER_BACKLOG);
770+
arq_fsm_dispatch(&sess, &ev);
771+
TEST_ASSERT_EQUAL_INT(MERCURY_MODE_MFSK, sess.peer_tx_mode); /* stepped back to the floor */
772+
}
773+
667774
int main(void)
668775
{
669776
UNITY_BEGIN();
@@ -695,5 +802,8 @@ int main(void)
695802
RUN_TEST(test_default_call_accept_slots_are_short);
696803
RUN_TEST(test_stop_listen);
697804
RUN_TEST(test_timeout_ms_idle);
805+
RUN_TEST(test_irs_mirror_climbs_with_peer);
806+
RUN_TEST(test_irs_mirror_steps_down_on_duplicate);
807+
RUN_TEST(test_irs_mirror_resets_toward_floor_on_silence);
698808
return UNITY_END();
699809
}

0 commit comments

Comments
 (0)