fix(t1000): give USB data priority over Bluetooth
This commit is contained in:
1 parent
046b4a6719
commit
0fc7f377aa
11 files changed
+480
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
Vendored
+36
-1
@@ -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,ðernet));
|
||||
}
|
||||
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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in new issue
Block a user