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}