diff --git a/CMakeLists.txt b/CMakeLists.txt index 73e20e4..6bba88c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,7 @@ set(CPCD_SOURCES "${PROJECT_SOURCE_DIR}/driver/driver_ezsp.c" "${PROJECT_SOURCE_DIR}/driver/driver_kill.c" "${PROJECT_SOURCE_DIR}/driver/driver_spi.c" + "${PROJECT_SOURCE_DIR}/driver/driver_tcp.c" "${PROJECT_SOURCE_DIR}/driver/driver_uart.c" "${PROJECT_SOURCE_DIR}/driver/driver_xmodem.c" "${PROJECT_SOURCE_DIR}/misc/board_controller.c" diff --git a/driver/driver_tcp.c b/driver/driver_tcp.c new file mode 100644 index 0000000..7038c22 --- /dev/null +++ b/driver/driver_tcp.c @@ -0,0 +1,580 @@ +/***************************************************************************//** + * @file + * @brief Co-Processor Communication Protocol (CPC) - TCP driver + ******************************************************************************* + * # License + * Copyright 2022 Silicon Laboratories Inc. www.silabs.com + ******************************************************************************* + * + * The licensor of this software is Silicon Laboratories Inc. Your use of this + * software is governed by the terms of Silicon Labs Master Software License + * Agreement (MSLA) available at + * www.silabs.com/about-us/legal/master-software-license-agreement. This + * software is distributed to you in Source Code format and is governed by the + * sections of the MSLA applicable to Source Code. + * + ******************************************************************************/ + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cpcd/config.h" +#include "cpcd/logging.h" +#include "cpcd/sleep.h" +#include "cpcd/utils.h" + +#include "driver/driver_tcp.h" +#include "server_core/core/hdlc.h" +#include "server_core/core/crc.h" +#include "driver/driver_kill.h" + +#define TCP_BUFFER_SIZE (4096 + SLI_CPC_HDLC_HEADER_RAW_SIZE) + +// Fixed back-off between reconnection attempts (milliseconds) +#define TCP_RECONNECT_BACKOFF_MS 1000 +// Bounded retries for the very first connection before giving up (× backoff) +#define TCP_INITIAL_CONNECT_ATTEMPTS 60 + +static int fd_tcp = -1; +static int fd_core; +static int fd_core_notify; +static int fd_stop_drv; + +static const char *server_address; +static char server_port_str[8]; + +// Protects fd_tcp and tcp_connected against the rx (reconnecting) thread and +// the tx (writing) thread touching the socket concurrently. +static pthread_mutex_t tcp_lock = PTHREAD_MUTEX_INITIALIZER; +static bool tcp_connected = false; + +static pthread_t rx_drv_thread; +static bool rx_drv_thread_started = false; + +static pthread_t tx_drv_thread; +static bool tx_drv_thread_started = false; + +// rx frame-delimiter state — file scope so a reconnect can reset it cleanly +static uint8_t rx_buffer[TCP_BUFFER_SIZE]; +static size_t rx_buffer_head = 0; +static enum { EXPECTING_HEADER, EXPECTING_PAYLOAD } rx_state = EXPECTING_HEADER; + +static void* receive_driver_thread_func(void* param); +static void* transmit_driver_thread_func(void* param); + +static int driver_tcp_try_connect(void); +static int driver_tcp_connect_blocking(int max_attempts); +static bool stop_requested(int timeout_ms); + +// Reads once from the socket, then delimits and pushes every complete frame the +// buffer now holds. On a transport disconnect, sets *disconnected. +static void driver_tcp_process_tcp(bool *disconnected); + +static bool validate_header(uint8_t *header_start); +static bool header_synch(uint8_t *buffer, size_t *buffer_head); +static bool delimit_and_push_frames_to_core(uint8_t *buffer, size_t *buffer_head); + +void driver_tcp_init(int *fd_to_core, int *fd_notify_core, const char *address, unsigned int port) +{ + int fd_sockets[2]; + int fd_sockets_notify[2]; + ssize_t ret; + + server_address = address; + snprintf(server_port_str, sizeof(server_port_str), "%u", port); + + // Create the stop event first so the (blocking) connect can honour a kill + fd_stop_drv = eventfd(0, EFD_CLOEXEC); + FATAL_SYSCALL_ON(fd_stop_drv == -1); + + fd_tcp = driver_tcp_connect_blocking(TCP_INITIAL_CONNECT_ATTEMPTS); + if (fd_tcp < 0) { + FATAL("Could not connect to %s:%s", server_address, server_port_str); + } + tcp_connected = true; + + ret = socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fd_sockets); + FATAL_SYSCALL_ON(ret < 0); + + fd_core = fd_sockets[0]; + *fd_to_core = fd_sockets[1]; + + ret = socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fd_sockets_notify); + FATAL_SYSCALL_ON(ret < 0); + + fd_core_notify = fd_sockets_notify[0]; + *fd_notify_core = fd_sockets_notify[1]; + + driver_kill_init(driver_tcp_kill); + + ret = pthread_create(&tx_drv_thread, NULL, transmit_driver_thread_func, NULL); + FATAL_ON(ret != 0); + tx_drv_thread_started = true; + + ret = pthread_create(&rx_drv_thread, NULL, receive_driver_thread_func, NULL); + FATAL_ON(ret != 0); + rx_drv_thread_started = true; + + ret = pthread_setname_np(tx_drv_thread, "tx_drv_thread"); + FATAL_ON(ret != 0); + ret = pthread_setname_np(rx_drv_thread, "rx_drv_thread"); + FATAL_ON(ret != 0); + + TRACE_DRIVER("Connected to %s:%s", server_address, server_port_str); + TRACE_DRIVER("Init done"); +} + +void driver_tcp_kill(void) +{ + ssize_t ret; + const uint64_t event_value = 1; + + ret = write(fd_stop_drv, &event_value, sizeof(event_value)); + FATAL_SYSCALL_ON(ret != sizeof(event_value)); + + if (tx_drv_thread_started) { + pthread_join(tx_drv_thread, NULL); + tx_drv_thread_started = false; + } + if (rx_drv_thread_started) { + pthread_join(rx_drv_thread, NULL); + rx_drv_thread_started = false; + } + + TRACE_DRIVER("TCP driver threads cancelled"); + + if (fd_tcp >= 0) { + close(fd_tcp); + } + close(fd_core); + close(fd_core_notify); + close(fd_stop_drv); +} + +/* + * Poll fd_stop_drv (without consuming it — both worker threads observe it + * level-triggered) for up to timeout_ms. Returns true if a kill was requested. + */ +static bool stop_requested(int timeout_ms) +{ + struct pollfd pfd = { .fd = fd_stop_drv, .events = POLLIN }; + int ret; + + do { + ret = poll(&pfd, 1, timeout_ms); + } while (ret == -1 && errno == EINTR); + + FATAL_SYSCALL_ON(ret < 0); + + return ret > 0 && (pfd.revents & POLLIN); +} + +/* One connection attempt. Returns a connected, TCP_NODELAY socket fd, or -1. */ +static int driver_tcp_try_connect(void) +{ + struct addrinfo hints = { 0 }; + struct addrinfo *result = NULL; + struct addrinfo *rp; + int fd = -1; + int gai; + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + gai = getaddrinfo(server_address, server_port_str, &hints, &result); + if (gai != 0) { + TRACE_DRIVER("getaddrinfo(%s:%s): %s", server_address, server_port_str, gai_strerror(gai)); + return -1; + } + + for (rp = result; rp != NULL; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol); + if (fd < 0) { + continue; + } + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) { + break; // success + } + close(fd); + fd = -1; + } + + freeaddrinfo(result); + + if (fd < 0) { + return -1; + } + + // Mirror the gateway-side bridge: disable Nagle so CPC's tight ACK timing + // is not coalesced (rtl8196e-uart-bridge sets TCP_NODELAY on its end too). + { + int one = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) < 0) { + WARN("TCP_NODELAY: %s", strerror(errno)); + } + } + + return fd; +} + +/* + * Loop driver_tcp_try_connect() with a fixed back-off until it succeeds, a kill + * is requested, or (when max_attempts > 0) the attempt budget is exhausted. + * Returns the connected fd, or -1. + */ +static int driver_tcp_connect_blocking(int max_attempts) +{ + int attempt = 0; + + while (true) { + int fd = driver_tcp_try_connect(); + if (fd >= 0) { + return fd; + } + + attempt++; + if (max_attempts > 0 && attempt >= max_attempts) { + return -1; + } + + TRACE_DRIVER("Connection to %s:%s failed, retrying in %d ms", + server_address, server_port_str, TCP_RECONNECT_BACKOFF_MS); + + if (stop_requested(TCP_RECONNECT_BACKOFF_MS)) { + return -1; + } + } +} + +static void* receive_driver_thread_func(void* param) +{ + struct epoll_event events[2] = {}; + bool exit_thread = false; + int fd_epoll; + int ret; + + (void) param; + + TRACE_DRIVER("Receiver thread start"); + + fd_epoll = epoll_create1(EPOLL_CLOEXEC); + FATAL_SYSCALL_ON(fd_epoll < 0); + + events[0].events = EPOLLIN; + events[0].data.fd = fd_tcp; + ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_tcp, &events[0]); + FATAL_SYSCALL_ON(ret < 0); + + events[1].events = EPOLLIN; + events[1].data.fd = fd_stop_drv; + ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_stop_drv, &events[1]); + FATAL_SYSCALL_ON(ret < 0); + + while (!exit_thread) { + int event_count; + + do { + event_count = epoll_wait(fd_epoll, events, 2, -1); + if (event_count == -1 && errno == EINTR) { + continue; + } + FATAL_SYSCALL_ON(event_count == -1); + break; + } while (1); + + FATAL_ON(event_count == 0); + + for (size_t event_i = 0; event_i != (size_t)event_count; event_i++) { + int current_event_fd = events[event_i].data.fd; + + if (current_event_fd == fd_stop_drv) { + exit_thread = true; + continue; + } + + if (current_event_fd == fd_tcp) { + bool disconnected = false; + + driver_tcp_process_tcp(&disconnected); + + if (disconnected) { + int old_fd = fd_tcp; + int new_fd; + + WARN("TCP link to %s:%s dropped, reconnecting", server_address, server_port_str); + + // Closing old_fd removes it from this epoll set automatically. + epoll_ctl(fd_epoll, EPOLL_CTL_DEL, old_fd, NULL); + + pthread_mutex_lock(&tcp_lock); + tcp_connected = false; + close(old_fd); + fd_tcp = -1; + new_fd = driver_tcp_connect_blocking(0 /* forever, until stop */); + if (new_fd >= 0) { + fd_tcp = new_fd; + tcp_connected = true; + } + pthread_mutex_unlock(&tcp_lock); + + if (new_fd < 0) { + // Only returns < 0 on a kill request + exit_thread = true; + break; + } + + // Drop any partially-buffered frame from the dead connection + rx_buffer_head = 0; + rx_state = EXPECTING_HEADER; + + events[0].events = EPOLLIN; + events[0].data.fd = new_fd; + ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, new_fd, &events[0]); + FATAL_SYSCALL_ON(ret < 0); + + PRINT_INFO("Reconnected to %s:%s", server_address, server_port_str); + } + } + } + } + + close(fd_epoll); + return 0; +} + +static void* transmit_driver_thread_func(void* param) +{ + struct epoll_event events[2] = {}; + bool exit_thread = false; + int fd_epoll; + int ret; + + (void) param; + + TRACE_DRIVER("Transmitter thread start"); + + fd_epoll = epoll_create1(EPOLL_CLOEXEC); + FATAL_SYSCALL_ON(fd_epoll < 0); + + events[0].events = EPOLLIN; + events[0].data.fd = fd_core; + ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_core, &events[0]); + FATAL_SYSCALL_ON(ret < 0); + + events[1].events = EPOLLIN; + events[1].data.fd = fd_stop_drv; + ret = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, fd_stop_drv, &events[1]); + FATAL_SYSCALL_ON(ret < 0); + + while (!exit_thread) { + int event_count; + + do { + event_count = epoll_wait(fd_epoll, events, 2, -1); + if (event_count == -1 && errno == EINTR) { + continue; + } + FATAL_SYSCALL_ON(event_count == -1); + break; + } while (1); + + FATAL_ON(event_count == 0); + + for (size_t event_i = 0; event_i != (size_t)event_count; event_i++) { + int current_event_fd = events[event_i].data.fd; + + if (current_event_fd == fd_stop_drv) { + exit_thread = true; + continue; + } + + if (current_event_fd == fd_core) { + uint8_t buffer[TCP_BUFFER_SIZE]; + ssize_t read_retval = read(fd_core, buffer, sizeof(buffer)); + FATAL_SYSCALL_ON(read_retval < 0); + + pthread_mutex_lock(&tcp_lock); + if (tcp_connected) { + ssize_t write_retval = write(fd_tcp, buffer, (size_t)read_retval); + if (write_retval != read_retval) { + // Treat any short/failed write as a transport drop; the rx thread + // owns reconnection and will pick it up on its EOF/error. + WARN("TCP write failed (%zd/%zd): %s", write_retval, read_retval, strerror(errno)); + tcp_connected = false; + } + } else { + // Mid-reconnect: drop the frame. CPC retransmits unacked frames. + TRACE_DRIVER("Dropping core frame: TCP link down"); + } + pthread_mutex_unlock(&tcp_lock); + + // Notify the core that the bytes have been handed to the kernel. + // Unlike UART there is no baud-rate drain time to model. + struct timespec tx_complete_timestamp; + clock_gettime(CLOCK_MONOTONIC, &tx_complete_timestamp); + ssize_t notify_retval = write(fd_core_notify, &tx_complete_timestamp, sizeof(tx_complete_timestamp)); + FATAL_SYSCALL_ON(notify_retval != sizeof(tx_complete_timestamp)); + } + } + } + + close(fd_epoll); + return 0; +} + +static void driver_tcp_process_tcp(bool *disconnected) +{ + const size_t available_space = sizeof(rx_buffer) - rx_buffer_head - 1; + + // Guard against a 0-length read (would look like EOF): only read when there + // is room. If the buffer is momentarily full, fall through and keep parsing. + if (available_space > 0) { + uint8_t temp_buffer[TCP_BUFFER_SIZE]; + ssize_t read_retval; + + do { + read_retval = read(fd_tcp, temp_buffer, available_space); + } while (read_retval == -1 && errno == EINTR); + + if (read_retval == 0) { + *disconnected = true; // peer closed the connection + return; + } + if (read_retval < 0) { + *disconnected = true; // ECONNRESET, ETIMEDOUT, … — transport drop + return; + } + + memcpy(&rx_buffer[rx_buffer_head], temp_buffer, (size_t)read_retval); + rx_buffer_head += (size_t)read_retval; + } + + while (1) { + switch (rx_state) { + case EXPECTING_HEADER: + if (header_synch(rx_buffer, &rx_buffer_head)) { + rx_state = EXPECTING_PAYLOAD; + } else { + return; + } + break; + + case EXPECTING_PAYLOAD: + if (delimit_and_push_frames_to_core(rx_buffer, &rx_buffer_head)) { + rx_state = EXPECTING_HEADER; + } else { + return; + } + break; + + default: + BUG("Illegal switch, Case : %d", rx_state); + return; + } + } +} + +// ------------------------------------------------------------------------- +// HDLC frame delimiting — duplicated from driver_uart.c (transport-agnostic). +// Kept local to the TCP driver so the working UART path is untouched; a later +// productization step can lift these into a shared driver_frame.c. +// ------------------------------------------------------------------------- + +static bool validate_header(uint8_t *header_start) +{ + uint16_t hcs; + uint16_t payload_size; + + if (header_start[SLI_CPC_HDLC_FLAG_POS] != SLI_CPC_HDLC_FLAG_VAL) { + return false; + } + + hcs = hdlc_get_hcs(header_start); + + if (!sli_cpc_validate_crc_sw(header_start, SLI_CPC_HDLC_HEADER_SIZE, hcs)) { + TRACE_DRIVER_INVALID_HEADER_CHECKSUM(); + return false; + } + + payload_size = hdlc_get_length(header_start); + if (payload_size > TCP_BUFFER_SIZE) { + TRACE_DRIVER("RX buffer size from bus is invalid: %d", payload_size); + return false; + } + + return true; +} + +static bool header_synch(uint8_t *buffer, size_t *buffer_head) +{ + if (*buffer_head < SLI_CPC_HDLC_HEADER_RAW_SIZE) { + return false; + } + + const size_t num_header_combination = *buffer_head - SLI_CPC_HDLC_HEADER_RAW_SIZE + 1; + size_t i; + + for (i = 0; i != num_header_combination; i++) { + if (validate_header(&buffer[i])) { + if (i != 0) { + memmove(&buffer[0], &buffer[i], *buffer_head - i); + *buffer_head -= i; + } + return true; + } + } + + memmove(&buffer[0], &buffer[num_header_combination], SLI_CPC_HDLC_HEADER_RAW_SIZE - 1); + *buffer_head = SLI_CPC_HDLC_HEADER_RAW_SIZE - 1; + + return false; +} + +static bool delimit_and_push_frames_to_core(uint8_t *buffer, size_t *buffer_head) +{ + uint16_t payload_len; + size_t frame_size; + + if (*buffer_head < SLI_CPC_HDLC_HEADER_RAW_SIZE) { + return false; + } + + payload_len = hdlc_get_length(buffer); + frame_size = payload_len + SLI_CPC_HDLC_HEADER_RAW_SIZE; + + if (frame_size > *buffer_head) { + return false; + } + + { + TRACE_FRAME("Driver : Frame delimiter : push delimited frame to core : ", buffer, frame_size); + + ssize_t write_retval = write(fd_core, buffer, frame_size); + FATAL_SYSCALL_ON(write_retval < 0); + FATAL_ON((size_t)write_retval != frame_size); + } + + { + const size_t remaining_bytes = *buffer_head - frame_size; + memmove(buffer, &buffer[frame_size], remaining_bytes); + *buffer_head = remaining_bytes; + } + + return true; +} diff --git a/driver/driver_tcp.h b/driver/driver_tcp.h new file mode 100644 index 0000000..49c8e67 --- /dev/null +++ b/driver/driver_tcp.h @@ -0,0 +1,37 @@ +/***************************************************************************//** + * @file + * @brief Co-Processor Communication Protocol (CPC) - TCP driver + ******************************************************************************* + * # License + * Copyright 2022 Silicon Laboratories Inc. www.silabs.com + ******************************************************************************* + * + * The licensor of this software is Silicon Laboratories Inc. Your use of this + * software is governed by the terms of Silicon Labs Master Software License + * Agreement (MSLA) available at + * www.silabs.com/about-us/legal/master-software-license-agreement. This + * software is distributed to you in Source Code format and is governed by the + * sections of the MSLA applicable to Source Code. + * + ******************************************************************************/ + +#ifndef DRIVER_TCP_H +#define DRIVER_TCP_H + +/* + * Initialize the TCP driver. Crashes the app if the initial connection fails. + * + * This driver is meant to talk to the EFR32 RCP over a transparent UART<->TCP + * bridge (e.g. the in-kernel rtl8196e-uart-bridge on the Lidl gateway). It owns + * its own reconnection: if the TCP link drops mid-session it reconnects under + * cpcd without the daemon exiting, removing the need for an external socat PTY + * shim. + * + * Returns, through the out-parameters, the file descriptors of the paired + * sockets to the driver for the core to use in its select()/epoll() calls. + */ +void driver_tcp_init(int *fd_to_core, int *fd_notify_core, const char *server_address, unsigned int server_port); + +void driver_tcp_kill(void); + +#endif // DRIVER_TCP_H diff --git a/include/cpcd/config.h b/include/cpcd/config.h index 3b4a1e1..9e965a4 100644 --- a/include/cpcd/config.h +++ b/include/cpcd/config.h @@ -24,6 +24,7 @@ typedef enum { UART, SPI, + TCP, UNCHOSEN } bus_t; @@ -71,6 +72,9 @@ typedef struct __attribute__((packed)) { const char *spi_irq_chip; unsigned int spi_irq_pin; + const char *tcp_server_address; + unsigned int tcp_server_port; + const char *fwu_reset_chip; int fwu_spi_reset_pin; const char *fwu_wake_chip; diff --git a/misc/config.c b/misc/config.c index 493c3d2..9e0c0ea 100644 --- a/misc/config.c +++ b/misc/config.c @@ -93,6 +93,10 @@ config_t config = { .spi_irq_chip = NULL, .spi_irq_pin = 0, + // TCP config + .tcp_server_address = NULL, + .tcp_server_port = 0, + // Firmware update .fwu_reset_chip = NULL, .fwu_spi_reset_pin = -1, @@ -160,6 +164,8 @@ static const char* config_bus_to_str(bus_t value) return "UART"; case SPI: return "SPI"; + case TCP: + return "TCP"; case UNCHOSEN: return "UNCHOSEN"; default: @@ -279,6 +285,9 @@ static void config_print(void) CONFIG_PRINT_STR_COND(config.spi_irq_chip, config.bus == SPI); CONFIG_PRINT_DEC_COND(config.spi_irq_pin, config.bus == SPI); + CONFIG_PRINT_STR_COND(config.tcp_server_address, config.bus == TCP); + CONFIG_PRINT_DEC_COND(config.tcp_server_port, config.bus == TCP); + CONFIG_PRINT_BOOL_TO_STR(config.fwu_recovery_pins_enabled); CONFIG_PRINT_STR_COND(config.fwu_reset_chip, config.fwu_recovery_pins_enabled); CONFIG_PRINT_DEC_COND(config.fwu_spi_reset_pin, config.fwu_recovery_pins_enabled); @@ -727,6 +736,8 @@ static void config_parse_config_file(void) config.bus = UART; } else if (0 == strcmp(val, "SPI")) { config.bus = SPI; + } else if (0 == strcmp(val, "TCP")) { + config.bus = TCP; } else { FATAL("Config file error : bad bus_type value\n"); } @@ -745,6 +756,14 @@ static void config_parse_config_file(void) if (*endptr != '\0') { FATAL("Bad config line \"%s\"", line); } + } else if (0 == strcmp(name, "tcp_server_address")) { + config.tcp_server_address = strdup(val); + FATAL_ON(config.tcp_server_address == NULL); + } else if (0 == strcmp(name, "tcp_server_port")) { + config.tcp_server_port = (unsigned int)strtoul(val, &endptr, 10); + if (*endptr != '\0') { + FATAL("Bad config line \"%s\"", line); + } } else if (0 == strcmp(name, "bootloader_recovery_pins_enabled")) { if (0 == strcmp(val, "true")) { config.fwu_recovery_pins_enabled = true; @@ -952,6 +971,13 @@ static void config_validate_configuration(void) } prevent_device_collision(config.uart_file); + } else if (config.bus == TCP) { + if (config.tcp_server_address == NULL) { + FATAL("tcp_server_address is required"); + } + if (config.tcp_server_port == 0) { + FATAL("tcp_server_port is required"); + } } else { FATAL("Invalid bus configuration."); } diff --git a/modes/normal.c b/modes/normal.c index 56d89f7..64a162f 100644 --- a/modes/normal.c +++ b/modes/normal.c @@ -28,6 +28,7 @@ #include "driver/driver_uart.h" #include "driver/driver_spi.h" +#include "driver/driver_tcp.h" #include "driver/driver_ezsp.h" void run_normal_mode(void) @@ -50,6 +51,11 @@ void run_normal_mode(void) config.spi_bitrate, config.spi_irq_chip, config.spi_irq_pin); + } else if (config.bus == TCP) { + driver_tcp_init(&fd_socket_driver_core, + &fd_socket_driver_core_notify, + config.tcp_server_address, + config.tcp_server_port); } else { BUG(); } @@ -92,6 +98,10 @@ bool is_bootloader_running(void) config.spi_bitrate, config.spi_irq_chip, config.spi_irq_pin); + } else if (config.bus == TCP) { + // Bootloader probing / firmware update over the TCP bridge is out of scope; + // assume the secondary is running its application. + secondary_running_bootloader = false; } else { BUG(); }