/* * SPDX-License-Identifier: Apache-2.0 * ZephCore - Repeater (USB CLI, Event-Driven) * * This is the main entry point for the repeater role. * Repeaters use USB serial CLI for configuration (no BLE). */ #include #include #include #include #include LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); #include #include #include #include #include #include #include #include "oled_power.h" /* BLE controller assert handler — BT is compiled even for repeater (via zephcore_common.conf) */ #if IS_ENABLED(CONFIG_BT_CTLR_ASSERT_HANDLER) extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) { LOG_ERR("!!! BLE CONTROLLER ASSERT: %s:%u !!!", file ? file : "?", line); k_sleep(K_MSEC(100)); sys_reboot(SYS_REBOOT_COLD); } #endif /* USB CDC with 1200 baud touch detection (when not using auto-init) */ #if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) #include #endif #include #include #include #include /* UI subsystem (display, buttons, buzzer) */ #include "ui_task.h" /* Radio + mesh includes (shared header selects LR1110 or SX126x) */ #include #if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA) #include "wifi_ota.h" #endif /* LED configuration */ #if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) #define LED0_NODE DT_ALIAS(led0) static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(LED0_NODE, gpios); #endif #if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) #define LED1_NODE DT_ALIAS(led1) static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); #endif /* USB CLI configuration */ #define USB_RING_BUF_SIZE 512 #define CLI_LINE_BUF_SIZE 256 /* * Event-driven mesh loop - replaces 50ms polling with true event signaling. * Events are signaled from ISR/callbacks, mesh loop wakes immediately. */ #define MESH_EVENT_LORA_RX BIT(0) /* LoRa packet received */ #define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete */ #define MESH_EVENT_CLI_RX BIT(2) /* CLI command received */ #define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */ #define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change (must run on main thread!) */ #define MESH_EVENT_TX_DRAIN BIT(5) /* Outbound packet delay expired, run checkSend */ #define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN) /* Housekeeping interval - infrequent to preserve power savings */ #define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS /* Event object for mesh loop */ static struct k_event mesh_events; /* USB CDC state */ static const struct device *usb_dev; static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE]; static struct ring_buf usb_ring_buf; static char cli_line_buf[CLI_LINE_BUF_SIZE]; static char cli_reply_buf[256]; static uint16_t cli_line_idx; /* Work items for event-driven processing */ static void cli_rx_work_fn(struct k_work *work); static void housekeeping_timer_fn(struct k_timer *timer); static void tx_drain_work_fn(struct k_work *work); static void initial_advert_work_fn(struct k_work *work); K_WORK_DEFINE(cli_rx_work, cli_rx_work_fn); K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn); K_WORK_DELAYABLE_DEFINE(initial_advert_work, initial_advert_work_fn); /* Housekeeping timer for periodic tasks (noise floor calibration, etc.) */ K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL); /* Forward declarations */ #ifdef ZEPHCORE_LORA static RepeaterMesh *repeater_mesh_ptr; #endif /* Print string to USB serial */ static void cli_print(const char *str) { if (!usb_dev) return; while (*str) { uart_poll_out(usb_dev, *str++); } } /* USB CDC UART interrupt callback */ static void cli_uart_isr(const struct device *dev, void *user_data) { ARG_UNUSED(user_data); while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { if (uart_irq_rx_ready(dev)) { uint8_t buf[64]; int recv_len = uart_fifo_read(dev, buf, sizeof(buf)); if (recv_len > 0) { ring_buf_put(&usb_ring_buf, buf, recv_len); k_work_submit(&cli_rx_work); } } } } /* CLI RX work - processes line-based CLI commands * Matches Arduino behavior: echo each char, then " -> reply" on enter */ static void cli_rx_work_fn(struct k_work *work) { ARG_UNUSED(work); uint8_t byte; while (ring_buf_get(&usb_ring_buf, &byte, 1) == 1) { /* Process command on \r OR \n (support echo from Linux) */ if (byte == '\r' || byte == '\n') { if (cli_line_idx > 0) { cli_line_buf[cli_line_idx] = '\0'; /* Debug: log received command */ LOG_INF("CLI cmd len=%d: %.40s%s", cli_line_idx, cli_line_buf, cli_line_idx > 40 ? "..." : ""); /* Process CLI command */ #ifdef ZEPHCORE_LORA if (repeater_mesh_ptr) { cli_reply_buf[0] = '\0'; repeater_mesh_ptr->handleCommand(0, cli_line_buf, cli_reply_buf); if (cli_reply_buf[0] != '\0') { /* Arduino format: newline, then " -> reply" */ cli_print("\r\n -> "); cli_print(cli_reply_buf); } } #endif cli_line_idx = 0; } /* New line for next command */ cli_print("\r\n"); } else if (byte == 0x7F || byte == 0x08) { /* Backspace - echo backspace sequence */ if (cli_line_idx > 0) { cli_line_idx--; if (usb_dev) { uart_poll_out(usb_dev, '\b'); uart_poll_out(usb_dev, ' '); uart_poll_out(usb_dev, '\b'); } } } else if (cli_line_idx < sizeof(cli_line_buf) - 1) { /* Echo character back (like Arduino) */ if (usb_dev) { uart_poll_out(usb_dev, byte); } cli_line_buf[cli_line_idx++] = (char)byte; } } } /* Housekeeping timer callback - signals event to wake mesh loop periodically */ static void housekeeping_timer_fn(struct k_timer *timer) { ARG_UNUSED(timer); k_event_post(&mesh_events, MESH_EVENT_HOUSEKEEPING); } #ifdef ZEPHCORE_LORA /* LoRa RX callback - called from ISR context when packet received */ static void lora_rx_callback(void *user_data) { ARG_UNUSED(user_data); k_event_post(&mesh_events, MESH_EVENT_LORA_RX); } /* LoRa TX complete callback */ static void lora_tx_done_callback(void *user_data) { ARG_UNUSED(user_data); k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE); } /* TX drain — Dispatcher queued a packet with a delay. * Schedule a precise wake so checkSend runs when the delay expires. */ static void tx_drain_work_fn(struct k_work *work) { ARG_UNUSED(work); k_event_post(&mesh_events, MESH_EVENT_TX_DRAIN); } /* Deferred initial advertisement — gives GPS time to get a fix at boot */ static void initial_advert_work_fn(struct k_work *work) { ARG_UNUSED(work); #ifdef ZEPHCORE_LORA if (repeater_mesh_ptr) { LOG_INF("Sending deferred initial advertisement"); repeater_mesh_ptr->sendSelfAdvertisement(500, false); } #endif } static void tx_queued_callback(uint32_t delay_ms, void *user_data) { ARG_UNUSED(user_data); k_work_reschedule(&tx_drain_work, K_MSEC(delay_ms)); } #endif /* Global instances */ static mesh::ZephyrRTCClock rtc_clock; /* GPS event callback - called when GPS work handlers need the main thread * to process a state transition (wake from standby, fix done, timeout). * Runs from system work queue context — just posts an event, no blocking. */ static void gps_event_callback(void) { k_event_post(&mesh_events, MESH_EVENT_GPS_ACTION); } static RepeaterDataStore data_store; /* GPS fix callback - syncs RTC from GPS time and updates node position */ static void gps_fix_callback(double lat, double lon, int64_t utc_time) { if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); } #ifdef ZEPHCORE_LORA /* Update node position for mesh advertising */ NodePrefs *prefs = repeater_mesh_ptr ? repeater_mesh_ptr->getNodePrefs() : nullptr; if (prefs && (lat != prefs->node_lat || lon != prefs->node_lon)) { prefs->node_lat = lat; prefs->node_lon = lon; int lat_deg = (int)lat; int lon_deg = (int)lon; int lat_frac = (int)((lat - lat_deg) * 1000000); int lon_frac = (int)((lon - lon_deg) * 1000000); if (lat_frac < 0) lat_frac = -lat_frac; if (lon_frac < 0) lon_frac = -lon_frac; LOG_INF("GPS fix: position updated lat=%d.%06d lon=%d.%06d", lat_deg, lat_frac, lon_deg, lon_frac); data_store.savePrefs(*prefs); } #endif } #ifdef ZEPHCORE_LORA static mesh::ZephyrBoard zephyr_board; /* Radio prefs — initialized with defaults in main(), updated after mesh.begin() * loads persisted prefs. Passed to radio adapter at static construction time. */ static NodePrefs radio_prefs; #if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110) /* LR1110 via Zephyr LoRa driver */ static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &radio_prefs); #else /* SX126x via Zephyr LoRa driver */ static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0)); static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &radio_prefs); #endif static mesh::ZephyrMillisecondClock ms_clock; static mesh::ZephyrRNG zephyr_rng; static mesh::SimpleMeshTables mesh_tables; /* RepeaterMesh requires: board, radio, ms_clock, rng, rtc, tables */ static RepeaterMesh repeater_mesh(zephyr_board, lora_radio, ms_clock, zephyr_rng, rtc_clock, mesh_tables); #endif /* Repeater event loop */ static void repeater_event_loop(void) { LOG_INF("starting event-driven loop"); /* Print startup banner (no prompt - Arduino style) */ cli_print("\r\n=== ZephCore Repeater ===\r\n"); /* Start housekeeping timer for periodic maintenance tasks */ k_timer_start(&housekeeping_timer, K_MSEC(HOUSEKEEPING_INTERVAL_MS), K_MSEC(HOUSEKEEPING_INTERVAL_MS)); for (;;) { /* Wait for any mesh event - blocks until signaled */ uint32_t events = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER); k_event_clear(&mesh_events, events); /* GPS state transitions must run on main thread (GNSS driver * modem_chat blocks on system work queue semaphore). */ if (events & MESH_EVENT_GPS_ACTION) { gps_process_event(); } #ifdef ZEPHCORE_LORA /* Packet processing — only on radio/CLI/TX events */ if (repeater_mesh_ptr && (events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_TX_DRAIN))) { repeater_mesh_ptr->loop(); } #endif /* Periodic housekeeping — maintenance + display refresh */ if (events & MESH_EVENT_HOUSEKEEPING) { #ifdef ZEPHCORE_LORA /* Radio maintenance: noise floor calibration, AGC reset, * RX watchdog. Separated from loop() so these never run * on packet-driven events. */ if (repeater_mesh_ptr) { repeater_mesh_ptr->maintenanceLoop(); } #endif ui_set_clock(rtc_clock.getCurrentTime()); #ifdef ZEPHCORE_LORA /* Refresh radio params (noise floor changes from calibration) */ if (repeater_mesh_ptr) { NodePrefs *p = repeater_mesh_ptr->getNodePrefs(); ui_set_radio_params( (uint32_t)(p->freq * 1000000.0f + 0.5f), p->sf, (uint16_t)(p->bw * 10.0f + 0.5f), p->cr, p->tx_power_dbm, lora_radio.getNoiseFloor()); } /* Battery every ~60s (12 × 5s housekeeping) */ static uint8_t batt_counter; if (++batt_counter >= 12) { batt_counter = 0; ui_set_battery(zephyr_board.getBattMilliVolts(), 0); } #endif ui_refresh_display(); } } } int main(void) { /* Initialize radio prefs with safe defaults before anything else */ initNodePrefs(&radio_prefs); strcpy(radio_prefs.node_name, "Repeater"); #ifdef ZEPHCORE_LORA /* Clear any stale bootloader magic from previous sessions. * Prevents nRF52 boards from re-entering bootloader after reboot. */ zephyr_board.clearBootloaderMagic(); #endif /* Wait for USB CDC to enumerate before any logging */ k_sleep(K_MSEC(2000)); LOG_INF("=== ZephCore Repeater starting ==="); #if IS_ENABLED(CONFIG_ZEPHCORE_WIFI_OTA) /* Confirm MCUboot image early — if we just booted after OTA, * this marks the image as good so MCUboot keeps it. */ wifi_ota_confirm_image(); #endif /* Configure LEDs */ #if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) if (gpio_is_ready_dt(&led0)) { gpio_pin_configure_dt(&led0, GPIO_OUTPUT_INACTIVE); } #endif #if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios) if (gpio_is_ready_dt(&led1)) { gpio_pin_configure_dt(&led1, GPIO_OUTPUT_INACTIVE); } #endif /* Initialize repeater data store */ if (!data_store.begin()) { LOG_ERR("RepeaterDataStore init failed"); } /* Initialize sensor manager */ sensor_manager_init(); /* Set GPS to repeater mode: power off now, wake every 48h for time sync only. * This prevents GPS from draining power on boards that have it (e.g., Wio Tracker). */ if (gps_is_available()) { gps_set_fix_callback(gps_fix_callback); gps_set_event_callback(gps_event_callback); gps_set_repeater_mode(true); } /* Initialize UI (display + buttons). Shows splash screen, then auto- * transitions to STATUS page. Display sleeps after auto-off timeout; * user button is the only wake source for repeater. */ ui_init(); #if !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY) oled_sleep(); #endif /* Log environment sensor availability */ if (env_sensors_available()) { LOG_INF("Environment sensors available"); } #ifdef ZEPHCORE_LORA repeater_mesh_ptr = &repeater_mesh; /* Initialize mesh event object BEFORE begin() — radio callbacks post events */ k_event_init(&mesh_events); /* Set LoRa callbacks for event-driven packet processing */ lora_radio.setRxCallback(lora_rx_callback, nullptr); lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr); repeater_mesh.setTxQueuedCallback(tx_queued_callback, nullptr); /* Load or generate identity BEFORE begin() */ mesh::LocalIdentity self_identity; if (!data_store.loadIdentity(self_identity)) { LOG_INF("No identity found, generating new keypair..."); self_identity = mesh::LocalIdentity(&zephyr_rng); /* Ensure pub_key[0] is not reserved (0x00 or 0xFF) */ int count = 0; while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) { self_identity = mesh::LocalIdentity(&zephyr_rng); count++; } data_store.saveIdentity(self_identity); LOG_INF("New identity saved"); } repeater_mesh.self_id = self_identity; /* Log repeater ID (first 8 bytes of public key) */ LOG_INF("Repeater ID: %02x%02x%02x%02x%02x%02x%02x%02x...", self_identity.pub_key[0], self_identity.pub_key[1], self_identity.pub_key[2], self_identity.pub_key[3], self_identity.pub_key[4], self_identity.pub_key[5], self_identity.pub_key[6], self_identity.pub_key[7]); /* Start mesh with data store - this loads prefs, ACL, regions */ repeater_mesh.begin(&data_store); /* Generate default node name from hardware device ID if not set */ NodePrefs* prefs = repeater_mesh.getNodePrefs(); if (strlen(prefs->node_name) == 0 || strcmp(prefs->node_name, "Repeater") == 0) { uint8_t dev_id[8]; ssize_t id_len = hwinfo_get_device_id(dev_id, sizeof(dev_id)); if (id_len >= 4) { snprintf(prefs->node_name, sizeof(prefs->node_name), "Repeater-%02X%02X%02X%02X", dev_id[0], dev_id[1], dev_id[2], dev_id[3]); } } /* Apply RX boost and duty cycle from prefs */ lora_radio.setRxBoost(prefs->rx_boost != 0); lora_radio.enableRxDutyCycle(prefs->rx_duty_cycle != 0); /* Feed initial UI state from loaded prefs */ ui_set_node_name(prefs->node_name); ui_set_radio_params( (uint32_t)(prefs->freq * 1000000.0f + 0.5f), /* MHz → Hz */ prefs->sf, (uint16_t)(prefs->bw * 10.0f + 0.5f), /* kHz → 0.1 kHz */ prefs->cr, prefs->tx_power_dbm, lora_radio.getNoiseFloor()); ui_set_battery(zephyr_board.getBattMilliVolts(), 0); ui_set_gps_available(gps_is_available()); /* Defer initial advertisement by 10s — gives GPS time for a quick fix. * Advert payload (including coords) is built when the work fires. */ LOG_INF("Initial advertisement scheduled in 10s"); k_work_schedule(&initial_advert_work, K_SECONDS(10)); #endif /* Initialize USB serial for CLI */ #if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT) && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) zephcore_usbd_init(); #endif #if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); #else /* No CDC ACM (e.g. ESP32 usb_serial) — use chosen console UART */ usb_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console)); #endif if (device_is_ready(usb_dev)) { LOG_INF("USB CDC device ready: %s", usb_dev->name); ring_buf_init(&usb_ring_buf, sizeof(usb_ring_buf_data), usb_ring_buf_data); /* Set up UART interrupt callback */ uart_irq_callback_set(usb_dev, cli_uart_isr); uart_irq_rx_enable(usb_dev); } else { LOG_ERR("USB CDC device not ready"); usb_dev = NULL; } /* * Event-driven architecture: main thread runs repeater event loop. * No BLE - all configuration via USB serial CLI. */ #ifdef ZEPHCORE_LORA repeater_event_loop(); /* Never returns */ #else for (;;) { k_sleep(K_FOREVER); } #endif return 0; }