feat(wake): add controller-free USB serial wake beacon
Add a standalone Pico2W image reusing the configured nonconnectable wake burst without Bluepad32, controller-host profiles, Classic/LE-central roles, connection pools, pairing or writable TLV storage. Explicitly disable and verify Classic inquiry/page scan before declaring readiness. Keep the original controller firmware and captured wake identity unchanged. Expose bounded SPWB1 CDC commands with exactly-once request IDs, explicit failures and radio progress independent of serial backpressure. Add the cross-platform switch-pico-wake script with strict preflight, bounded I/O, no automatic rebroadcast, status-only mode and preserved uncertain-outcome metadata. Standard USB serial requires no WinUSB/Zadig binding. Add isolated --wake-only build/publication selection and document setup, backup and physical-BOOTSEL recovery. Validate776 tests, focused framing and lifecycle cases, ELF isolation and actual CDC operation on only the new board. One burst completed in2.095s; malformed/status/reconnect input and same-ID replay caused no additional broadcast. Original Pico untouched. Private firmware images, console configuration and backups are excluded.
This commit is contained in:
parent
e277d34c20
commit
bed71f77f1
14 changed files with 2042 additions and 3 deletions
68
tools/switch2_wake_beacon/CMakeLists.txt
Normal file
68
tools/switch2_wake_beacon/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
set(PICO_BOARD pico2_w CACHE STRING "Target board")
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../../pico_sdk_import.cmake)
|
||||
project(switch2_wake_beacon C CXX ASM)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(FIRMWARE_DIR ${CMAKE_CURRENT_LIST_DIR}/../../src/firmware)
|
||||
if(NOT EXISTS "${FIRMWARE_DIR}/platform/pico/switch2_wake_config.h")
|
||||
message(FATAL_ERROR
|
||||
"Wake-only firmware requires the existing private switch2_wake_config.h. Capture/configure it first; no generic wake packet is provided.")
|
||||
endif()
|
||||
|
||||
pico_sdk_init()
|
||||
if(NOT PICO_CYW43_SUPPORTED)
|
||||
message(FATAL_ERROR "Wake-only firmware requires a wireless Pico board (default: pico2_w).")
|
||||
endif()
|
||||
|
||||
add_executable(switch2-wake-beacon
|
||||
main.cpp
|
||||
beacon_protocol.cpp
|
||||
${FIRMWARE_DIR}/input/switch2_wake.cpp
|
||||
)
|
||||
target_include_directories(switch2-wake-beacon PRIVATE
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${FIRMWARE_DIR}
|
||||
)
|
||||
target_compile_definitions(switch2-wake-beacon PRIVATE
|
||||
SWITCH2_WAKE_CONFIGURED=1
|
||||
ENABLE_BLE=1
|
||||
CYW43_LWIP=0
|
||||
PICO_CYW43_LOGGING_ENABLED=0
|
||||
PICO_BTSTACK_CYW43_MAX_HCI_PROCESS_LOOP_COUNT=4
|
||||
USBD_VID=0xCAFE
|
||||
USBD_PID=0x4030
|
||||
USBD_MANUFACTURER="switch-pico"
|
||||
USBD_PRODUCT="switch-pico wake beacon"
|
||||
USBD_DESC_STR_MAX=32
|
||||
PICO_STDIO_USB_ENABLE_RESET_VIA_BAUD_RATE=0
|
||||
PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE=0
|
||||
PICO_STDIO_USB_RESET_INTERFACE_SUPPORT_MS_OS_20_DESCRIPTOR=0
|
||||
PICO_STDIO_USB_ENABLE_IRQ_BACKGROUND_TASK=0
|
||||
PICO_STDIO_USB_SUPPORT_CHARS_AVAILABLE_CALLBACK=0
|
||||
PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=0
|
||||
PICO_STDIO_USB_STDOUT_TIMEOUT_US=0
|
||||
PICO_STDIO_USB_DEFAULT_CRLF=0
|
||||
CFG_TUSB_OS=OPT_OS_PICO
|
||||
CFG_TUD_TASK_QUEUE_SZ=16
|
||||
)
|
||||
# Do not link pico_btstack_cyw43: its startup initializes writable flash TLV.
|
||||
# HCI-only BLE needs neither pico_btstack_ble's SM/GATT/HID profiles nor Classic.
|
||||
# The exported low-level SDK driver and transport use our Core-0 polled context.
|
||||
target_link_libraries(switch2-wake-beacon PRIVATE
|
||||
pico_stdlib
|
||||
pico_async_context_poll
|
||||
pico_cyw43_driver
|
||||
cyw43_driver_picow
|
||||
pico_btstack_base
|
||||
pico_btstack_hci_transport_cyw43
|
||||
pico_btstack_run_loop_async_context
|
||||
)
|
||||
pico_enable_stdio_usb(switch2-wake-beacon 1)
|
||||
pico_enable_stdio_uart(switch2-wake-beacon 0)
|
||||
pico_enable_stdio_rtt(switch2-wake-beacon 0)
|
||||
pico_set_program_name(switch2-wake-beacon "switch-pico wake beacon")
|
||||
pico_set_program_version(switch2-wake-beacon "1.0.0")
|
||||
pico_add_extra_outputs(switch2-wake-beacon)
|
||||
193
tools/switch2_wake_beacon/beacon_protocol.cpp
Normal file
193
tools/switch2_wake_beacon/beacon_protocol.cpp
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
#include "beacon_protocol.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace wake_beacon {
|
||||
|
||||
void Protocol::connected(bool connected) {
|
||||
if (!connected) {
|
||||
// An interrupted line must never be completed by a later USB session.
|
||||
line_size_ = 0;
|
||||
invalid_line_ = false;
|
||||
response_size_ = 0;
|
||||
response_offset_ = 0;
|
||||
}
|
||||
connected_ = connected;
|
||||
}
|
||||
|
||||
bool Protocol::can_receive() const {
|
||||
return connected_ && output_size() == 0;
|
||||
}
|
||||
|
||||
bool Protocol::receive(uint8_t byte) {
|
||||
if (!can_receive()) {
|
||||
return false;
|
||||
}
|
||||
if (byte == '\n') {
|
||||
if (invalid_line_) {
|
||||
respond("malformed");
|
||||
} else {
|
||||
command();
|
||||
}
|
||||
line_size_ = 0;
|
||||
invalid_line_ = false;
|
||||
} else if (line_size_ == kMaxLineBytes ||
|
||||
(byte != '\r' && (byte < 0x20 || byte > 0x7e))) {
|
||||
// Discard through LF, never parse an overflowing suffix as a command.
|
||||
invalid_line_ = true;
|
||||
} else if (!invalid_line_) {
|
||||
line_[line_size_++] = static_cast<char>(byte);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* Protocol::output_data() const {
|
||||
return response_ + response_offset_;
|
||||
}
|
||||
|
||||
size_t Protocol::output_size() const {
|
||||
return response_size_ - response_offset_;
|
||||
}
|
||||
|
||||
void Protocol::consume_output(size_t count) {
|
||||
if (count > output_size()) {
|
||||
count = output_size();
|
||||
}
|
||||
response_offset_ += count;
|
||||
}
|
||||
|
||||
bool Protocol::active() const {
|
||||
return state_ == State::Queued || state_ == State::Broadcasting;
|
||||
}
|
||||
|
||||
bool Protocol::busy() const {
|
||||
return active() || (!radio_.failed && radio_.wake.busy);
|
||||
}
|
||||
|
||||
const char* Protocol::state_name() const {
|
||||
switch (state_) {
|
||||
case State::Idle: return "idle";
|
||||
case State::Queued: return "queued";
|
||||
case State::Broadcasting: return "broadcasting";
|
||||
case State::Complete: return "complete";
|
||||
case State::Unconfigured: return "unconfigured";
|
||||
case State::Failed: return "failed";
|
||||
}
|
||||
return "failed";
|
||||
}
|
||||
|
||||
void Protocol::observe(const RadioStatus& radio) {
|
||||
if (radio.failed && !radio_.failed) {
|
||||
++local_failures_;
|
||||
}
|
||||
radio_ = radio;
|
||||
if (active()) {
|
||||
if (radio_.failed) {
|
||||
state_ = State::Failed;
|
||||
} else if (radio_.initialized && !radio_.wake.configured) {
|
||||
state_ = State::Unconfigured;
|
||||
} else if (state_ == State::Broadcasting) {
|
||||
// A failed stop may still complete during cleanup. Failure wins.
|
||||
if (radio_.wake.failures != start_failures_) {
|
||||
state_ = State::Failed;
|
||||
} else if (radio_.wake.completed_bursts != start_completed_ &&
|
||||
!radio_.wake.busy) {
|
||||
state_ = State::Complete;
|
||||
}
|
||||
}
|
||||
} else if (request_id_ == 0) {
|
||||
if (radio_.failed) {
|
||||
state_ = State::Failed;
|
||||
} else if (radio_.initialized && !radio_.wake.configured) {
|
||||
state_ = State::Unconfigured;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Protocol::dispatch_pending() const {
|
||||
return state_ == State::Queued && radio_.ready && !radio_.failed &&
|
||||
radio_.wake.configured && !radio_.wake.busy;
|
||||
}
|
||||
|
||||
void Protocol::dispatched(bool accepted) {
|
||||
if (!dispatch_pending()) {
|
||||
return;
|
||||
}
|
||||
if (accepted) {
|
||||
start_completed_ = radio_.wake.completed_bursts;
|
||||
start_failures_ = radio_.wake.failures;
|
||||
state_ = State::Broadcasting;
|
||||
} else {
|
||||
++local_failures_;
|
||||
state_ = State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
void Protocol::command() {
|
||||
size_t length = line_size_;
|
||||
if (length != 0 && line_[length - 1] == '\r') {
|
||||
--length;
|
||||
}
|
||||
constexpr char status[] = "SPWB1 STATUS";
|
||||
constexpr char wake[] = "SPWB1 WAKE ";
|
||||
if (length == sizeof(status) - 1 &&
|
||||
memcmp(line_, status, sizeof(status) - 1) == 0) {
|
||||
respond("");
|
||||
return;
|
||||
}
|
||||
if (length <= sizeof(wake) - 1 ||
|
||||
memcmp(line_, wake, sizeof(wake) - 1) != 0) {
|
||||
respond("malformed");
|
||||
return;
|
||||
}
|
||||
uint32_t id = 0;
|
||||
constexpr uint32_t max_id = 0x7fffffff;
|
||||
for (size_t index = sizeof(wake) - 1; index < length; ++index) {
|
||||
const char digit = line_[index];
|
||||
if (digit < '0' || digit > '9' ||
|
||||
id > (max_id - static_cast<uint32_t>(digit - '0')) / 10) {
|
||||
respond("malformed");
|
||||
return;
|
||||
}
|
||||
id = id * 10 + static_cast<uint32_t>(digit - '0');
|
||||
}
|
||||
if (id == 0) {
|
||||
respond("malformed");
|
||||
} else if (id == request_id_) {
|
||||
// Retain idempotency across disconnects, failures and completion.
|
||||
respond("");
|
||||
} else if (busy()) {
|
||||
respond("busy");
|
||||
} else if (radio_.failed) {
|
||||
respond("radio_init_failed");
|
||||
} else {
|
||||
request_id_ = id;
|
||||
state_ = State::Queued;
|
||||
++accepted_requests_;
|
||||
respond("");
|
||||
}
|
||||
}
|
||||
|
||||
void Protocol::respond(const char* error) {
|
||||
const int length = snprintf(
|
||||
response_, sizeof(response_),
|
||||
"SPWB1 {\"protocol\":1,\"role\":\"wake-only\",\"firmware\":\"1.0.0\","
|
||||
"\"radio_ready\":%s,\"controller_hosting\":false,"
|
||||
"\"request_id\":%" PRIu32 ",\"state\":\"%s\",\"configured\":%s,"
|
||||
"\"busy\":%s,\"accepted_requests\":%" PRIu32 ","
|
||||
"\"completed_bursts\":%" PRIu32 ",\"failures\":%" PRIu32 ","
|
||||
"\"error\":\"%s\"}\n",
|
||||
radio_.ready ? "true" : "false", request_id_, state_name(),
|
||||
radio_.wake.configured ? "true" : "false", busy() ? "true" : "false",
|
||||
accepted_requests_, radio_.wake.completed_bursts,
|
||||
radio_.wake.failures + local_failures_, error);
|
||||
// All strings are fixed literals and even maximum counters fit in 512 bytes.
|
||||
response_size_ = length > 0 && static_cast<size_t>(length) < sizeof(response_)
|
||||
? static_cast<size_t>(length)
|
||||
: 0;
|
||||
response_offset_ = 0;
|
||||
}
|
||||
|
||||
} // namespace wake_beacon
|
||||
62
tools/switch2_wake_beacon/beacon_protocol.h
Normal file
62
tools/switch2_wake_beacon/beacon_protocol.h
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "input/switch2_wake.h"
|
||||
|
||||
namespace wake_beacon {
|
||||
|
||||
struct RadioStatus {
|
||||
bool ready = false;
|
||||
bool failed = false;
|
||||
bool initialized = false;
|
||||
Switch2WakeDiagnostics wake{};
|
||||
};
|
||||
|
||||
// Owned entirely by Core 0. Parsing only queues a request; the radio owner calls
|
||||
// dispatch_pending()/dispatched() separately, outside USB callbacks and IRQs.
|
||||
class Protocol {
|
||||
public:
|
||||
static constexpr size_t kMaxLineBytes = 64; // Excludes LF, includes optional CR.
|
||||
static constexpr size_t kResponseBytes = 512;
|
||||
|
||||
void connected(bool connected);
|
||||
bool can_receive() const;
|
||||
bool receive(uint8_t byte);
|
||||
const char* output_data() const;
|
||||
size_t output_size() const;
|
||||
void consume_output(size_t count);
|
||||
|
||||
void observe(const RadioStatus& radio);
|
||||
bool dispatch_pending() const;
|
||||
void dispatched(bool accepted);
|
||||
|
||||
private:
|
||||
enum class State : uint8_t {
|
||||
Idle, Queued, Broadcasting, Complete, Unconfigured, Failed,
|
||||
};
|
||||
|
||||
bool active() const;
|
||||
bool busy() const;
|
||||
const char* state_name() const;
|
||||
void command();
|
||||
void respond(const char* error);
|
||||
|
||||
RadioStatus radio_{};
|
||||
State state_ = State::Idle;
|
||||
uint32_t request_id_ = 0;
|
||||
uint32_t accepted_requests_ = 0;
|
||||
uint32_t local_failures_ = 0;
|
||||
uint32_t start_completed_ = 0;
|
||||
uint32_t start_failures_ = 0;
|
||||
bool connected_ = false;
|
||||
bool invalid_line_ = false;
|
||||
char line_[kMaxLineBytes]{};
|
||||
size_t line_size_ = 0;
|
||||
char response_[kResponseBytes]{};
|
||||
size_t response_size_ = 0;
|
||||
size_t response_offset_ = 0;
|
||||
};
|
||||
|
||||
} // namespace wake_beacon
|
||||
5
tools/switch2_wake_beacon/bluetooth_transport_config.h
Normal file
5
tools/switch2_wake_beacon/bluetooth_transport_config.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
// Policy for the shared wake engine, not the controller-host firmware config.
|
||||
#define SWITCH_PICO_ENABLE_BLE 1
|
||||
#define SWITCH_PICO_ENABLE_CLASSIC 0
|
||||
24
tools/switch2_wake_beacon/btstack_config.h
Normal file
24
tools/switch2_wake_beacon/btstack_config.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#ifndef SWITCH2_WAKE_BEACON_BTSTACK_CONFIG_H
|
||||
#define SWITCH2_WAKE_BEACON_BTSTACK_CONFIG_H
|
||||
|
||||
// BTstack requires its peripheral/advertiser fields for BLE advertising.
|
||||
// There is no central/Classic role, connection pool, GATT/SM/HID stack or scan.
|
||||
#if defined(ENABLE_CLASSIC) || defined(ENABLE_LE_CENTRAL)
|
||||
#error "Wake-only firmware must not enable controller discovery/hosting roles"
|
||||
#endif
|
||||
#define ENABLE_LE_PERIPHERAL
|
||||
#define HAVE_EMBEDDED_TIME_MS
|
||||
// Required by the SDK's compiled dump helper; no logger/dump is initialized.
|
||||
#define ENABLE_PRINTF_HEXDUMP
|
||||
#define HAVE_ASSERT
|
||||
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4
|
||||
#define HCI_INCOMING_PRE_BUFFER_SIZE 4
|
||||
#define HCI_ACL_PAYLOAD_SIZE 251
|
||||
#define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4
|
||||
#define MAX_NR_HCI_CONNECTIONS 0
|
||||
#define MAX_NR_L2CAP_CHANNELS 0
|
||||
#define MAX_NR_L2CAP_SERVICES 0
|
||||
#define MAX_NR_WHITELIST_ENTRIES 0
|
||||
#define HCI_RESET_RESEND_TIMEOUT_MS 1000
|
||||
|
||||
#endif
|
||||
281
tools/switch2_wake_beacon/main.cpp
Normal file
281
tools/switch2_wake_beacon/main.cpp
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
#include "beacon_protocol.h"
|
||||
|
||||
#include <btstack.h>
|
||||
#include "device/dcd.h"
|
||||
#include "hardware/sync.h"
|
||||
#include "pico/async_context_poll.h"
|
||||
#include "pico/btstack_hci_transport_cyw43.h"
|
||||
#include "pico/btstack_run_loop_async_context.h"
|
||||
#include "pico/cyw43_driver.h"
|
||||
#include "pico/stdio_usb.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include "tusb.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kStartupTimeoutMs = 10000;
|
||||
constexpr uint32_t kCommandTimeoutMs = 1000;
|
||||
constexpr uint32_t kBurstWatchdogMs = 8000;
|
||||
constexpr size_t kUsbBytesPerTurn = 64;
|
||||
const hci_cmd_t kReadScanEnable{0x0c19, ""};
|
||||
|
||||
enum class RadioPhase : uint8_t {
|
||||
Starting,
|
||||
DisableClassicScan,
|
||||
VerifyClassicScan,
|
||||
InitializeWake,
|
||||
Ready,
|
||||
Unconfigured,
|
||||
Failed,
|
||||
};
|
||||
|
||||
wake_beacon::Protocol protocol;
|
||||
async_context_poll_t radio_context;
|
||||
btstack_packet_callback_registration_t event_registration{};
|
||||
RadioPhase radio_phase = RadioPhase::Starting;
|
||||
bool driver_live = false;
|
||||
bool hci_initialized = false;
|
||||
bool wake_initialized = false;
|
||||
bool burst_watchdog_armed = false;
|
||||
uint16_t pending_opcode = 0;
|
||||
uint32_t command_deadline = 0;
|
||||
uint32_t startup_deadline = 0;
|
||||
uint32_t burst_deadline = 0;
|
||||
volatile bool usb_session_reset = false;
|
||||
|
||||
bool expired(uint32_t now, uint32_t deadline) {
|
||||
return static_cast<int32_t>(now - deadline) >= 0;
|
||||
}
|
||||
|
||||
void fail_radio() {
|
||||
// Deinitialization happens in the owner loop, not recursively in HCI events.
|
||||
radio_phase = RadioPhase::Failed;
|
||||
pending_opcode = 0;
|
||||
}
|
||||
|
||||
void handle_packet(uint8_t packet_type, uint16_t,
|
||||
uint8_t* packet, uint16_t size) {
|
||||
if (packet_type != HCI_EVENT_PACKET || size < 2 ||
|
||||
radio_phase == RadioPhase::Failed) {
|
||||
return;
|
||||
}
|
||||
const uint8_t event = hci_event_packet_get_type(packet);
|
||||
if (event == BTSTACK_EVENT_POWERON_FAILED ||
|
||||
event == HCI_EVENT_HARDWARE_ERROR) {
|
||||
fail_radio();
|
||||
return;
|
||||
}
|
||||
if (event == BTSTACK_EVENT_STATE && size >= 3) {
|
||||
const uint8_t state = btstack_event_state_get_state(packet);
|
||||
if (state == HCI_STATE_WORKING && radio_phase == RadioPhase::Starting) {
|
||||
radio_phase = RadioPhase::DisableClassicScan;
|
||||
} else if (state != HCI_STATE_INITIALIZING && state != HCI_STATE_WORKING) {
|
||||
fail_radio();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event != HCI_EVENT_COMMAND_COMPLETE || size < 5 ||
|
||||
pending_opcode == 0 ||
|
||||
hci_event_command_complete_get_command_opcode(packet) != pending_opcode) {
|
||||
return;
|
||||
}
|
||||
pending_opcode = 0;
|
||||
if (size < 6 ||
|
||||
hci_event_command_complete_get_return_parameters(packet)[0] !=
|
||||
ERROR_CODE_SUCCESS) {
|
||||
fail_radio();
|
||||
return;
|
||||
}
|
||||
if (radio_phase == RadioPhase::DisableClassicScan) {
|
||||
radio_phase = RadioPhase::VerifyClassicScan;
|
||||
} else if (radio_phase == RadioPhase::VerifyClassicScan) {
|
||||
// Both inquiry and page scanning must be off on the dual-mode CYW43.
|
||||
if (size < 7 || packet[6] != 0) {
|
||||
fail_radio();
|
||||
} else {
|
||||
radio_phase = RadioPhase::InitializeWake;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wake_beacon::RadioStatus radio_status() {
|
||||
wake_beacon::RadioStatus status;
|
||||
status.ready = radio_phase == RadioPhase::Ready;
|
||||
status.failed = radio_phase == RadioPhase::Failed;
|
||||
status.initialized = wake_initialized;
|
||||
if (wake_initialized) {
|
||||
switch2_wake_diagnostics(&status.wake);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void radio_owner_task() {
|
||||
const uint32_t now = to_ms_since_boot(get_absolute_time());
|
||||
if (radio_phase != RadioPhase::Ready &&
|
||||
radio_phase != RadioPhase::Unconfigured &&
|
||||
radio_phase != RadioPhase::Failed && expired(now, startup_deadline)) {
|
||||
fail_radio();
|
||||
}
|
||||
if (pending_opcode != 0 && expired(now, command_deadline)) {
|
||||
fail_radio();
|
||||
}
|
||||
if (radio_phase == RadioPhase::Failed) {
|
||||
if (driver_live) {
|
||||
// Stop the physical radio too: a controller fault or failed stop
|
||||
// must not leave advertising running while USB reports failure.
|
||||
if (hci_initialized) {
|
||||
hci_close();
|
||||
}
|
||||
cyw43_driver_deinit(&radio_context.core);
|
||||
driver_live = false;
|
||||
}
|
||||
protocol.observe(radio_status());
|
||||
return;
|
||||
}
|
||||
if (pending_opcode == 0 && hci_can_send_command_packet_now()) {
|
||||
if (radio_phase == RadioPhase::DisableClassicScan) {
|
||||
pending_opcode = hci_write_scan_enable.opcode;
|
||||
command_deadline = now + kCommandTimeoutMs;
|
||||
if (hci_send_cmd(&hci_write_scan_enable, 0) != ERROR_CODE_SUCCESS) {
|
||||
fail_radio();
|
||||
}
|
||||
} else if (radio_phase == RadioPhase::VerifyClassicScan) {
|
||||
pending_opcode = kReadScanEnable.opcode;
|
||||
command_deadline = now + kCommandTimeoutMs;
|
||||
if (hci_send_cmd(&kReadScanEnable) != ERROR_CODE_SUCCESS) {
|
||||
fail_radio();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (radio_phase == RadioPhase::InitializeWake) {
|
||||
if (!wake_initialized) {
|
||||
switch2_wake_initialize();
|
||||
wake_initialized = true;
|
||||
}
|
||||
const auto status = radio_status();
|
||||
if (!status.wake.configured) {
|
||||
radio_phase = RadioPhase::Unconfigured;
|
||||
} else if (status.wake.failures != 0) {
|
||||
fail_radio();
|
||||
} else if (switch2_wake_ready_for_connections() && !status.wake.busy) {
|
||||
radio_phase = RadioPhase::Ready;
|
||||
}
|
||||
}
|
||||
auto status = radio_status();
|
||||
if (burst_watchdog_armed) {
|
||||
if (!status.wake.busy) {
|
||||
burst_watchdog_armed = false;
|
||||
} else if (expired(now, burst_deadline)) {
|
||||
fail_radio();
|
||||
status = radio_status();
|
||||
}
|
||||
}
|
||||
protocol.observe(status);
|
||||
if (protocol.dispatch_pending()) {
|
||||
const bool accepted = switch2_wake_request();
|
||||
protocol.dispatched(accepted);
|
||||
if (accepted) {
|
||||
burst_deadline = now + kBurstWatchdogMs;
|
||||
burst_watchdog_armed = true;
|
||||
}
|
||||
protocol.observe(radio_status());
|
||||
}
|
||||
}
|
||||
|
||||
void reset_usb_session() {
|
||||
protocol.connected(false);
|
||||
tud_cdc_read_flush();
|
||||
tud_cdc_write_clear();
|
||||
}
|
||||
|
||||
void service_usb() {
|
||||
// TinyUSB's SDK version drains its event queue. Mask IRQs for this bounded
|
||||
// queue snapshot so continuous host traffic cannot keep refilling it. The
|
||||
// PICO OSAL preserves this interrupt mask; callbacks never touch the radio.
|
||||
const uint32_t saved = save_and_disable_interrupts();
|
||||
if (usb_session_reset) {
|
||||
usb_session_reset = false;
|
||||
reset_usb_session();
|
||||
}
|
||||
tud_task_ext(0, false);
|
||||
restore_interrupts(saved);
|
||||
|
||||
const bool connected = stdio_usb_connected();
|
||||
protocol.connected(connected);
|
||||
if (!connected) {
|
||||
tud_cdc_read_flush();
|
||||
tud_cdc_write_clear();
|
||||
return;
|
||||
}
|
||||
// One retained response applies backpressure before another request can be
|
||||
// parsed. Never use printf/stdio flush: they may wait for a disconnected PC.
|
||||
size_t count = protocol.output_size();
|
||||
const size_t available = tud_cdc_write_available();
|
||||
if (count > available) count = available;
|
||||
if (count > kUsbBytesPerTurn) count = kUsbBytesPerTurn;
|
||||
if (count != 0) {
|
||||
protocol.consume_output(tud_cdc_write(protocol.output_data(), count));
|
||||
}
|
||||
tud_cdc_write_flush();
|
||||
for (size_t index = 0; index < kUsbBytesPerTurn && protocol.can_receive();
|
||||
++index) {
|
||||
const int byte = tud_cdc_read_char();
|
||||
if (byte < 0) break;
|
||||
protocol.receive(static_cast<uint8_t>(byte));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void tud_cdc_line_state_cb(uint8_t interface, bool dtr, bool) {
|
||||
if (interface == 0 && !dtr) {
|
||||
reset_usb_session();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void tud_umount_cb() {
|
||||
reset_usb_session();
|
||||
}
|
||||
|
||||
extern "C" void tud_event_hook_cb(uint8_t, uint32_t event, bool) {
|
||||
// This hook may run in USB IRQ context. Only invalidate the session here;
|
||||
// TinyUSB and protocol work is deferred to service_usb() on Core 0.
|
||||
if (event == DCD_EVENT_BUS_RESET || event == DCD_EVENT_UNPLUGGED) {
|
||||
usb_session_reset = true;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Keep the SDK's unique-board-ID CDC descriptors, but use its TinyUSB FIFO
|
||||
// directly for bounded protocol output. No SDK/radio log belongs on CDC.
|
||||
if (!stdio_usb_init()) {
|
||||
return 1;
|
||||
}
|
||||
stdio_set_driver_enabled(&stdio_usb, false);
|
||||
startup_deadline = to_ms_since_boot(get_absolute_time()) + kStartupTimeoutMs;
|
||||
if (!async_context_poll_init_with_defaults(&radio_context) ||
|
||||
!cyw43_driver_init(&radio_context.core)) {
|
||||
fail_radio();
|
||||
} else {
|
||||
driver_live = true;
|
||||
// Deliberately bypass btstack_cyw43_init(): it initializes flash TLV.
|
||||
btstack_memory_init();
|
||||
btstack_run_loop_init(
|
||||
btstack_run_loop_async_context_get_instance(&radio_context.core));
|
||||
hci_init(hci_transport_cyw43_instance(), nullptr);
|
||||
hci_initialized = true;
|
||||
event_registration.callback = handle_packet;
|
||||
hci_add_event_handler(&event_registration);
|
||||
if (hci_power_control(HCI_POWER_ON) != ERROR_CODE_SUCCESS) {
|
||||
fail_radio();
|
||||
}
|
||||
}
|
||||
for (;;) {
|
||||
if (driver_live && radio_phase != RadioPhase::Failed) {
|
||||
async_context_poll(&radio_context.core);
|
||||
}
|
||||
radio_owner_task();
|
||||
service_usb();
|
||||
sleep_us(100);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue