Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions data_interfaces/tcp_interfaces.c
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,21 @@ static void execute_control_command(char *buffer)
return;
}

/* Secondary callsigns (remaining tokens) */
/* Secondary callsigns (remaining tokens). Accepted ones are kept so
* each can be acknowledged below: "REGISTERED <Call>" is per call
* sign in VARA, and Mercury answers for every one of these. */
char accepted_secondaries[CALLSIGN_MAX_SECONDARY][CALLSIGN_MAX_SIZE];
int accepted_count = 0;
while ((tok = strtok_r(NULL, " \t\r\n", &saveptr)) != NULL)
{
memset(&cmd, 0, sizeof(cmd));
cmd.type = ARQ_CMD_ADD_SECONDARY_CALLSIGN;
snprintf(cmd.arg0, sizeof(cmd.arg0), "%s", tok);
arq_submit_tcp_cmd(&cmd); /* best-effort; overflow is logged in arq.c */
if (arq_submit_tcp_cmd(&cmd) != 0) /* best-effort; overflow is logged in arq.c */
continue;
if (accepted_count < CALLSIGN_MAX_SECONDARY)
snprintf(accepted_secondaries[accepted_count++],
CALLSIGN_MAX_SIZE, "%s", tok);
}

tcp_write(CTL_TCP_PORT, (uint8_t *)"OK\r", 3);
Expand All @@ -321,6 +329,12 @@ static void execute_control_command(char *buffer)
* after the write makes the order documented in docs/TNC.md hold by
* construction instead of by timing. */
tnc_send_registered(primary_call);
/* One line per callsign the station now answers for. Only the ones
* the ARQ layer actually took: past CALLSIGN_MAX_SECONDARY it drops
* them, and claiming a registration we did not accept would leave the
* host addressing a callsign we will never answer. */
for (int i = 0; i < accepted_count; i++)
tnc_send_registered(accepted_secondaries[i]);
return;
}

Expand Down
15 changes: 14 additions & 1 deletion docs/TNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,20 @@ Sent when a callsign is set via `MYCALL`, confirming the callsign is
licensed (VARA compatibility). Arrives after the `OK\r` response to
`MYCALL`. Also sent on client reconnect if a callsign is already set.

The `<callsign>` is the primary callsign exactly as accepted by `MYCALL`.
`REGISTERED` is **per call sign**, so `MYCALL` emits one line for the primary
and one for each secondary it accepted, in the order given:

```
MYCALL AAAA BBBB CCCC\r
OK\r
REGISTERED AAAA\r
REGISTERED BBBB\r
REGISTERED CCCC\r
```

Secondaries beyond the four Mercury can hold are dropped, and dropped ones are
not acknowledged — the host would otherwise address a callsign this station
never answers.

---

Expand Down
45 changes: 44 additions & 1 deletion tests/data_interfaces/test_tcp_interfaces.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ static int chan_select_call_count = 0;
/* How many tcp_write() calls had already happened when a line was queued.
* Lets a test pin the ORDER of a direct write against a queued notification. */
static int queued_after_tcp_writes = -1;
/* Full sequence, not just the last line: MYCALL acknowledges every callsign
* it accepted, so a test has to see all of them and in order. */
#define MAX_QUEUED_LINES 8
static char queued_lines[MAX_QUEUED_LINES][256];
static int queued_line_count = 0;

chan_t *chan_init(size_t capacity)
{
Expand Down Expand Up @@ -223,6 +228,12 @@ int chan_select(chan_t *recv_chans[], int recv_count, void **recv_out,
if (len < sizeof(last_queued_line))
memcpy(last_queued_line, data_ptr, len);
queued_after_tcp_writes = tcp_write_call_count;
if (queued_line_count < MAX_QUEUED_LINES && len < sizeof(queued_lines[0]))
{
memset(queued_lines[queued_line_count], 0, sizeof(queued_lines[0]));
memcpy(queued_lines[queued_line_count], data_ptr, len);
queued_line_count++;
}
/* Returning 0 signals the message was accepted by the channel, so
* tnc_queue_line() transfers ownership and does not free it (in
* production the send_thread consumer frees each msg). This mock is
Expand Down Expand Up @@ -258,6 +269,8 @@ void setUp(void)
memset(last_queued_line, 0, sizeof(last_queued_line));
chan_select_call_count = 0;
queued_after_tcp_writes = -1;
memset(queued_lines, 0, sizeof(queued_lines));
queued_line_count = 0;
memset(&arq_conn, 0, sizeof(arq_conn));
mock_bandwidth_hz = 2300;

Expand Down Expand Up @@ -322,10 +335,38 @@ void test_cmd_mycall_registered_follows_ok(void)
execute_control_command(cmd);

assert_ok_response();
TEST_ASSERT_EQUAL_STRING("REGISTERED TEST1\r", last_queued_line);
/* First line out is the primary; the secondaries follow it. */
TEST_ASSERT_EQUAL_STRING("REGISTERED TEST1\r", queued_lines[0]);
TEST_ASSERT_GREATER_THAN_INT(0, queued_after_tcp_writes);
}

/* "REGISTERED <Call>" is per call sign in VARA, and MYCALL can carry
* secondaries that Mercury will answer for, so each accepted one is
* acknowledged -- in the order given, primary first. */
void test_cmd_mycall_registers_every_callsign(void)
{
char cmd[] = "MYCALL TEST1 SEC1 SEC2";
execute_control_command(cmd);

assert_ok_response();
TEST_ASSERT_EQUAL_INT(3, queued_line_count);
TEST_ASSERT_EQUAL_STRING("REGISTERED TEST1\r", queued_lines[0]);
TEST_ASSERT_EQUAL_STRING("REGISTERED SEC1\r", queued_lines[1]);
TEST_ASSERT_EQUAL_STRING("REGISTERED SEC2\r", queued_lines[2]);
}

/* Past CALLSIGN_MAX_SECONDARY the ARQ layer drops the extras, so the host
* must not be told they are registered -- it would address a callsign this
* station never answers. */
void test_cmd_mycall_does_not_register_dropped_secondaries(void)
{
char cmd[] = "MYCALL TEST1 S1 S2 S3 S4 S5 S6";
execute_control_command(cmd);

assert_ok_response();
TEST_ASSERT_EQUAL_INT(1 + CALLSIGN_MAX_SECONDARY, queued_line_count);
}

void test_cmd_listen_on(void)
{
char cmd[] = "LISTEN ON";
Expand Down Expand Up @@ -1013,6 +1054,8 @@ int main(void)
/* Command parser tests */
RUN_TEST(test_cmd_mycall);
RUN_TEST(test_cmd_mycall_registered_follows_ok);
RUN_TEST(test_cmd_mycall_registers_every_callsign);
RUN_TEST(test_cmd_mycall_does_not_register_dropped_secondaries);
RUN_TEST(test_cmd_listen_on);
RUN_TEST(test_cmd_listen_off);
RUN_TEST(test_cmd_public_on);
Expand Down
Loading