Repository navigation
plate(gcu-c): ship the escape hatch, host-test the link surface, record measured I2S DAC truths #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
plate(gcu-c): ship the escape hatch, host-test the link surface, record measured I2S DAC truths #115
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,15 @@ | |
| #include <unistd.h> | ||
|
|
||
| /* | ||
| * Link plumbing only. Parsing and dispatch live in portable domain code | ||
| * (gcu_handle_command) so the command surface is host-testable — this file | ||
| * moves bytes, it does not decide what a command means. | ||
| * | ||
| * Identity on the link (#78 / #79): boot-print alone is not enough for | ||
| * silico inspect after the greeting scrolls past. The app must also answer | ||
| * the host word "identity" (CR/LF framed) with fw_name=… fw_version=…. | ||
| * `repl` and `reboot` are required alongside it — a build without the | ||
| * escape hatch cannot be reclaimed without hardware gymnastics. | ||
| * | ||
| * stdin MUST be non-blocking before the forever loop. Blocking getchar() | ||
| * would park app_main and kill the product face (tick/LED) until a host | ||
|
|
@@ -35,7 +41,7 @@ static void stdin_set_nonblocking(void) { | |
| } | ||
| } | ||
|
|
||
| static void drain_identity_command(void) { | ||
| static void drain_link_commands(gcu_state_t *st) { | ||
| static char line[48]; | ||
| static int n; | ||
| int c; | ||
|
|
@@ -48,15 +54,10 @@ static void drain_identity_command(void) { | |
| while ((c = getchar()) != EOF) { | ||
| if (c == '\r' || c == '\n') { | ||
| if (n > 0) { | ||
| char reply[80]; | ||
| line[n] = '\0'; | ||
| char *p = line; | ||
| while (*p && isspace((unsigned char)*p)) { | ||
| p++; | ||
| } | ||
| if (strcmp(p, "identity") == 0) { | ||
| char id[64]; | ||
| gcu_identity_line(id, (int)sizeof id); | ||
| printf("%s\n", id); | ||
| if (gcu_handle_command(st, line, reply, (int)sizeof reply)) { | ||
| printf("%s\n", reply); | ||
| fflush(stdout); | ||
| } | ||
| n = 0; | ||
|
|
@@ -98,7 +99,14 @@ void app_main(void) { | |
|
|
||
| gcu_init(&st, hal); | ||
| for (;;) { | ||
| drain_identity_command(); | ||
| drain_link_commands(&st); | ||
| if (st.reboot_pending) { | ||
| /* Reply already flushed above; outputs already parked by the domain. */ | ||
| st.reboot_pending = 0; | ||
| if (hal && hal->reboot) { | ||
| hal->reboot(hal); | ||
| } | ||
|
Comment on lines
+105
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a derived product leaves the explicitly optional AGENTS.md reference: silico/plates/gcu-c/AGENTS.md:L99-L101 Useful? React with 👍 / 👎. |
||
| } | ||
| gcu_tick(&st); | ||
| if (hal && hal->delay_ms) { | ||
| hal->delay_ms(hal, gcu_tick_sleep_ms(&st)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| /* Link command surface — host test (no hardware). | ||
| * | ||
| * Why this file exists: the escape hatch (`repl` / `reboot`) is a product | ||
| * requirement, and an escape hatch that is only ever exercised on metal is | ||
| * the one that turns out to be missing at the worst moment. Parsing and | ||
| * dispatch live in src/domain.c precisely so this test can run on the host. | ||
| * | ||
| * Extend this alongside the product's declared command surface: every command | ||
| * the product spec lists should have a row here, including the ones that must | ||
| * FAIL (unknown input fails closed with a short error, not a help essay). | ||
| */ | ||
| #include "gcu/defaults.h" | ||
| #include "gcu/domain.h" | ||
| #include "gcu/hal.h" | ||
| #include "gcu/version.h" | ||
|
|
||
| #include <stdio.h> | ||
| #include <string.h> | ||
|
|
||
| static int led_state; | ||
| static int parked_calls; | ||
| static int reboot_calls; | ||
|
|
||
| static void set_led(gcu_hal_t *self, int on) { | ||
| (void)self; | ||
| led_state = on; | ||
| } | ||
|
|
||
| static void delay_ms(gcu_hal_t *self, int ms) { | ||
| (void)self; | ||
| (void)ms; | ||
| } | ||
|
|
||
| static void park_outputs(gcu_hal_t *self) { | ||
| (void)self; | ||
| parked_calls++; | ||
| } | ||
|
|
||
| static void board_reboot(gcu_hal_t *self) { | ||
| (void)self; | ||
| reboot_calls++; | ||
| } | ||
|
|
||
| static int fail(const char *msg) { | ||
| fprintf(stderr, "FAIL: %s\n", msg); | ||
| return 1; | ||
| } | ||
|
|
||
| int main(void) { | ||
| gcu_hal_t hal = { | ||
| .set_led = set_led, | ||
| .delay_ms = delay_ms, | ||
| .park_outputs = park_outputs, | ||
| .reboot = board_reboot, | ||
| }; | ||
| gcu_state_t st; | ||
| char reply[80]; | ||
|
|
||
| /* --- parsing: whole tokens, surrounding whitespace tolerated --- */ | ||
| if (gcu_parse_command("identity") != GCU_CMD_IDENTITY) { | ||
| return fail("identity not parsed"); | ||
| } | ||
| if (gcu_parse_command(" repl \r\n") != GCU_CMD_REPL) { | ||
| return fail("repl not parsed with surrounding whitespace"); | ||
| } | ||
| if (gcu_parse_command("reboot") != GCU_CMD_REBOOT) { | ||
| return fail("reboot not parsed"); | ||
| } | ||
| if (gcu_parse_command("") != GCU_CMD_NONE || | ||
| gcu_parse_command(" ") != GCU_CMD_NONE) { | ||
| return fail("blank line should be NONE"); | ||
| } | ||
| if (gcu_parse_command(NULL) != GCU_CMD_NONE) { | ||
| return fail("NULL line should be NONE"); | ||
| } | ||
| /* Prefix/substring must not match a command. */ | ||
| if (gcu_parse_command("identityX") != GCU_CMD_UNKNOWN || | ||
| gcu_parse_command("rep") != GCU_CMD_UNKNOWN) { | ||
| return fail("partial token matched a command"); | ||
| } | ||
|
|
||
| /* --- identity --- */ | ||
| gcu_init(&st, &hal); | ||
| if (!gcu_handle_command(&st, "identity", reply, (int)sizeof reply)) { | ||
| return fail("identity produced no reply"); | ||
| } | ||
| if (strstr(reply, "fw_name=") == NULL || strstr(reply, "fw_version=") == NULL) { | ||
| return fail("identity reply missing fw_name/fw_version"); | ||
| } | ||
| if (st.parked) { | ||
| return fail("identity must not park outputs"); | ||
| } | ||
|
|
||
| /* --- blank line: no reply, no chatter on the link --- */ | ||
| if (gcu_handle_command(&st, " ", reply, (int)sizeof reply)) { | ||
| return fail("blank line should produce no reply"); | ||
| } | ||
|
|
||
| /* --- unknown fails closed and short --- */ | ||
| if (!gcu_handle_command(&st, "sing", reply, (int)sizeof reply)) { | ||
| return fail("unknown command produced no reply"); | ||
| } | ||
| if (strncmp(reply, "err", 3) != 0) { | ||
| return fail("unknown command should reply with a short error"); | ||
| } | ||
| if (st.parked) { | ||
| return fail("unknown command must not park outputs"); | ||
| } | ||
|
|
||
| /* --- repl parks outputs and releases the console --- */ | ||
| gcu_init(&st, &hal); | ||
| led_state = 1; | ||
| parked_calls = 0; | ||
| if (!gcu_handle_command(&st, "repl", reply, (int)sizeof reply)) { | ||
| return fail("repl produced no reply"); | ||
| } | ||
| if (!st.parked) { | ||
| return fail("repl did not set parked"); | ||
| } | ||
| if (parked_calls != 1) { | ||
| return fail("repl did not call hal park_outputs"); | ||
| } | ||
| if (led_state != 0) { | ||
| return fail("repl left the LED driven"); | ||
| } | ||
| if (st.reboot_pending) { | ||
| return fail("repl must not request a reboot"); | ||
| } | ||
| /* Parked means parked: further ticks do not resume driving the face. */ | ||
| led_state = 1; | ||
| gcu_tick(&st); | ||
| gcu_tick(&st); | ||
| if (led_state != 1) { | ||
| return fail("tick drove outputs after repl parked them"); | ||
| } | ||
|
|
||
| /* --- reboot parks, acks, and defers the reset to main --- */ | ||
| gcu_init(&st, &hal); | ||
| parked_calls = 0; | ||
| reboot_calls = 0; | ||
| if (!gcu_handle_command(&st, "reboot", reply, (int)sizeof reply)) { | ||
| return fail("reboot produced no reply"); | ||
| } | ||
| if (parked_calls != 1) { | ||
| return fail("reboot did not park outputs"); | ||
| } | ||
| if (!st.reboot_pending) { | ||
| return fail("reboot did not set reboot_pending"); | ||
| } | ||
| if (reboot_calls != 0) { | ||
| return fail("domain must not reset before the reply is flushed"); | ||
| } | ||
|
|
||
| /* --- undersized reply buffer is refused, not overflowed --- */ | ||
| gcu_init(&st, &hal); | ||
| { | ||
| char tiny[4]; | ||
| if (gcu_handle_command(&st, "identity", tiny, (int)sizeof tiny)) { | ||
| return fail("undersized buffer should be refused"); | ||
| } | ||
| } | ||
|
|
||
| printf("OK protocol identity+repl+reboot+unknown\n"); | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a serial line exceeds 47 bytes, the existing overflow branch resets
nto zero but continues collecting the suffix, and this new dispatch call treats that suffix as a complete command. Thus an oversized or malformed line ending inreplorrebootcan unexpectedly park the product or reset it instead of failing closed; retain an overflow flag and ignore all bytes until the next delimiter.AGENTS.md reference: silico/plates/gcu-c/AGENTS.md:L105-L110
Useful? React with 👍 / 👎.