Add guarded Bluetooth CLI on/off with force override

Add get bluetooth and set bluetooth on|off, with ble aliases. Require another
active management connection before disabling Bluetooth unless off has a
trailing force argument. Keep the toggle local to the current boot.

Drain Bluetooth replies before disconnecting and recheck the remaining
connection before applying a normal shutdown. Enabling Bluetooth cancels a
pending shutdown. Document the commands and add compiled regression coverage
to CI for native USB, HWCDC, UART, network clients, force, and timer rollover.

Validation: CLI, Bluetooth, and USB regression tests passed. XIAO ESP32-S3
and RAK4631 Full Companion builds passed RAM, flash, and capability checks.
This commit is contained in:
mikecarper committed 2026-09-14 23:02:54 -07:00
1 parent 170c2c44b1
commit 975167938d
6 files changed
+428 -2

No files matched your search

+3
View File
@@ -125,6 +125,9 @@ jobs:
- name: Verify Companion contact persistence and OTA sleep guards
run: python3 -B test/test_companion_contact_sleep_contract.py -v
- name: Verify Companion Bluetooth CLI connection protection
run: python3 -B test/test_companion_bluetooth_control.py -v
- name: Verify message reader buttons, touch targets, and footer layouts
working-directory: test
run: >-
+33
View File
@@ -1410,6 +1410,39 @@ Station G2/G3 targets default to `off`.
---
#### Turn Bluetooth on or off (Companion)
```text
get bluetooth
set bluetooth off
set bluetooth off force
set bluetooth on
```
`get ble` and `set ble ...` are aliases. This controls the running Bluetooth
service on ESP32 and nRF52 Companions: `off` stops advertising and disconnects
Bluetooth clients; `on` enables it again. The change lasts until reboot.
Plain `off` requires another active management connection. A connected USB
terminal or binary client, TCP client, or browser terminal can provide that
connection. WiFi association, a listening server, and USB power alone do not.
On USB hardware without an observable open-host session (HWCDC or a UART
bridge), issue the command through that USB client so the request itself proves
it is active.
Append `force` to `off` to permit disconnecting the only active connection.
Use USB, another management transport, or reboot to regain access afterward.
`force` is accepted only as the final argument to `off`.
Commands sent through Bluetooth allow its reply to drain before shutdown
(normally 250 ms, at most two seconds). `get bluetooth` shows `off pending`
during that wait. A normal shutdown is cancelled if the other management
connection disappears before it applies; `force` bypasses that check too.
Bluetooth must be available in the current build and boot transport mode.
An exclusive-WiFi boot still requires `set companion.transport ble` and a
reboot before Bluetooth can be enabled.
#### View or change the independent Bluetooth name (Companion)
**Usage:**
@@ -0,0 +1,47 @@
#pragma once
#include <stddef.h>
#include <string.h>
enum class CompanionBluetoothCommandSource { Other, Terminal, Framed };
enum class CompanionBluetoothCommand { None, Get, On, Off, ForceOff, Invalid };
inline const char* skipBluetoothCommandSpace(const char* text) {
while (*text == ' ' || *text == '\t') ++text;
return text;
}
inline bool bluetoothCommandWord(const char* text, const char* word) {
const size_t length = strlen(word);
return strncmp(text, word, length) == 0
&& *skipBluetoothCommandSpace(text + length) == 0;
}
inline CompanionBluetoothCommand parseCompanionBluetoothCommand(const char* text) {
if (!text) return CompanionBluetoothCommand::None;
text = skipBluetoothCommandSpace(text);
if (bluetoothCommandWord(text, "get bluetooth")
|| bluetoothCommandWord(text, "get ble")) return CompanionBluetoothCommand::Get;
const char* value = nullptr;
const char* const prefixes[] = {"set bluetooth", "set ble"};
for (const char* prefix : prefixes) {
const size_t length = strlen(prefix);
if (strncmp(text, prefix, length) == 0
&& (text[length] == 0 || text[length] == ' ' || text[length] == '\t')) {
value = skipBluetoothCommandSpace(text + length);
break;
}
}
if (!value) return CompanionBluetoothCommand::None;
if (bluetoothCommandWord(value, "on")) return CompanionBluetoothCommand::On;
if (bluetoothCommandWord(value, "off")) return CompanionBluetoothCommand::Off;
if (strncmp(value, "off", 3) == 0 && (value[3] == ' ' || value[3] == '\t')
&& bluetoothCommandWord(skipBluetoothCommandSpace(value + 3), "force")) {
return CompanionBluetoothCommand::ForceOff;
}
return CompanionBluetoothCommand::Invalid;
}
bool handleCompanionBluetoothCommand(
const char* command, char* reply, size_t reply_size,
CompanionBluetoothCommandSource source = CompanionBluetoothCommandSource::Other);
+13 -1
View File
@@ -1,5 +1,6 @@
#include <helpers/ui/DisplayPowerSettings.h>
#include "MyMesh.h"
#include "CompanionBluetooth.h"
#if defined(MESH_SOAK_DIAGNOSTICS)
#include "../../tools/hil/S3SoakDiagnostics.h"
#endif
@@ -2301,6 +2302,8 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (!command || !reply || reply_size == 0) return false;
while (*command == ' ') command++;
if (handleCompanionBluetoothCommand(command, reply, reply_size)) return true;
if (strcmp(command, "get powersaving") == 0 || strcmp(command, "powersaving") == 0) {
snprintf(reply, reply_size, "> %s", _prefs.powersaving_enabled ? "on" : "off");
return true;
@@ -5331,7 +5334,9 @@ void MyMesh::handleCmdFrame(size_t len) {
char command[MAX_FRAME_SIZE] = {0};
memcpy(command, &cmd_frame[1], command_len);
char reply[MAX_FRAME_SIZE] = {0};
if (!handleLocalControlCommand(command, reply, sizeof(reply))) {
if (!handleCompanionBluetoothCommand(command, reply, sizeof(reply),
CompanionBluetoothCommandSource::Framed)
&& !handleLocalControlCommand(command, reply, sizeof(reply))) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
@@ -7492,6 +7497,11 @@ void MyMesh::handleTerminalCommand(char* command) {
#endif
char local_reply[160];
if (handleCompanionBluetoothCommand(command, local_reply, sizeof(local_reply),
CompanionBluetoothCommandSource::Terminal)) {
terminalOutput().printf(" %s\r\n", local_reply);
return;
}
if (handleDirectCommand(command, local_reply, sizeof(local_reply))) {
terminalOutput().printf(" %s\r\n", local_reply);
return;
@@ -7983,6 +7993,8 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().print(" get bluetooth.name\r\n");
terminalOutput().print(" set bluetooth.name <name|default>\r\n");
#if defined(BLE_PIN_CODE)
terminalOutput().print(" get bluetooth (alias: get ble)\r\n");
terminalOutput().print(" set bluetooth <on|off> [force] (this boot; force only with off)\r\n");
terminalOutput().print(" get bluetooth.mac\r\n");
terminalOutput().print(
" set bluetooth.mac <address|random|random-every-boot|random-after-connect|default>\r\n");
+117 -1
View File
@@ -2,6 +2,7 @@
#include <Mesh.h>
#include <helpers/BluetoothMac.h>
#include "MyMesh.h"
#include "CompanionBluetooth.h"
#include "CompanionWiFi.h"
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
@@ -1797,7 +1798,9 @@ void halt() {
ota_console_client.print("> ");
#else
char reply[160]; reply[0] = 0;
if (!the_mesh.handleLocalCommand(ota_console_line, reply)
if (!handleCompanionBluetoothCommand(ota_console_line, reply, sizeof(reply),
CompanionBluetoothCommandSource::Terminal)
&& !the_mesh.handleLocalCommand(ota_console_line, reply)
&& !mesh::ota::handle_ota_command(ota_console_line, reply, board))
strcpy(reply, "Unknown local or OTA command");
ota_console_client.print(" -> "); ota_console_client.print(reply); ota_console_client.print("\r\n> ");
@@ -2249,6 +2252,118 @@ void halt() {
}
#endif
#if defined(BLE_PIN_CODE)
static uint32_t companion_bluetooth_off_at = 0;
static bool companion_bluetooth_force_off = false;
static bool hasCompanionNonBluetoothClient() {
if (!interface_manager.isEnabled()) return false;
if (interface_manager.isInterfaceConnected(InterfaceType::WiFi)
|| interface_manager.isInterfaceConnected(InterfaceType::Ethernet)) return true;
#if COMPANION_FEATURE_TEXT_TERMINAL \
&& (COMPANION_FEATURE_NETWORK_TERMINAL || defined(WITH_WEBCONFIG))
if (the_mesh.isAnyNetworkTerminalMode()) return true;
#endif
#if defined(ENABLE_USB_INTERFACE) \
&& (defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) \
|| (defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& (!defined(ARDUINO_USB_MODE) || ARDUINO_USB_MODE == 0)))
// Native CDC reports an open host session (including nRF52's DTR-low
// MeshCLI session). HWCDC SOFs and UART bridges cannot prove an open app;
// those clients count only when they issue this command themselves.
#if COMPANION_FEATURE_USB_MOTA_SOURCE
if (usb_mota_mode) return false;
#endif
if (usb_serial_interface.isEnabled() && isUsbTerminalDataConnected()) return true;
#endif
return false;
}
static void disableCompanionBluetoothForCli() {
companion_bluetooth_off_at = 0;
the_mesh.cancelSerialOperationsForRoute(&bluetooth_interface);
interface_manager.disableBluetooth();
interface_manager.forgetReplyRouteForDisconnected(&bluetooth_interface);
}
static void serviceCompanionBluetoothControl() {
if (companion_bluetooth_off_at == 0) return;
const int32_t elapsed = (int32_t)(millis() - companion_bluetooth_off_at);
if (elapsed < 0) return;
// The other host may have disconnected while the BLE reply was draining.
if (!companion_bluetooth_force_off && !hasCompanionNonBluetoothClient()) {
companion_bluetooth_off_at = 0;
mesh::usbLoggingPort().println(
"Bluetooth off cancelled: no other active management connection");
return;
}
// Give the requesting BLE client its reply, with a bounded wait even when
// its notification queue is stuck. The command itself reports a request.
if (bluetooth_interface.hasPendingIO() && elapsed < 1750) return;
disableCompanionBluetoothForCli();
}
#endif
bool handleCompanionBluetoothCommand(const char* command, char* reply,
size_t reply_size,
CompanionBluetoothCommandSource source) {
if (!reply || reply_size == 0) return false;
const auto action = parseCompanionBluetoothCommand(command);
if (action == CompanionBluetoothCommand::None) return false;
if (action == CompanionBluetoothCommand::Invalid) {
snprintf(reply, reply_size, "Error: use set bluetooth on|off [force]; force only with off");
return true;
}
#if defined(BLE_PIN_CODE)
if (!companion_bluetooth_initialized) {
snprintf(reply, reply_size, "Error: Bluetooth unavailable in this boot; check transport mode/startup");
return true;
}
if (action == CompanionBluetoothCommand::Get) {
snprintf(reply, reply_size, "bluetooth %s%s",
interface_manager.isBluetoothEnabled() ? "on" : "off",
companion_bluetooth_off_at ? " (off pending)" : "");
return true;
}
if (action == CompanionBluetoothCommand::On) {
companion_bluetooth_off_at = 0;
interface_manager.enableBluetooth();
snprintf(reply, reply_size, "%s", interface_manager.isBluetoothEnabled()
? "OK - Bluetooth on (this boot)" : "Error: Bluetooth enable failed");
return true;
}
if (!interface_manager.isBluetoothEnabled()) {
companion_bluetooth_off_at = 0;
snprintf(reply, reply_size, "OK - Bluetooth already off (this boot)");
return true;
}
const bool force = action == CompanionBluetoothCommand::ForceOff;
BaseSerialInterface* route = interface_manager.captureReplyRoute();
const bool non_bluetooth_requester = source == CompanionBluetoothCommandSource::Terminal
|| (source == CompanionBluetoothCommandSource::Framed
&& route != &bluetooth_interface
&& interface_manager.isReplyRouteAvailable(route));
if (!force && !non_bluetooth_requester && !hasCompanionNonBluetoothClient()) {
snprintf(reply, reply_size,
"Error: no other active management connection; use USB/TCP or set bluetooth off force");
return true;
}
if (non_bluetooth_requester) {
disableCompanionBluetoothForCli();
snprintf(reply, reply_size, "OK - Bluetooth off (this boot)");
} else {
companion_bluetooth_force_off = force;
companion_bluetooth_off_at = millis() + 250;
if (companion_bluetooth_off_at == 0) companion_bluetooth_off_at = 1;
snprintf(reply, reply_size, "OK - Bluetooth off requested (this boot)");
}
#else
(void)source;
snprintf(reply, reply_size, "Error: Bluetooth is not supported in this build");
#endif
return true;
}
#if defined(ESP32_PLATFORM) && COMPANION_FEATURE_MEMORY_DIAGNOSTICS
static void logFullCompanionMemory(const char* stage) {
mesh::usbLoggingPort().printf(
@@ -2920,5 +3035,6 @@ void loop() {
serviceDeferredCompanionBluetooth();
#endif
serviceCompanionBluetoothIdentity();
serviceCompanionBluetoothControl();
#endif
}
+215
View File
@@ -0,0 +1,215 @@
"""Execute Bluetooth CLI parsing, real transport routing, and shutdown guards."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
MAIN = (ROOT / 'examples/companion_radio/main.cpp').read_text()
HARNESS = r'''
#include <cassert>
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <helpers/MultiSerialInterface.h>
#include "examples/companion_radio/CompanionBluetooth.h"
uint32_t now_ms=100;
uint32_t millis(){return now_ms;}
bool usb_open=false, usb_power=true, network_terminal=false;
bool companion_bluetooth_initialized=true, usb_mota_mode=false;
uint32_t companion_bluetooth_off_at=0;
bool companion_bluetooth_force_off=false;
struct Transport : BaseSerialInterface {
bool enabled=false,connected=false,busy=false,frame=false,enable_fails=false;
unsigned disabled=0;
void enable() override {enabled=!enable_fails;}
void disable() override {enabled=false;connected=false;++disabled;}
bool isEnabled() const override{return enabled;}
bool isConnected() const override{return connected;}
bool isReadBusy() const override{return false;}
bool isWriteBusy() const override{return busy;}
bool hasPendingIO() const override{return busy;}
size_t writeFrame(const uint8_t*,size_t n) override{return n;}
size_t checkRecvFrame(uint8_t* out) override{
if(!frame)return 0; frame=false;out[0]=0x42;return 1;
}
} bluetooth_interface,usb_serial_interface,wifi_interface,ethernet_interface;
MultiSerialInterface interface_manager;
bool isUsbTerminalDataConnected(){return usb_open;}
unsigned cancelled=0,logs=0;
struct {
bool isAnyNetworkTerminalMode(){return network_terminal;}
void cancelSerialOperationsForRoute(BaseSerialInterface* route){
assert(route==&bluetooth_interface);++cancelled;
}
} the_mesh;
namespace mesh {
struct Logger {void println(const char*){++logs;}} logger;
Logger& usbLoggingPort(){return logger;}
}
@METHODS@
char reply[160];
bool command(const char* text,CompanionBluetoothCommandSource source=CompanionBluetoothCommandSource::Framed){
memset(reply,0,sizeof(reply));
return handleCompanionBluetoothCommand(text,reply,sizeof(reply),source);
}
void route(Transport& target){
target.connected=true;target.frame=true;
uint8_t frame[176];assert(interface_manager.checkRecvFrame(frame)==1);
assert(interface_manager.captureReplyRoute()==&target);
}
int main(){
#if defined(BLE_PIN_CODE)
interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface);
interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface);
interface_manager.addInterface(InterfaceType::WiFi,&wifi_interface);
interface_manager.addInterface(InterfaceType::Ethernet,&ethernet_interface);
interface_manager.enable();route(bluetooth_interface);
assert(command("get bluetooth")&&strstr(reply,"bluetooth on"));
for(const char* invalid : {"set ble","set bluetooth on force","set ble off force extra",
"set ble off forced","set bluetooth off junk","set ble yes"}){
assert(command(invalid)&&strstr(reply,"Error:"));
assert(bluetooth_interface.disabled==0&&companion_bluetooth_off_at==0);
}
assert(!command("get bluetooth.mac"));
assert(!command("set ble.stealth on"));
assert(command("set bluetooth off")&&strstr(reply,"no other active"));
assert(usb_power&&bluetooth_interface.enabled&&!companion_bluetooth_off_at);
// A disabled TCP interface and a socket that is merely listening don't count.
wifi_interface.connected=true;wifi_interface.enabled=false;
assert(command("set ble off")&&strstr(reply,"Error:"));
wifi_interface.enabled=true;wifi_interface.connected=false;
assert(command("set ble off")&&strstr(reply,"Error:"));
// A real TCP client allows a delayed BLE response. Losing it cancels shutdown.
wifi_interface.connected=true;
assert(command("set ble off")&&strstr(reply,"requested"));
assert(command("get ble")&&strstr(reply,"off pending"));
now_ms+=249;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
wifi_interface.connected=false;++now_ms;serviceCompanionBluetoothControl();
assert(bluetooth_interface.enabled&&!companion_bluetooth_off_at&&logs==1);
// Force may disconnect the sole client, but waits for queued notifications.
assert(command(" set ble\t off\tforce \t")&&strstr(reply,"requested"));
bluetooth_interface.busy=true;now_ms+=250;serviceCompanionBluetoothControl();
assert(bluetooth_interface.enabled);
now_ms+=1749;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
++now_ms;serviceCompanionBluetoothControl();
assert(!bluetooth_interface.enabled&&cancelled==1);
assert(interface_manager.captureReplyRoute()==nullptr);
assert(command("set ble off")&&strstr(reply,"already off"));
assert(command("get ble")&&strcmp(reply,"bluetooth off")==0);
bluetooth_interface.busy=false;
assert(command("set bluetooth on")&&strstr(reply,"OK"));route(bluetooth_interface);
// Another native CDC session counts; HWCDC/bridge presence alone doesn't.
usb_open=true;usb_serial_interface.connected=true;
assert(command("set ble off"));
#if defined(NRF52_PLATFORM) || (defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 0)
assert(strstr(reply,"requested"));
usb_open=false;usb_serial_interface.connected=false;
now_ms+=250;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
#else
assert(strstr(reply,"Error:"));usb_open=false;usb_serial_interface.connected=false;
#endif
// USB MOTA ownership cannot count as a separate management terminal.
usb_mota_mode=true;usb_open=true;
assert(command("set ble off")&&strstr(reply,"Error:"));
usb_mota_mode=false;usb_open=false;
// An actual USB command proves that requester is present on every backend.
route(usb_serial_interface);
assert(command("set ble off")&&strstr(reply,"Bluetooth off (this boot)"));
assert(!bluetooth_interface.enabled&&interface_manager.captureReplyRoute()==&usb_serial_interface);
assert(command("set ble on")&&bluetooth_interface.enabled);
// ASCII terminal commands cannot inherit the previous framed BLE route.
route(bluetooth_interface);
assert(command("set ble off",CompanionBluetoothCommandSource::Terminal));
assert(!bluetooth_interface.enabled);
assert(command("set ble on"));route(bluetooth_interface);
// Active browser/TCP terminal and Ethernet sessions each qualify.
network_terminal=true;
assert(command("set ble off")&&strstr(reply,"requested"));
assert(command("set ble on")&&!companion_bluetooth_off_at);
now_ms+=300;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
network_terminal=false;ethernet_interface.connected=true;
assert(command("set ble off")&&strstr(reply,"requested"));
now_ms+=250;serviceCompanionBluetoothControl();assert(!bluetooth_interface.enabled);
assert(command("set ble on"));route(bluetooth_interface);ethernet_interface.connected=false;
// Pending deadlines work across millis() wrap, including the zero sentinel.
for(uint32_t start : {0xffffff00u,0xffffff06u}){
now_ms=start;assert(command("set ble off force"));
now_ms=start+249;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
now_ms=start+251;serviceCompanionBluetoothControl();assert(!bluetooth_interface.enabled);
assert(command("set ble on"));route(bluetooth_interface);
}
companion_bluetooth_initialized=false;
assert(command("set ble on")&&strstr(reply,"unavailable"));
assert(command("set ble off force")&&strstr(reply,"unavailable"));
companion_bluetooth_initialized=true;
assert(command("set ble off",CompanionBluetoothCommandSource::Terminal));
bluetooth_interface.enable_fails=true;
assert(command("set ble on")&&strstr(reply,"enable failed"));
#else
assert(command("get ble")&&strstr(reply,"not supported"));
assert(command("set ble off force")&&strstr(reply,"not supported"));
#endif
assert(!handleCompanionBluetoothCommand(nullptr,reply,sizeof(reply)));
assert(!handleCompanionBluetoothCommand("get ble",nullptr,sizeof(reply)));
assert(!handleCompanionBluetoothCommand("set ble off force",reply,0));
}
'''
class BluetoothControlTests(unittest.TestCase):
def test_actual_commands_and_connection_guards(self):
guards = '\n'.join(method(MAIN, signature) for signature in (
'static bool hasCompanionNonBluetoothClient(',
'static void disableCompanionBluetoothForCli(',
'static void serviceCompanionBluetoothControl('))
methods = '#if defined(BLE_PIN_CODE)\n' + guards + '\n#endif\n' + method(
MAIN, 'bool handleCompanionBluetoothCommand(')
configurations = {
'no_ble': [],
'nrf52': ['BLE_PIN_CODE=123456', 'NRF52_PLATFORM=1'],
'esp32_tinyusb': ['BLE_PIN_CODE=123456', 'ESP32=1',
'ARDUINO_USB_CDC_ON_BOOT=1', 'ARDUINO_USB_MODE=0'],
'esp32_hwcdc': ['BLE_PIN_CODE=123456', 'ESP32=1',
'ARDUINO_USB_CDC_ON_BOOT=1', 'ARDUINO_USB_MODE=1'],
'esp32_uart': ['BLE_PIN_CODE=123456', 'ESP32=1'],
}
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/'bluetooth.cpp'
source.write_text(HARNESS.replace('@METHODS@', methods))
for name, defines in configurations.items():
with self.subTest(platform=name):
binary = folder/name
built = subprocess.run([
os.environ.get('CXX','g++'), '-std=c++17', '-Wall', '-Wextra',
'-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'),
'-DENABLE_USB_INTERFACE=1', '-DCOMPANION_FEATURE_TEXT_TERMINAL=1',
'-DCOMPANION_FEATURE_NETWORK_TERMINAL=1',
'-DCOMPANION_FEATURE_USB_MOTA_SOURCE=1',
*['-D'+flag for flag in defines],str(source),'-o',str(binary)],
capture_output=True,text=True)
self.assertEqual(built.returncode,0,built.stderr)
ran = subprocess.run([str(binary)],capture_output=True,text=True)
self.assertEqual(ran.returncode,0,ran.stderr)
def test_cli_entry_points_pass_the_current_source(self):
mesh = (ROOT/'examples/companion_radio/MyMesh.cpp').read_text()
terminal = method(mesh,'void MyMesh::handleTerminalCommand(')
framed = method(mesh,'void MyMesh::handleCmdFrame(')
local = method(mesh,'bool MyMesh::handleLocalControlCommand(')
self.assertIn('CompanionBluetoothCommandSource::Terminal',terminal)
self.assertIn('CompanionBluetoothCommandSource::Framed',framed)
self.assertIn('handleCompanionBluetoothCommand(command, reply, reply_size)',local)
loop = method(MAIN,'\nvoid loop()')
self.assertIn('serviceCompanionBluetoothControl();',loop)
if __name__ == '__main__':
unittest.main()