QUELLCODE / C
main.c
pico/main.c
1/*2 * OpenAss: nur privater, experimenteller Gebrauch; auf eigene Verantwortung.3 * Niemals Unternehmensrechner, Produktivsysteme, Industrieanlagen oder4 * sicherheitskritische Systeme anschließen oder bedienen.5 * Haftungshinweis und gesetzliche Grenzen: ../DISCLAIMER.txt.6 */7#include <stdbool.h>8#include <stdint.h>9#include <string.h>1011#include "hardware/gpio.h"12#include "hardware/uart.h"13#include "hardware/watchdog.h"14#include "pico/stdlib.h"15#include "tusb.h"1617#include "protocol.h"18#include "usb_descriptors.h"1920#define HB_UART uart021#define HB_UART_TX_PIN 0u22#define HB_UART_RX_PIN 1u23#define HB_LOCAL_ARM_PIN 14u24#define HB_INTERLOCK_PIN 15u25#define HB_UART_BAUD 115200u26#define HB_WATCHDOG_US 750000u27#define HB_LOCAL_WINDOW_US 10000000u28#define HB_DEBOUNCE_US 30000u29#define HB_REPORT_TIMEOUT_US 100000u30#define HB_RX_TIMEOUT_US 250000u31#define HB_TX_CAPACITY 256u3233static volatile bool estop_irq_latched;34static bool estop_latched;35static bool armed;36static uint64_t last_ping_us;37static uint64_t local_window_until_us;38static const char *stop_reason = "NOT_ARMED";3940// HID state is explicit: no text mapping, no heap, no queued macro execution.41static uint8_t keyboard_state[8];42static uint8_t absolute_state[5];43static uint8_t relative_state[5];44static bool absolute_known;45static uint8_t release_pending;46static uint8_t release_inflight;47static uint32_t report_completed[HB_HID_COUNT];48static uint32_t report_failed[HB_HID_COUNT];49static uint8_t idle_rate[HB_HID_COUNT];50static uint64_t last_report_us[HB_HID_COUNT];5152static hb_sequence_t sequence_state;53static char rx_line[HB_FRAME_MAX + 1u];54static size_t rx_length;55static bool rx_discard;56static uint64_t rx_started_us;57static char tx_buffer[HB_TX_CAPACITY];58static size_t tx_head;59static size_t tx_tail;6061static void request_releases(void) {62 memset(keyboard_state, 0, sizeof(keyboard_state));63 absolute_state[0] = 0;64 memset(relative_state, 0, sizeof(relative_state));65 // Sending an unsolicited absolute zero report moves a pointer. Before the66 // first ABS command, only keyboard and relative interfaces need release.67 release_pending |= (uint8_t)((1u << HB_HID_KEYBOARD) | (1u << HB_HID_RELATIVE));68 if (absolute_known) release_pending |= (uint8_t)(1u << HB_HID_ABSOLUTE);69}7071static void disarm(const char *reason) {72 armed = false;73 local_window_until_us = 0;74 stop_reason = reason;75 request_releases();76}7778static void interlock_irq(uint gpio, uint32_t events) {79 (void)gpio;80 if (events & GPIO_IRQ_EDGE_RISE) estop_irq_latched = true;81}8283static void safety_service(void) {84 uint64_t now = time_us_64();85 if (gpio_get(HB_INTERLOCK_PIN)) estop_irq_latched = true;86 if (estop_irq_latched && !estop_latched) {87 estop_latched = true;88 disarm("ESTOP");89 }90 if (armed && now - last_ping_us >= HB_WATCHDOG_US) disarm("WATCHDOG");91 if (armed && (!tud_mounted() || tud_suspended())) disarm("USB_NOT_READY");9293 // A fresh, debounced press is required: holding GP14 low at startup grants94 // no window. Release it once, then press it. GP14 never clears an E-stop.95 static bool raw_last = false;96 static bool stable_pressed = true;97 static uint64_t raw_changed_us;98 bool pressed = !gpio_get(HB_LOCAL_ARM_PIN);99 if (pressed != raw_last) {100 raw_last = pressed;101 raw_changed_us = now;102 }103 if (pressed != stable_pressed && now - raw_changed_us >= HB_DEBOUNCE_US) {104 stable_pressed = pressed;105 if (pressed && !armed && !estop_latched) local_window_until_us = now + HB_LOCAL_WINDOW_US;106 }107 if (local_window_until_us && now >= local_window_until_us) local_window_until_us = 0;108#ifdef PICO_DEFAULT_LED_PIN109 // Solid = armed; slow flash = local grant; fast flash = latched E-stop.110 bool led = armed || (estop_latched && ((now / 100000u) & 1u)) ||111 (local_window_until_us && ((now / 500000u) & 1u));112 gpio_put(PICO_DEFAULT_LED_PIN, led);113#endif114}115116static size_t tx_free(void) {117 return (tx_tail + HB_TX_CAPACITY - tx_head - 1u) % HB_TX_CAPACITY;118}119120static void uart_tx_service(void) {121 while (tx_tail != tx_head && uart_is_writable(HB_UART)) {122 uart_putc_raw(HB_UART, tx_buffer[tx_tail]);123 tx_tail = (tx_tail + 1u) % HB_TX_CAPACITY;124 }125}126127static bool uart_queue(const char *response) {128 size_t length = strlen(response);129 if (length > tx_free()) {130 // The parser reserves HB_RESPONSE_MAX bytes before consuming a frame;131 // fail closed if that invariant ever changes.132 disarm("UART_TX_FULL");133 return false;134 }135 for (size_t i = 0; i < length; ++i) {136 tx_buffer[tx_head] = response[i];137 tx_head = (tx_head + 1u) % HB_TX_CAPACITY;138 }139 return true;140}141142static uint8_t *state_for(uint8_t instance, uint16_t *length) {143 switch (instance) {144 case HB_HID_KEYBOARD: *length = sizeof(keyboard_state); return keyboard_state;145 case HB_HID_ABSOLUTE: *length = sizeof(absolute_state); return absolute_state;146 case HB_HID_RELATIVE:147 *length = tud_hid_n_get_protocol(instance) == HID_PROTOCOL_BOOT ? 3u : 5u;148 return relative_state;149 default: *length = 0; return NULL;150 }151}152153static void release_service(void) {154 if (!tud_mounted() || tud_suspended()) return;155 for (uint8_t instance = 0; instance < HB_HID_COUNT; ++instance) {156 uint8_t bit = (uint8_t)(1u << instance);157 if (!(release_pending & bit) || (release_inflight & bit) || !tud_hid_n_ready(instance)) continue;158 uint16_t length;159 uint8_t *report = state_for(instance, &length);160 if (tud_hid_n_report(instance, 0, report, length)) release_inflight |= bit;161 }162}163164static void idle_service(void) {165 if (!tud_mounted() || tud_suspended()) return;166 uint64_t now = time_us_64();167 for (uint8_t instance = 0; instance < HB_HID_COUNT; ++instance) {168 uint8_t bit = (uint8_t)(1u << instance);169 if (!idle_rate[instance] || ((release_pending | release_inflight) & bit)) continue;170 if (instance == HB_HID_ABSOLUTE && !absolute_known) continue;171 if (now - last_report_us[instance] < (uint64_t)idle_rate[instance] * 4000u) continue;172 if (!tud_hid_n_ready(instance)) continue;173 uint16_t length;174 uint8_t *state = state_for(instance, &length);175 if (tud_hid_n_report(instance, 0, state, length)) last_report_us[instance] = now;176 }177}178179static void core_service(void) {180 safety_service();181 tud_task();182 safety_service();183 release_service();184 idle_service();185 uart_tx_service();186 watchdog_update();187}188189static const char *wait_released(bool require_armed, uint64_t deadline) {190 while (release_pending || release_inflight) {191 core_service();192 if (require_armed && !armed) return stop_reason;193 if (!tud_mounted() || tud_suspended()) return "USB_NOT_READY";194 if (time_us_64() >= deadline) {195 disarm("USB_TIMEOUT");196 return "USB_TIMEOUT";197 }198 tight_loop_contents();199 }200 return NULL;201}202203static const char *send_input(uint8_t instance, const uint8_t *report, uint16_t length) {204 uint64_t deadline = time_us_64() + HB_REPORT_TIMEOUT_US;205 const char *error = wait_released(true, deadline);206 if (error) return error;207 while (!tud_hid_n_ready(instance)) {208 core_service();209 if (!armed) return stop_reason;210 if (time_us_64() >= deadline) {211 disarm("USB_TIMEOUT");212 return "USB_TIMEOUT";213 }214 tight_loop_contents();215 }216 safety_service();217 if (!armed) return stop_reason;218 if (time_us_64() >= deadline) {219 disarm("USB_TIMEOUT");220 return "USB_TIMEOUT";221 }222 uint32_t completed = report_completed[instance];223 uint32_t failed = report_failed[instance];224 if (!tud_hid_n_report(instance, 0, report, length)) {225 disarm("USB_FAILED");226 return "USB_FAILED";227 }228 uint16_t state_length;229 uint8_t *state = state_for(instance, &state_length);230 (void)state_length;231 memcpy(state, report, length);232 if (instance == HB_HID_ABSOLUTE) absolute_known = true;233 if (instance == HB_HID_RELATIVE) memset(relative_state + 1, 0, 4u);234235 // Wait for USB transfer completion with safety checks throughout. A stop236 // cannot retract an in-flight report; it schedules zero reports afterward.237 while (report_completed[instance] == completed && report_failed[instance] == failed) {238 core_service();239 if (!armed) return stop_reason;240 if (time_us_64() >= deadline) {241 disarm("USB_TIMEOUT");242 return "USB_TIMEOUT";243 }244 tight_loop_contents();245 }246 if (report_failed[instance] != failed) {247 disarm("USB_FAILED");248 return "USB_FAILED";249 }250 safety_service();251 if (!armed) return stop_reason;252 // Relative deltas are events, not held state: GET_REPORT and safety release253 // must never repeat a movement or scroll.254 return NULL;255}256257static bool within(int32_t value, int32_t minimum, int32_t maximum) {258 return value >= minimum && value <= maximum;259}260261static const char *error_payload(const char *error) {262 if (!strcmp(error, "ESTOP")) return "ERR ESTOP";263 if (!strcmp(error, "WATCHDOG")) return "ERR WATCHDOG";264 if (!strcmp(error, "USB_TIMEOUT")) return "ERR USB_TIMEOUT";265 if (!strcmp(error, "USB_FAILED")) return "ERR USB_FAILED";266 if (!strcmp(error, "USB_NOT_READY")) return "ERR USB_NOT_READY";267 return "ERR NOT_ARMED";268}269270static const char *execute(const hb_request_t *request) {271 const int32_t *args = request->args;272 size_t count = request->argc;273 if (!strcmp(request->op, "HELLO")) {274 if (count) return "ERR ARG_COUNT";275 return "OK 1";276 }277 if (!strcmp(request->op, "STOP")) {278 if (count) return "ERR ARG_COUNT";279 disarm("STOPPED");280 return "OK";281 }282 if (!strcmp(request->op, "ARM")) {283 if (count) return "ERR ARG_COUNT";284 if (estop_latched) return "ERR ESTOP";285 if (armed) return "ERR ALREADY_ARMED";286 if (!local_window_until_us) return "ERR LOCAL_ARM_REQUIRED";287 if (!tud_mounted() || tud_suspended()) return "ERR USB_NOT_READY";288 const char *error = wait_released(false, time_us_64() + HB_REPORT_TIMEOUT_US);289 if (error) return error_payload(error);290 safety_service();291 if (estop_latched) return "ERR ESTOP";292 if (!local_window_until_us) return "ERR LOCAL_ARM_REQUIRED";293 if (!tud_mounted() || tud_suspended()) return "ERR USB_NOT_READY";294 local_window_until_us = 0;295 last_ping_us = time_us_64();296 armed = true;297 return "OK";298 }299 if (!strcmp(request->op, "PING")) {300 if (count) return "ERR ARG_COUNT";301 if (estop_latched) return "ERR ESTOP";302 if (!armed) return "ERR NOT_ARMED";303 last_ping_us = time_us_64();304 return "OK";305 }306 if (!strcmp(request->op, "RELEASE")) {307 if (count) return "ERR ARG_COUNT";308 request_releases();309 const char *error = wait_released(armed, time_us_64() + HB_REPORT_TIMEOUT_US);310 if (error) return error_payload(error);311 return "OK";312 }313314 uint8_t report[8] = {0};315 uint8_t instance;316 uint16_t length;317 if (!strcmp(request->op, "ABS")) {318 if (count != 3u) return "ERR ARG_COUNT";319 if (!within(args[0], 0, 32767) || !within(args[1], 0, 32767) || !within(args[2], 0, 7)) return "ERR RANGE";320 instance = HB_HID_ABSOLUTE;321 length = 5u;322 report[0] = (uint8_t)args[2];323 report[1] = (uint8_t)args[0]; report[2] = (uint8_t)((uint32_t)args[0] >> 8u);324 report[3] = (uint8_t)args[1]; report[4] = (uint8_t)((uint32_t)args[1] >> 8u);325 } else if (!strcmp(request->op, "REL")) {326 if (count != 5u) return "ERR ARG_COUNT";327 for (size_t i = 0; i < 4u; ++i) if (!within(args[i], -127, 127)) return "ERR RANGE";328 if (!within(args[4], 0, 7)) return "ERR RANGE";329 instance = HB_HID_RELATIVE;330 bool boot = tud_hid_n_get_protocol(instance) == HID_PROTOCOL_BOOT;331 // Do not silently acknowledge scrolling that a boot host cannot see.332 if (boot && (args[2] || args[3])) return "ERR BOOT_SCROLL_UNAVAILABLE";333 length = boot ? 3u : 5u;334 report[0] = (uint8_t)args[4];335 for (size_t i = 0; i < 4u; ++i) report[i + 1u] = (uint8_t)(int8_t)args[i];336 } else if (!strcmp(request->op, "KEY")) {337 if (count != 7u) return "ERR ARG_COUNT";338 for (size_t i = 0; i < 7u; ++i) if (!within(args[i], 0, 255)) return "ERR RANGE";339 instance = HB_HID_KEYBOARD;340 length = 8u;341 report[0] = (uint8_t)args[0];342 for (size_t i = 0; i < 6u; ++i) report[i + 2u] = (uint8_t)args[i + 1u];343 } else return "ERR UNKNOWN_OP";344345 if (estop_latched) return "ERR ESTOP";346 if (!armed) return "ERR NOT_ARMED";347348 // Switching pointer interfaces while a button is held can leave the other349 // logical mouse dragging. Release the previous mouse before switching.350 if (instance == HB_HID_ABSOLUTE && relative_state[0]) {351 relative_state[0] = 0;352 release_pending |= (uint8_t)(1u << HB_HID_RELATIVE);353 }354 if (instance == HB_HID_RELATIVE && absolute_state[0]) {355 absolute_state[0] = 0;356 release_pending |= (uint8_t)(1u << HB_HID_ABSOLUTE);357 }358 const char *error = send_input(instance, report, length);359 if (!error) return "OK";360 return error_payload(error);361}362363static void process_line(const char *line, size_t length) {364 hb_request_t request;365 char response[HB_RESPONSE_MAX];366 hb_parse_status_t parsed = hb_parse(line, length, &request);367 if (parsed != HB_PARSE_OK) {368 if (hb_response(response, sizeof(response), request.seq,369 parsed == HB_PARSE_CRC ? "ERR CRC" : "ERR BAD_FRAME")) uart_queue(response);370 return;371 }372 safety_service();373 hb_sequence_status_t ordering = hb_sequence_check(&sequence_state, &request, line, length, !armed);374 if (ordering == HB_SEQ_DUPLICATE) {375 uart_queue(sequence_state.response);376 return;377 }378 if (ordering != HB_SEQ_NEW) {379 if (hb_response(response, sizeof(response), request.seq,380 ordering == HB_SEQ_OLD ? "ERR SEQ_OLD" : "ERR SEQ_CONFLICT")) uart_queue(response);381 return;382 }383 const char *payload = execute(&request);384 if (hb_response(response, sizeof(response), request.seq, payload)) {385 hb_sequence_commit(&sequence_state, &request, line, length, response);386 uart_queue(response);387 } else disarm("RESPONSE_FAILED");388}389390static void uart_rx_service(void) {391 if (rx_length && time_us_64() - rx_started_us >= HB_RX_TIMEOUT_US) {392 rx_length = 0;393 rx_discard = true;394 }395 unsigned budget = 64u;396 while (budget-- && tx_free() >= HB_RESPONSE_MAX && uart_is_readable(HB_UART)) {397 safety_service();398 // Read the PL011 error flags with the byte. A framing/parity/break or399 // FIFO-overrun error invalidates the whole line up to its next LF.400 uint32_t raw = uart_get_hw(HB_UART)->dr;401 uint8_t byte = (uint8_t)raw;402 if (raw & 0x0f00u) {403 uart_get_hw(HB_UART)->rsr = 0;404 rx_length = 0;405 rx_discard = true;406 }407 if (byte == '\n') {408 if (rx_discard) {409 char response[HB_RESPONSE_MAX];410 if (hb_response(response, sizeof(response), 0, "ERR BAD_FRAME")) uart_queue(response);411 } else if (rx_length) {412 process_line(rx_line, rx_length);413 }414 rx_length = 0;415 rx_discard = false;416 } else if (!rx_discard) {417 if (!rx_length) rx_started_us = time_us_64();418 if (byte < 0x20u || byte > 0x7eu || rx_length >= HB_FRAME_MAX) {419 rx_length = 0;420 rx_discard = true;421 } else rx_line[rx_length++] = (char)byte;422 }423 }424}425426void tud_mount_cb(void) {427 release_inflight = 0;428 disarm("USB_RECONNECTED");429}430431void tud_umount_cb(void) {432 release_inflight = 0;433 disarm("USB_DISCONNECTED");434}435436void tud_suspend_cb(bool remote_wakeup_enabled) {437 (void)remote_wakeup_enabled;438 disarm("USB_SUSPENDED");439}440441void tud_resume_cb(void) {442 release_inflight = 0;443 disarm("USB_RESUMED");444}445446void tud_hid_report_complete_cb(uint8_t instance, const uint8_t *report, uint16_t length) {447 (void)report; (void)length;448 if (instance >= HB_HID_COUNT) return;449 ++report_completed[instance];450 last_report_us[instance] = time_us_64();451 uint8_t bit = (uint8_t)(1u << instance);452 if (release_inflight & bit) {453 release_inflight &= (uint8_t)~bit;454 release_pending &= (uint8_t)~bit;455 }456}457458void tud_hid_report_failed_cb(uint8_t instance, hid_report_type_t type, const uint8_t *report, uint16_t length) {459 (void)type; (void)report; (void)length;460 if (instance >= HB_HID_COUNT) return;461 ++report_failed[instance];462 release_inflight &= (uint8_t)~(1u << instance);463 disarm("USB_FAILED");464}465466uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t type,467 uint8_t *buffer, uint16_t requested_length) {468 if (report_id != 0 || type != HID_REPORT_TYPE_INPUT || instance >= HB_HID_COUNT) return 0;469 safety_service();470 if (instance == HB_HID_ABSOLUTE && !absolute_known) return 0;471 uint16_t length;472 uint8_t *state = state_for(instance, &length);473 if (length > requested_length) length = requested_length;474 memcpy(buffer, state, length);475 return length;476}477478void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t type,479 const uint8_t *buffer, uint16_t length) {480 (void)instance; (void)report_id; (void)type; (void)buffer; (void)length;481 // Keyboard LEDs are output reports. They grant no control permission.482}483484void tud_hid_set_protocol_cb(uint8_t instance, uint8_t protocol) {485 (void)instance; (void)protocol;486 // Protocol changes can alter report lengths; disarm and start with zeros.487 disarm("USB_PROTOCOL_CHANGED");488}489490bool tud_hid_set_idle_cb(uint8_t instance, uint8_t rate) {491 if (instance >= HB_HID_COUNT) return false;492 idle_rate[instance] = rate;493 last_report_us[instance] = time_us_64();494 return true;495}496497int main(void) {498 gpio_init(HB_LOCAL_ARM_PIN);499 gpio_set_dir(HB_LOCAL_ARM_PIN, GPIO_IN);500 gpio_pull_up(HB_LOCAL_ARM_PIN);501 gpio_init(HB_INTERLOCK_PIN);502 gpio_set_dir(HB_INTERLOCK_PIN, GPIO_IN);503 gpio_pull_up(HB_INTERLOCK_PIN);504 estop_irq_latched = gpio_get(HB_INTERLOCK_PIN);505 gpio_set_irq_enabled_with_callback(HB_INTERLOCK_PIN, GPIO_IRQ_EDGE_RISE, true, interlock_irq);506#ifdef PICO_DEFAULT_LED_PIN507 gpio_init(PICO_DEFAULT_LED_PIN);508 gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);509#endif510 uart_init(HB_UART, HB_UART_BAUD);511 gpio_set_function(HB_UART_TX_PIN, GPIO_FUNC_UART);512 gpio_set_function(HB_UART_RX_PIN, GPIO_FUNC_UART);513 uart_set_hw_flow(HB_UART, false, false);514 uart_set_format(HB_UART, 8, 1, UART_PARITY_NONE);515 uart_set_fifo_enabled(HB_UART, true);516 watchdog_enable(1000, false);517 tud_init(0);518 request_releases();519 for (;;) {520 core_service();521 uart_rx_service();522 tight_loop_contents();523 }524}