fix(t1000): give USB data priority over Bluetooth

This commit is contained in:
mikecarper committed 2026-10-20 07:52:37 -07:00
1 parent 046b4a6719
commit 0fc7f377aa
11 files changed
+480 -9

No files matched your search

+6
View File
@@ -701,6 +701,12 @@ a fixed pairing PIN, then `reboot` to activate it. `set pin 0` restores the
default/generated PIN policy. A PIN saved through the Companion protocol also
takes effect after reboot. See the [PIN command reference](cli_commands.md#set-the-bluetooth-pairing-pin-companion).
On T1000-E, a USB data host automatically pauses Bluetooth, including an
existing phone connection. Unplug USB to resume Bluetooth after 500 ms; a
saved off setting stays off. A wall charger without a USB data host does not
pause Bluetooth. See the [T1000-E transport policy](role_feature_switches.md)
for status and saved-setting behavior.
The Bluetooth device name is independently configurable. In the text terminal,
use `get bluetooth.name` and `set bluetooth.name <name>`; use
`set bluetooth.name default` to restore `MeshCore-<node name>`. The WebConfig
+8
View File
@@ -178,6 +178,14 @@ See [Bluetooth controls](cli_commands.md#turn-bluetooth-on-or-off-companion)
for reply draining and boot-transport restrictions. Ethernet controls remain
specific to the board's recipe; there is no universal Ethernet-off command.
**T1000-E Companion:** USB data takes priority over Bluetooth. Once a computer
enumerates USB, the firmware stops Bluetooth advertising and disconnects any
Bluetooth client, even if no serial application is open. Bluetooth resumes
500 ms after USB disconnects, unless it was switched off manually. Battery
power and a charge-only wall supply leave Bluetooth available. The saved
Bluetooth setting, pairing PIN and bonds are preserved. `get bluetooth`
reports the USB pause; `set bluetooth on` saves the choice for after unplugging.
**SenseCAP Indicator Full only:** `set companion.transport wifi` followed by
`reboot` selects WiFi; `set companion.transport ble` followed by `reboot`
selects Bluetooth. Check `get companion.transport`. USB remains available.
+34 -3
View File
@@ -63,6 +63,7 @@ static uint32_t _atoi(const char* sp) {
// interface manager
#include <helpers/MultiSerialInterface.h>
#include <helpers/UsbBluetoothPriority.h>
MultiSerialInterface interface_manager;
// include bluetooth interface
@@ -2914,6 +2915,23 @@ void halt() {
if (companion_bluetooth_start_at == 0) companion_bluetooth_start_at = 1;
}
static void serviceCompanionUsbBluetoothPriority() {
#if defined(T1000_E) && defined(USE_TINYUSB)
static mesh::UsbBluetoothPriority priority;
const bool inhibited = priority.update(board.isUsbHostConnected(), millis());
if (inhibited && !interface_manager.isBluetoothInhibited()) {
// Abandon only this transport's work; USB must not inherit a partially
// streamed Bluetooth reply when the link is disconnected.
the_mesh.cancelSerialResponseStream(&bluetooth_interface);
the_mesh.cancelSerialOperationsForRoute(&bluetooth_interface);
}
interface_manager.setBluetoothInhibited(inhibited);
if (inhibited) {
interface_manager.forgetReplyRouteForDisconnected(&bluetooth_interface);
}
#endif
}
static void startCompanionBluetooth() {
if (companion_bluetooth_initialized) return;
companion_bluetooth_start_at = 0;
@@ -3009,8 +3027,9 @@ void halt() {
#endif
companion_bluetooth_initialized = true;
companion_bluetooth_start_failure[0] = 0;
serviceCompanionUsbBluetoothPriority();
if (interface_manager.isEnabled() && the_mesh.isBluetoothEnabledPreference()) {
bluetooth_interface.enable();
interface_manager.enableBluetooth();
}
}
@@ -3236,7 +3255,9 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
if (action == CompanionBluetoothCommand::Get) {
snprintf(reply, reply_size, "bluetooth %s%s",
interface_manager.isBluetoothEnabled() ? "on" : "off",
companion_bluetooth_off_at ? " (off pending)" : "");
companion_bluetooth_off_at ? " (off pending)"
: interface_manager.isBluetoothInhibited()
? " (USB data connected)" : "");
return true;
}
if (action == CompanionBluetoothCommand::On) {
@@ -3257,7 +3278,11 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
}
interface_manager.setBluetoothAutoEnable(true);
interface_manager.enableBluetooth();
if (interface_manager.isBluetoothEnabled()) {
if (interface_manager.isBluetoothInhibited()) {
companion_bluetooth_off_at = 0;
snprintf(reply, reply_size,
"OK - Bluetooth on (saved); paused while USB data is connected");
} else if (interface_manager.isBluetoothEnabled()) {
companion_bluetooth_off_at = 0;
snprintf(reply, reply_size, "OK - Bluetooth on (saved)");
} else if (the_mesh.setBluetoothEnabledPreference(previous_preference)) {
@@ -3276,6 +3301,7 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
}
companion_bluetooth_off_at = 0;
interface_manager.setBluetoothAutoEnable(false);
disableCompanionBluetoothForCli();
snprintf(reply, reply_size, "OK - Bluetooth already off (saved)");
return true;
}
@@ -3394,6 +3420,7 @@ public:
companion_bluetooth_off_at = 0;
if (on) interface_manager.enableBluetooth();
else disableCompanionBluetoothForCli();
if (on && interface_manager.isBluetoothInhibited()) return Result::Done;
return interface_manager.isBluetoothEnabled() == on ? Result::Done : Result::Failed;
}
#endif
@@ -3821,6 +3848,7 @@ void setup() {
// Keep stack registration available for a later USB `set bluetooth on`,
// without advertising even briefly when startup enables all interfaces.
interface_manager.setBluetoothAutoEnable(the_mesh.isBluetoothEnabledPreference());
serviceCompanionUsbBluetoothPriority();
#endif
// Lock the saved transport selection before bringing up either wireless stack.
@@ -4116,6 +4144,9 @@ static bool usbLoggingRecoverySafe(void*) {
}
void loop() {
#if defined(BLE_PIN_CODE)
serviceCompanionUsbBluetoothPriority();
#endif
#if defined(NRF52_PLATFORM)
board.prepareSleepCycle();
#endif
+24 -1
View File
@@ -28,6 +28,8 @@ private:
bool _enabled = false;
bool _bluetooth_auto_enable = true;
bool _bluetooth_requested = false;
bool _bluetooth_inhibited = false;
uint8_t _nextRxIndex = 0;
RegisteredInterface _interfaces[MAX_INTERFACES] = {};
BaseSerialInterface* _lastRxInterface = nullptr;
@@ -61,6 +63,21 @@ public:
// Explicit Bluetooth/wireless controls remain able to enable it temporarily.
void setBluetoothAutoEnable(bool enabled) { _bluetooth_auto_enable = enabled; }
// A temporary transport priority must not change the saved preference or
// lose explicit on/off requests made while Bluetooth is unavailable.
bool isBluetoothInhibited() const { return _bluetooth_inhibited; }
void setBluetoothInhibited(bool inhibited) {
if (_bluetooth_inhibited == inhibited) return;
_bluetooth_inhibited = inhibited;
if (inhibited) {
const bool requested = _bluetooth_requested;
disableBluetooth();
_bluetooth_requested = requested;
} else if (_enabled && _bluetooth_requested) {
enableBluetooth();
}
}
bool addInterface(InterfaceType type, BaseSerialInterface* iface) {
// make sure an interface was provided
if(iface == nullptr){
@@ -100,6 +117,8 @@ public:
}
void enableBluetooth() {
_bluetooth_requested = true;
if (_bluetooth_inhibited) return;
if (mesh::wireless::control().blocked(mesh::wireless::Bluetooth)) return;
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
@@ -109,6 +128,7 @@ public:
}
void disableBluetooth() {
_bluetooth_requested = false;
for(auto iface : _interfaces){
if(iface.instance && iface.type == InterfaceType::Bluetooth){
iface.instance->disable();
@@ -145,12 +165,15 @@ public:
// enable all interfaces
void enable() override {
_enabled = true;
_bluetooth_requested = _bluetooth_auto_enable;
_nextRxIndex = 0;
_lastRxInterface = nullptr;
_lockedReplyInterface = nullptr;
for(auto iface : _interfaces){
if(iface.instance){
if (iface.type == InterfaceType::Bluetooth && !_bluetooth_auto_enable) continue;
if (iface.type == InterfaceType::Bluetooth
&& (!_bluetooth_auto_enable || _bluetooth_inhibited
|| mesh::wireless::control().blocked(mesh::wireless::Bluetooth))) continue;
iface.instance->enable();
}
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <stdint.h>
namespace mesh {
// USB enumeration, rather than VBUS or an open terminal, identifies a data
// host. Hold priority across brief bus resets so Bluetooth does not flap as
// the same host re-enumerates. A wall charger never acquires priority.
class UsbBluetoothPriority {
bool inhibited_ = false;
bool disconnect_pending_ = false;
uint32_t disconnected_at_ = 0;
public:
bool update(bool host_connected, uint32_t now) {
if (host_connected) {
inhibited_ = true;
disconnect_pending_ = false;
} else if (inhibited_) {
if (!disconnect_pending_) {
disconnected_at_ = now;
disconnect_pending_ = true;
} else if (uint32_t(now - disconnected_at_) >= 500) {
inhibited_ = false;
disconnect_pending_ = false;
}
}
return inhibited_;
}
};
} // namespace mesh
+10 -4
View File
@@ -316,6 +316,12 @@ void SerialBLEInterface::onConnect(uint16_t connection_handle) {
instance->_pairingRequestPending.store(false, std::memory_order_release);
instance->_conn_handle = connection_handle;
instance->_isDeviceConnected = false;
// A queued connect callback can arrive after disable() stopped advertising
// and sampled an invalid handle. Close that late link as well.
if (!instance->_isEnabled) {
instance->disconnect();
return;
}
instance->_security_timer.start(millis());
instance->_companion_start_timer.cancel();
instance->_companionDataSeen.store(false, std::memory_order_release);
@@ -763,7 +769,7 @@ bool SerialBLEInterface::configureBondedOnlyAdvertising(
Bluefruit.Advertising.setType(
BLE_GAP_ADV_TYPE_CONNECTABLE_NONSCANNABLE_DIRECTED);
Bluefruit.Advertising.setPeerAddress(native_peer);
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.restartOnDisconnect(_isEnabled);
_bonded_only = true;
_stealth_pair_once = false;
_advertisingSuppressed.store(false, std::memory_order_release);
@@ -1016,7 +1022,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT);
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.restartOnDisconnect(_isEnabled);
if (bonded_only_peer != nullptr) {
configureBondedOnlyAdvertising(*bonded_only_peer, true);
@@ -1119,7 +1125,7 @@ void SerialBLEInterface::cancelStealthPairingTransition() {
_advertisingSuppressed.store(false, std::memory_order_release);
Bluefruit.Advertising.setType(
BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED);
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.restartOnDisconnect(_isEnabled);
if (_isEnabled && _conn_handle == BLE_CONN_HANDLE_INVALID
&& !isAdvertising()) {
startAdvertising("advertising failed after stealth cancellation");
@@ -1275,7 +1281,7 @@ void SerialBLEInterface::recoverStalledTx(const char* cause,
}
bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const {
if (_conn_handle != handle) {
if (!_isEnabled || _conn_handle != handle) {
return false;
}
BLEConnection* conn = Bluefruit.Connection(handle);
+36 -1
View File
@@ -114,7 +114,12 @@ static bool cccd_written_this_connection = false;
static struct { std::atomic<bool> valid{false}; } cccd_ram_cache;
static void captureCccdDeferred(uint16_t, ble_gap_addr_t*) {}
template <typename... Args> static bool ada_callback(Args...) { return true; }
static uint32_t sd_ble_gap_disconnect(uint16_t, uint8_t) { return NRF_SUCCESS; }
static std::vector<std::pair<uint16_t, uint8_t>> disconnect_requests;
static uint32_t sd_ble_gap_disconnect(uint16_t handle, uint8_t reason) {
assert(!critical_depth);
disconnect_requests.emplace_back(handle, reason);
return NRF_SUCCESS;
}
static uint32_t sd_ble_gap_conn_param_update(uint16_t, const void*) { return NRF_SUCCESS; }
static uint32_t millis() { return 100; }
@@ -146,6 +151,7 @@ struct SerialBLEInterface {
ble_gap_addr_t _peer_address;
bool _peer_address_valid = false, _bond_removed_for_connection = false;
bool _bonded_only = false, _isDeviceConnected = false;
bool _isEnabled = true;
uint16_t _conn_handle = BLE_CONN_HANDLE_INVALID;
unsigned removed_bonds = 0, bonded_recoveries = 0;
bool connected = true;
@@ -204,6 +210,7 @@ struct SerialBLEInterface {
++bonded_recoveries;
}
void clearBuffers();
void disconnect();
void resetTxState();
bool servicePendingTxReset();
static void onBLEEvent(ble_evt_t*);
@@ -476,9 +483,37 @@ static void production_connection_events_preserve_disconnect_cleanup() {
assert(link.recv_queue.snapshot(7, generation));
}
static void late_connect_after_disable_is_disconnected() {
SerialBLEInterface link;
instance = &link;
link._isEnabled = false;
link.recv_queue.setEnabled(false);
// The physical event and deferred callback may already be queued when USB
// priority disables Bluetooth. It must not leave an occupied hidden link.
ble_evt_t event;
event.header.evt_id = BLE_GAP_EVT_CONNECTED;
SerialBLEInterface::onBLEEvent(&event);
const size_t requests_before = disconnect_requests.size();
SerialBLEInterface::onConnect(7);
assert(disconnect_requests.size() == requests_before + 1);
assert(disconnect_requests.back().first == 7);
assert(disconnect_requests.back().second == BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
assert(!link._security_timer.pending() && !link._companion_start_timer.pending());
assert(!link._isDeviceConnected && !link._pairingRequestPending);
link.recv_queue.secure(7);
uint32_t generation;
assert(!link.recv_queue.snapshot(7, generation));
assert(link.removed_bonds == 0 && link.bonded_recoveries == 0);
// Keep the actual handle until the confirmed disconnect cleans it up.
assert(link._conn_handle == 7);
SerialBLEInterface::onDisconnect(7, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
assert(link._conn_handle == BLE_CONN_HANDLE_INVALID);
}
int main() {
ring_order_capacity_and_interleaved_copy();
session_reset_and_stale_handles();
callback_read_reset_and_reconnect();
production_connection_events_preserve_disconnect_cleanup();
late_connect_after_disable_is_disconnected();
}
@@ -194,6 +194,7 @@ class BluetoothPersistenceTests(unittest.TestCase):
main = (ROOT/'examples/companion_radio/main.cpp').read_text()
methods = '\n'.join(method(main, signature) for signature in (
'static void scheduleCompanionBluetoothRetry(',
'static void serviceCompanionUsbBluetoothPriority(',
'static void startCompanionBluetooth(',
'static void serviceDeferredCompanionBluetooth('))
with tempfile.TemporaryDirectory() as directory:
@@ -0,0 +1,326 @@
"""Execute USB/Bluetooth priority transitions with the real transport manager."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_companion_bluetooth_control import HARNESS as BLUETOOTH_HARNESS
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
TRANSPORT_HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <helpers/MultiSerialInterface.h>
#include <helpers/UsbBluetoothPriority.h>
struct Transport : BaseSerialInterface {
bool enabled=false,connected=false,frame=false;
unsigned enables=0,disables=0,writes=0;
void enable() override {enabled=true;++enables;}
void disable() override {enabled=false;connected=false;++disables;}
bool isEnabled() const override {return enabled;}
bool isConnected() const override {return connected;}
bool isReadBusy() const override {return false;}
bool isWriteBusy() const override {return false;}
size_t writeFrame(const uint8_t*,size_t n) override {++writes;return n;}
size_t checkRecvFrame(uint8_t* out) override {
if(!frame)return 0;frame=false;out[0]=0x42;return 1;
}
};
struct Fixture {
MultiSerialInterface manager;
Transport ble,usb,wifi,ethernet;
Fixture(){
assert(manager.addInterface(InterfaceType::Bluetooth,&ble));
assert(manager.addInterface(InterfaceType::USB,&usb));
assert(manager.addInterface(InterfaceType::WiFi,&wifi));
assert(manager.addInterface(InterfaceType::Ethernet,&ethernet));
}
void route(Transport& target){
target.connected=true;target.frame=true;
uint8_t data[176];assert(manager.checkRecvFrame(data)==1);
assert(manager.captureReplyRoute()==&target);
}
};
'''
class UsbBluetoothPriorityTests(unittest.TestCase):
def compile_and_run(self, source_text, flags=()):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
(folder / "Arduino.h").write_text(
"#pragma once\n#include <cstdint>\n#include <cstddef>\n"
"#include <initializer_list>\n"
)
source = folder / "usb_ble_priority.cpp"
source.write_text(source_text)
binary = folder / "usb_ble_priority"
built = subprocess.run(
[
os.environ.get("CXX", "g++"),
"-std=c++17", "-Wall", "-Wextra",
"-I", str(folder), "-I", str(ROOT), "-I", str(ROOT / "src"),
*["-D" + flag for flag in flags],
str(source), "-o", str(binary),
],
capture_output=True, text=True,
)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
def test_data_host_priority_and_disconnect_hold(self):
self.compile_and_run(TRANSPORT_HARNESS + r'''
int main(){
mesh::UsbBluetoothPriority policy;
// Battery and a charge-only cable have no enumerated data host.
assert(!policy.update(false,0));
assert(!policy.update(false,60000));
assert(policy.update(true,60001));
assert(policy.update(true,90000));
// A bus reset cannot briefly advertise Bluetooth to a nearby phone.
assert(policy.update(false,90001));
assert(policy.update(false,90500));
assert(!policy.update(false,90501));
assert(!policy.update(false,100000));
// Re-enumeration cancels the pending release, without inheriting its timer.
assert(policy.update(true,100001));
assert(policy.update(false,100010));
assert(policy.update(true,100509));
assert(policy.update(false,100510));
assert(policy.update(false,101009));
assert(!policy.update(false,101010));
// The absence hold has the same duration when millis() wraps.
mesh::UsbBluetoothPriority wrapped;
assert(wrapped.update(true,0xfffffe00u));
assert(wrapped.update(false,0xffffff00u));
assert(wrapped.update(false,0x000000f3u));
assert(!wrapped.update(false,0x000000f4u));
}
''')
def test_boot_wake_and_explicit_requests_during_usb_priority(self):
self.compile_and_run(TRANSPORT_HARNESS + r'''
int main(){
Fixture f;
f.manager.setBluetoothInhibited(true);
assert(f.manager.isBluetoothInhibited());
f.manager.enable();
assert(!f.ble.enabled&&f.ble.enables==0);
assert(f.usb.enabled&&f.wifi.enabled&&f.ethernet.enabled);
// Opening/closing a serial application is not an enable route around USB.
f.manager.enableBluetooth();
assert(!f.ble.enabled&&f.ble.enables==0);
const unsigned stopped=f.ble.disables;
f.manager.setBluetoothInhibited(true);
assert(f.ble.disables==stopped); // Repeated mounted polls are idempotent.
f.manager.disable();f.manager.enable();
assert(!f.ble.enabled&&f.ble.enables==0&&f.usb.enabled);
f.manager.setBluetoothInhibited(false);
assert(f.ble.enabled&&f.ble.enables==1);
f.manager.setBluetoothInhibited(false);
assert(f.ble.enables==1);
// An off request while paused survives unplugging.
f.manager.setBluetoothInhibited(true);
f.manager.disableBluetooth();
f.manager.setBluetoothInhibited(false);
assert(!f.ble.enabled&&f.ble.enables==1);
// An explicit on request is remembered, without advertising during USB.
f.manager.setBluetoothInhibited(true);
f.manager.enableBluetooth();
assert(!f.ble.enabled&&f.ble.enables==1);
f.manager.setBluetoothInhibited(false);
assert(f.ble.enabled&&f.ble.enables==2);
// Saved-off startup/wake remains off after the temporary policy clears.
Fixture off;
off.manager.setBluetoothAutoEnable(false);
off.manager.setBluetoothInhibited(true);off.manager.enable();
off.manager.setBluetoothInhibited(false);
assert(!off.ble.enabled&&off.ble.enables==0&&off.usb.enabled);
off.manager.setBluetoothInhibited(true);off.manager.enableBluetooth();
off.manager.setBluetoothInhibited(false);
assert(off.ble.enabled&&off.ble.enables==1); // Runtime-only override.
off.manager.disable();off.manager.enable();
assert(!off.ble.enabled&&off.ble.enables==1);
// An unplug cannot wake the aggregate transport when it is disabled.
Fixture sleeping;
sleeping.manager.enable();sleeping.manager.setBluetoothInhibited(true);
sleeping.manager.disable();
const unsigned sleeping_enables=sleeping.ble.enables;
sleeping.manager.setBluetoothInhibited(false);
assert(!sleeping.ble.enabled&&sleeping.ble.enables==sleeping_enables);
}
''')
def test_actual_t1000_service_uses_enumeration_and_cancels_only_bluetooth(self):
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
service = method(main, "static void serviceCompanionUsbBluetoothPriority(")
source = TRANSPORT_HARNESS + r'''
uint32_t now_ms=0;
uint32_t millis(){return now_ms;}
MultiSerialInterface interface_manager;
Transport bluetooth_interface,usb_interface;
struct {
bool host=false,charging=true,reader=false;
unsigned samples=0;
bool isUsbHostConnected(){++samples;return host;}
} board;
struct {
unsigned streams=0,operations=0;
void cancelSerialResponseStream(BaseSerialInterface* route){
assert(route==&bluetooth_interface);++streams;
}
void cancelSerialOperationsForRoute(BaseSerialInterface* route){
assert(route==&bluetooth_interface);++operations;
}
} the_mesh;
''' + service + r'''
int main(){
interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface);
interface_manager.addInterface(InterfaceType::USB,&usb_interface);
serviceCompanionUsbBluetoothPriority();interface_manager.enable();
assert(bluetooth_interface.enabled&&board.charging&&!board.reader);
board.host=true;now_ms=100;serviceCompanionUsbBluetoothPriority();
#if defined(T1000_E) && defined(USE_TINYUSB)
assert(interface_manager.isBluetoothInhibited()&&!bluetooth_interface.enabled);
assert(usb_interface.enabled&&the_mesh.streams==1&&the_mesh.operations==1);
// The host stays enumerated even with both serial readers closed.
now_ms=10000;serviceCompanionUsbBluetoothPriority();
assert(!bluetooth_interface.enabled&&the_mesh.streams==1);
board.reader=true;now_ms=10001;serviceCompanionUsbBluetoothPriority();
board.reader=false;now_ms=10002;serviceCompanionUsbBluetoothPriority();
assert(!bluetooth_interface.enabled&&the_mesh.operations==1);
board.host=false;now_ms=10003;serviceCompanionUsbBluetoothPriority();
now_ms=10502;serviceCompanionUsbBluetoothPriority();
assert(!bluetooth_interface.enabled);
now_ms=10503;serviceCompanionUsbBluetoothPriority();
assert(bluetooth_interface.enabled&&!interface_manager.isBluetoothInhibited());
assert(the_mesh.streams==1&&the_mesh.operations==1&&usb_interface.enabled);
#else
assert(bluetooth_interface.enabled&&!interface_manager.isBluetoothInhibited());
assert(board.samples==0&&the_mesh.streams==0&&the_mesh.operations==0);
#endif
}
'''
for flags in (
("T1000_E=1", "USE_TINYUSB=1"),
("USE_TINYUSB=1",),
("T1000_E=1",),
):
with self.subTest(configuration=flags):
self.compile_and_run(source, flags=flags)
def test_priority_preserves_usb_routes_and_rejects_old_bluetooth_replies(self):
self.compile_and_run(TRANSPORT_HARNESS + r'''
int main(){
const uint8_t reply[]={1,2,3};
Fixture f;f.manager.enable();f.route(f.usb);
const unsigned usb_enables=f.usb.enables,usb_disables=f.usb.disables;
f.manager.setBluetoothInhibited(true);
assert(f.manager.captureReplyRoute()==&f.usb);
assert(f.manager.isReplyRouteAvailable(&f.usb));
assert(f.manager.writeFrame(reply,sizeof(reply))==sizeof(reply));
assert(f.usb.writes==1&&f.ble.writes==0);
assert(f.usb.enabled&&f.wifi.enabled&&f.ethernet.enabled);
assert(f.usb.enables==usb_enables&&f.usb.disables==usb_disables);
// An interrupted multi-frame BLE reply must not fall back to USB.
f.manager.setBluetoothInhibited(false);f.route(f.ble);
f.manager.lockReplyRoute();f.manager.setBluetoothInhibited(true);
assert(!f.manager.isReplyRouteAvailable(&f.ble));
assert(f.manager.writeFrameToRoute(&f.ble,reply,sizeof(reply))==0);
assert(f.manager.writeFrame(reply,sizeof(reply))==0);
assert(f.usb.writes==1&&f.ble.writes==0);
f.manager.unlockReplyRoute();f.route(f.usb);
assert(f.manager.writeFrame(reply,sizeof(reply))==sizeof(reply));
assert(f.usb.writes==2);
}
''')
def test_wireless_master_off_remains_off_after_usb_disconnect(self):
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
prefix = BLUETOOTH_HARNESS.split("@METHODS@")[0]
adapter = method(main, "class CompanionWirelessBackend") + ";\n"
disable = method(main, "static void disableCompanionBluetoothForCli(")
self.compile_and_run(prefix + disable + adapter + r'''
int main(){
using namespace mesh::wireless;
interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface);
interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface);
interface_manager.enable();
CompanionWirelessBackend backend;control().begin(backend);
interface_manager.setBluetoothInhibited(true);
assert(!bluetooth_interface.enabled&&the_mesh.durable_bluetooth_preference);
char status[192];
assert(control().handle("set 2.4ghz off",status,sizeof(status),now_ms,Independent));
now_ms+=250;control().service(now_ms);
assert(control().masterOff()&&control().blocked(Bluetooth));
assert(!control().pending());
const unsigned previous_enables=bluetooth_interface.enables;
interface_manager.setBluetoothInhibited(false);
interface_manager.enableBluetooth(); // Master-off is still authoritative.
assert(!bluetooth_interface.enabled&&bluetooth_interface.enables==previous_enables);
interface_manager.disable();interface_manager.enable();
assert(!bluetooth_interface.enabled&&usb_serial_interface.enabled);
assert(the_mesh.durable_bluetooth_preference&&the_mesh.saves==0);
// Explicit restore-all is accepted while USB owns the transport. Reporting
// a startup failure here would contradict the remembered resume request.
interface_manager.setBluetoothInhibited(true);
assert(control().handle("set 2.4ghz on all",status,sizeof(status),now_ms,Independent));
now_ms+=250;control().service(now_ms);
assert(!control().pending()&&!control().masterOff()&&!control().blocked(Bluetooth));
assert(!bluetooth_interface.enabled); // No bypass during USB priority.
assert(control().handle("get 2.4ghz",status,sizeof(status),now_ms,Independent));
assert(!strstr(status,"failed"));
interface_manager.setBluetoothInhibited(false);
assert(bluetooth_interface.enabled&&usb_serial_interface.enabled);
assert(the_mesh.durable_bluetooth_preference&&the_mesh.saves==0);
}
''', flags=("BLE_PIN_CODE=1", "NRF52_PLATFORM=1", "ENABLE_USB_INTERFACE=1"))
def test_cli_saved_on_pauses_for_usb_and_saved_off_survives_unplug(self):
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
prefix = BLUETOOTH_HARNESS.split("@METHODS@")[0]
methods = "\n".join(method(main, signature) for signature in (
"static bool hasCompanionNonBluetoothClient(",
"static void disableCompanionBluetoothForCli(",
"bool handleCompanionBluetoothCommand(",
))
self.compile_and_run(prefix + methods + r'''
int main(){
interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface);
interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface);
interface_manager.enable();interface_manager.setBluetoothInhibited(true);
char reply[192];
assert(handleCompanionBluetoothCommand("get bluetooth",reply,sizeof(reply)));
assert(strstr(reply,"off")&&strstr(reply,"USB"));
const unsigned previous_enables=bluetooth_interface.enables;
assert(handleCompanionBluetoothCommand("set bluetooth on",reply,sizeof(reply)));
assert(strstr(reply,"OK")&&strstr(reply,"USB"));
assert(the_mesh.durable_bluetooth_preference&&!bluetooth_interface.enabled);
assert(bluetooth_interface.enables==previous_enables);
interface_manager.setBluetoothInhibited(false);
assert(bluetooth_interface.enabled);
interface_manager.setBluetoothInhibited(true);
assert(handleCompanionBluetoothCommand("set bluetooth off",reply,sizeof(reply)));
assert(strstr(reply,"OK")&&!the_mesh.durable_bluetooth_preference);
interface_manager.setBluetoothInhibited(false);
assert(!bluetooth_interface.enabled);
// Failed preference persistence cannot turn a saved-off device on later.
interface_manager.setBluetoothInhibited(true);the_mesh.storage_accepts=false;
assert(handleCompanionBluetoothCommand("set bluetooth on",reply,sizeof(reply)));
assert(strstr(reply,"Error")&&!the_mesh.durable_bluetooth_preference);
interface_manager.setBluetoothInhibited(false);
assert(!bluetooth_interface.enabled&&usb_serial_interface.enabled);
}
''', flags=("BLE_PIN_CODE=1", "NRF52_PLATFORM=1", "ENABLE_USB_INTERFACE=1"))
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -32,6 +32,7 @@ class Nrf52BleRxQueueTests(unittest.TestCase):
"static bool isBondAuthenticationFailure(",
"void SerialBLEInterface::onBLEEvent(",
"void SerialBLEInterface::onConnect(",
"void SerialBLEInterface::disconnect()",
"void SerialBLEInterface::onDisconnect(",
"void SerialBLEInterface::onBleUartRX(",
"bool SerialBLEInterface::isReadBusy() const",
+1
View File
@@ -151,6 +151,7 @@ struct MotaStream {
@FORMAT_DEVICE_NAME@
struct SerialBLEInterface {
@STARTUP_FIELDS@
bool _isEnabled = false;
std::atomic<bool> _companionDataSeen{false};
SecuritySessionTimer _companion_start_timer;
std::atomic<bool> _successfulConnectionPending{false};