Bluetooth (#518)
* Bluetooth LE addition * fixes * use the psram! helps a little on S3 (t-deck) * custom device name * Update symbols.c * Feedback + fixes Fixes external app start/stop server (child devices) Fixes BtManage causing a full system hang upon disabling bt when a device is connected to the host. * updoot * more updoot * move back! * Revert "move back!" This reverts commit d3694365c634acc5db62ac59771c496cb971a727. * fix some of the things * Addressing feedback? hmm * Fixes Bug 1 — Reconnect loop / Reconnect not working fixed Bug 2 — Name-only advertising overwrites HID advertising Bug 3 — BleHidDeviceCtx leak on re-enable Enhancement — HID device auto-start on radio re-enable * stuff... * update for consistency with others * fix crashes and some bonus symbols * a few symbols, i2c speed, cdn message 100kHz i2c speed seems to be more compatible with m5stack modules...and probably in general. cdn message no longer applies * Hide BT Settings when bt not enabled * Addressing things and device fixes * Missed one! * stuff
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
#include <Tactility/app/btmanage/BtManagePrivate.h>
|
||||
#include <Tactility/app/btmanage/View.h>
|
||||
|
||||
#include <Tactility/app/AppContext.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/LogMessages.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <tactility/lvgl_icon_shared.h>
|
||||
|
||||
namespace tt::app::btmanage {
|
||||
|
||||
static const auto LOGGER = Logger("BtManage");
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
static void onBtToggled(bool enabled) {
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
if (!dev) return;
|
||||
bluetooth_set_radio_enabled(dev, enabled);
|
||||
}
|
||||
|
||||
static void onScanToggled(bool enabled) {
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
if (!dev) return;
|
||||
if (enabled) {
|
||||
bluetooth_scan_start(dev);
|
||||
} else {
|
||||
bluetooth_scan_stop(dev);
|
||||
}
|
||||
}
|
||||
|
||||
static void onConnectPeer(const std::array<uint8_t, 6>& addr, int profileId) {
|
||||
bluetooth::connect(addr, profileId);
|
||||
}
|
||||
|
||||
static void onDisconnectPeer(const std::array<uint8_t, 6>& addr, int profileId) {
|
||||
bluetooth::disconnect(addr, profileId);
|
||||
}
|
||||
|
||||
static void onPairPeer(const std::array<uint8_t, 6>& addr) {
|
||||
// Clicking an unrecognised scan result initiates a HID host connection.
|
||||
// Bond exchange happens automatically during the first connection.
|
||||
bluetooth::hidHostConnect(addr);
|
||||
}
|
||||
|
||||
static void onForgetPeer(const std::array<uint8_t, 6>& addr) {
|
||||
bluetooth::unpair(addr);
|
||||
}
|
||||
|
||||
BtManage::BtManage() {
|
||||
bindings = (Bindings) {
|
||||
.onBtToggled = onBtToggled,
|
||||
.onScanToggled = onScanToggled,
|
||||
.onConnectPeer = onConnectPeer,
|
||||
.onDisconnectPeer = onDisconnectPeer,
|
||||
.onPairPeer = onPairPeer,
|
||||
.onForgetPeer = onForgetPeer,
|
||||
};
|
||||
}
|
||||
|
||||
void BtManage::lock() {
|
||||
mutex.lock();
|
||||
}
|
||||
|
||||
void BtManage::unlock() {
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
void BtManage::requestViewUpdate() {
|
||||
lock();
|
||||
if (isViewEnabled) {
|
||||
if (lvgl::lock(1000)) {
|
||||
view.update();
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
void BtManage::onBtEvent(const struct BtEvent& event) {
|
||||
auto radio_state = bluetooth::getRadioState();
|
||||
LOGGER.info("Update with state {}", bluetooth::radioStateToString(radio_state));
|
||||
getState().setRadioState(radio_state);
|
||||
switch (event.type) {
|
||||
case BT_EVENT_SCAN_STARTED:
|
||||
getState().setScanning(true);
|
||||
break;
|
||||
case BT_EVENT_SCAN_FINISHED:
|
||||
getState().setScanning(false);
|
||||
getState().updateScanResults();
|
||||
getState().updatePairedPeers();
|
||||
break;
|
||||
case BT_EVENT_PEER_FOUND:
|
||||
getState().updateScanResults();
|
||||
break;
|
||||
case BT_EVENT_PAIR_RESULT:
|
||||
getState().updatePairedPeers();
|
||||
break;
|
||||
case BT_EVENT_PROFILE_STATE_CHANGED:
|
||||
getState().updateScanResults();
|
||||
getState().updatePairedPeers();
|
||||
break;
|
||||
case BT_EVENT_RADIO_STATE_CHANGED:
|
||||
if (event.radio_state == BT_RADIO_STATE_ON) {
|
||||
getState().updatePairedPeers();
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
if (dev && !bluetooth_is_scanning(dev)) {
|
||||
bluetooth_scan_start(dev);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
requestViewUpdate();
|
||||
}
|
||||
|
||||
static void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent event) {
|
||||
// BT event callbacks can fire from the NimBLE host task (e.g. DISCONNECT during
|
||||
// nimble_port_stop shutdown). Calling onBtEvent() synchronously from the NimBLE
|
||||
// task would block it on the LVGL mutex (held by the LVGL task waiting in
|
||||
// nimble_port_stop), creating a permanent deadlock. Dispatch to the main task so
|
||||
// the NimBLE host task is never blocked by BtManage's state updates or LVGL lock.
|
||||
auto* self = static_cast<BtManage*>(context);
|
||||
getMainDispatcher().dispatch([self, event] {
|
||||
self->onBtEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
void BtManage::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
// Initialise state and view before subscribing to avoid incoming events
|
||||
// racing with state initialisation.
|
||||
state.setRadioState(bluetooth::getRadioState());
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
state.setScanning(dev ? bluetooth_is_scanning(dev) : false);
|
||||
state.updateScanResults();
|
||||
state.updatePairedPeers();
|
||||
|
||||
lock();
|
||||
isViewEnabled = true;
|
||||
view.init(app, parent);
|
||||
view.update();
|
||||
unlock();
|
||||
|
||||
btDevice = dev;
|
||||
if (btDevice) {
|
||||
bluetooth_add_event_callback(btDevice, this, onKernelBtEvent);
|
||||
}
|
||||
|
||||
auto radio_state = bluetooth::getRadioState();
|
||||
bool can_scan = radio_state == bluetooth::RadioState::On;
|
||||
LOGGER.info("Radio: {}, Scanning: {}, Can scan: {}",
|
||||
bluetooth::radioStateToString(radio_state),
|
||||
dev ? bluetooth_is_scanning(dev) : false,
|
||||
can_scan);
|
||||
if (can_scan && dev && !bluetooth_is_scanning(dev)) {
|
||||
bluetooth_scan_start(dev);
|
||||
}
|
||||
}
|
||||
|
||||
void BtManage::onHide(AppContext& app) {
|
||||
lock();
|
||||
if (btDevice) {
|
||||
bluetooth_remove_event_callback(btDevice, onKernelBtEvent);
|
||||
btDevice = nullptr;
|
||||
}
|
||||
isViewEnabled = false;
|
||||
unlock();
|
||||
}
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.appId = "BtManage",
|
||||
.appName = "Bluetooth",
|
||||
.appIcon = LVGL_ICON_SHARED_BLUETOOTH,
|
||||
.appCategory = Category::Settings,
|
||||
.createApp = create<BtManage>
|
||||
};
|
||||
|
||||
LaunchId start() {
|
||||
return app::start(manifest.appId);
|
||||
}
|
||||
|
||||
} // namespace tt::app::btmanage
|
||||
@@ -0,0 +1,44 @@
|
||||
#include <Tactility/app/btmanage/BtManagePrivate.h>
|
||||
|
||||
namespace tt::app::btmanage {
|
||||
|
||||
void State::setScanning(bool isScanning) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
scanning = isScanning;
|
||||
}
|
||||
|
||||
bool State::isScanning() const {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
return scanning;
|
||||
}
|
||||
|
||||
void State::setRadioState(bluetooth::RadioState s) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
radioState = s;
|
||||
}
|
||||
|
||||
bluetooth::RadioState State::getRadioState() const {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
return radioState;
|
||||
}
|
||||
|
||||
void State::updateScanResults() {
|
||||
// Fetch outside the lock to avoid holding it during a service call.
|
||||
auto results = bluetooth::getScanResults();
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
scanResults = std::move(results);
|
||||
}
|
||||
|
||||
void State::updatePairedPeers() {
|
||||
auto peers = bluetooth::getPairedPeers();
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
pairedPeers = std::move(peers);
|
||||
}
|
||||
|
||||
} // namespace tt::app::btmanage
|
||||
@@ -0,0 +1,246 @@
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
#include <tactility/lvgl_module.h>
|
||||
|
||||
#include <Tactility/app/btmanage/View.h>
|
||||
#include <Tactility/app/btmanage/BtManagePrivate.h>
|
||||
#include <Tactility/app/btpeersettings/BtPeerSettings.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
#include <Tactility/bluetooth/BluetoothSettings.h>
|
||||
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
namespace tt::app::btmanage {
|
||||
|
||||
static const auto LOGGER = Logger("BtManageView");
|
||||
|
||||
static void onEnableSwitchChanged(lv_event_t* event) {
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
|
||||
bt->getBindings().onBtToggled(is_on);
|
||||
}
|
||||
|
||||
static void onEnableOnBootSwitchChanged(lv_event_t* event) {
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
getMainDispatcher().dispatch([is_on] {
|
||||
bluetooth::settings::setEnableOnBoot(is_on);
|
||||
});
|
||||
}
|
||||
|
||||
static void onEnableOnBootParentClicked(lv_event_t* event) {
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
||||
bool new_state = !lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
if (new_state) {
|
||||
lv_obj_add_state(enable_switch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(enable_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
// add/remove_state does not fire LV_EVENT_VALUE_CHANGED, so persist here directly.
|
||||
getMainDispatcher().dispatch([new_state] {
|
||||
bluetooth::settings::setEnableOnBoot(new_state);
|
||||
});
|
||||
}
|
||||
|
||||
static void onScanButtonClicked(lv_event_t* event) {
|
||||
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
bool scanning = dev ? bluetooth_is_scanning(dev) : false;
|
||||
bt->getBindings().onScanToggled(!scanning);
|
||||
}
|
||||
|
||||
// region Peer list callbacks
|
||||
|
||||
struct PeerListItemData {
|
||||
View* view;
|
||||
size_t index;
|
||||
bool isPaired;
|
||||
};
|
||||
|
||||
void View::onConnect(lv_event_t* event) {
|
||||
auto* data = static_cast<PeerListItemData*>(lv_event_get_user_data(event));
|
||||
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
|
||||
auto& state = bt->getState();
|
||||
|
||||
if (data->isPaired) {
|
||||
// Open the per-device settings screen for paired devices
|
||||
auto peers = state.getPairedPeers();
|
||||
if (data->index < peers.size()) {
|
||||
btpeersettings::start(bluetooth::settings::addrToHex(peers[data->index].addr));
|
||||
}
|
||||
} else {
|
||||
// Unrecognised scan result — initiate pairing
|
||||
auto peers = state.getScanResults();
|
||||
if (data->index < peers.size()) {
|
||||
bt->getBindings().onPairPeer(peers[data->index].addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Peer list callbacks
|
||||
|
||||
static uint8_t mapRssiToPercentage(int8_t rssi) {
|
||||
auto abs_rssi = std::abs(rssi);
|
||||
if (abs_rssi < 30) abs_rssi = 30;
|
||||
if (abs_rssi > 90) abs_rssi = 90;
|
||||
return static_cast<uint8_t>((float)(90 - abs_rssi) / 60.f * 100.f);
|
||||
}
|
||||
|
||||
void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index) {
|
||||
const auto percentage = mapRssiToPercentage(record.rssi);
|
||||
const auto label = record.name.empty()
|
||||
? std::format("Unknown ({:02x}{:02x}{:02x}{:02x}{:02x}{:02x}) {}%",
|
||||
record.addr[0], record.addr[1], record.addr[2],
|
||||
record.addr[3], record.addr[4], record.addr[5],
|
||||
percentage)
|
||||
: std::format("{} {}%", record.name, percentage);
|
||||
|
||||
auto* button = lv_list_add_button(peers_list, nullptr, label.c_str());
|
||||
|
||||
auto* item_data = new PeerListItemData { this, index, isPaired };
|
||||
lv_obj_set_user_data(button, item_data);
|
||||
lv_obj_add_event_cb(button, onConnect, LV_EVENT_SHORT_CLICKED, item_data);
|
||||
lv_obj_add_event_cb(button, [](lv_event_t* e) {
|
||||
delete static_cast<PeerListItemData*>(lv_obj_get_user_data(lv_event_get_current_target_obj(e)));
|
||||
}, LV_EVENT_DELETE, nullptr);
|
||||
}
|
||||
|
||||
// region Secondary updates
|
||||
|
||||
void View::updateBtToggle() {
|
||||
lv_obj_clear_state(enable_switch, LV_STATE_ANY);
|
||||
switch (state->getRadioState()) {
|
||||
using enum bluetooth::RadioState;
|
||||
case On:
|
||||
lv_obj_add_state(enable_switch, LV_STATE_CHECKED);
|
||||
break;
|
||||
case OnPending:
|
||||
lv_obj_add_state(enable_switch, LV_STATE_CHECKED);
|
||||
lv_obj_add_state(enable_switch, LV_STATE_DISABLED);
|
||||
break;
|
||||
case Off:
|
||||
lv_obj_remove_state(enable_switch, LV_STATE_CHECKED);
|
||||
lv_obj_remove_state(enable_switch, LV_STATE_DISABLED);
|
||||
break;
|
||||
case OffPending:
|
||||
lv_obj_remove_state(enable_switch, LV_STATE_CHECKED);
|
||||
lv_obj_add_state(enable_switch, LV_STATE_DISABLED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void View::updateEnableOnBootToggle() {
|
||||
if (enable_on_boot_switch != nullptr) {
|
||||
lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY);
|
||||
if (bluetooth::settings::shouldEnableOnBoot()) {
|
||||
lv_obj_add_state(enable_on_boot_switch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(enable_on_boot_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void View::updateScanning() {
|
||||
if (state->getRadioState() == bluetooth::RadioState::On && state->isScanning()) {
|
||||
lv_obj_remove_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
void View::updatePeerList() {
|
||||
lv_obj_clean(peers_list);
|
||||
|
||||
// Enable on boot row
|
||||
auto* enable_on_boot_wrapper = lv_obj_create(peers_list);
|
||||
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
|
||||
lv_label_set_text(enable_label, "Enable on boot");
|
||||
lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper);
|
||||
lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
|
||||
|
||||
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
|
||||
lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 2, LV_STATE_DEFAULT);
|
||||
} else {
|
||||
lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 8, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
updateEnableOnBootToggle();
|
||||
|
||||
using enum bluetooth::RadioState;
|
||||
if (state->getRadioState() == On) {
|
||||
// Paired peers section
|
||||
auto paired = state->getPairedPeers();
|
||||
if (!paired.empty()) {
|
||||
lv_list_add_text(peers_list, "Paired");
|
||||
for (size_t i = 0; i < paired.size(); ++i) {
|
||||
createPeerListItem(paired[i], true, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan results section
|
||||
auto scan_results = state->getScanResults();
|
||||
lv_list_add_text(peers_list, "Available");
|
||||
if (!scan_results.empty()) {
|
||||
for (size_t i = 0; i < scan_results.size(); ++i) {
|
||||
createPeerListItem(scan_results[i], false, i);
|
||||
}
|
||||
} else if (!state->isScanning()) {
|
||||
auto* no_devices_label = lv_label_create(peers_list);
|
||||
lv_label_set_text(no_devices_label, "No devices found.");
|
||||
}
|
||||
// Never hide peers_list: it always contains the "Enable on boot" row.
|
||||
// While scanning with no results the spinner in the toolbar provides feedback.
|
||||
|
||||
// Scan button
|
||||
auto* scan_button = lv_button_create(peers_list);
|
||||
lv_obj_set_width(scan_button, LV_PCT(100));
|
||||
lv_obj_set_style_margin_ver(scan_button, 4, LV_STATE_DEFAULT);
|
||||
auto* scan_label = lv_label_create(scan_button);
|
||||
lv_label_set_text(scan_label, state->isScanning() ? "Stop scan" : "Scan");
|
||||
lv_obj_add_event_cb(scan_button, onScanButtonClicked, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Secondary updates
|
||||
|
||||
void View::init(const AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
root = parent;
|
||||
paths = app.getPaths();
|
||||
|
||||
// Toolbar
|
||||
auto* toolbar = lvgl::toolbar_create(parent, app);
|
||||
|
||||
scanning_spinner = lvgl::toolbar_add_spinner_action(toolbar);
|
||||
|
||||
enable_switch = lvgl::toolbar_add_switch_action(toolbar);
|
||||
lv_obj_add_event_cb(enable_switch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
|
||||
// Peer list
|
||||
peers_list = lv_list_create(parent);
|
||||
lv_obj_set_flex_grow(peers_list, 1);
|
||||
lv_obj_set_width(peers_list, LV_PCT(100));
|
||||
}
|
||||
|
||||
void View::update() {
|
||||
updateBtToggle();
|
||||
updateScanning();
|
||||
updatePeerList();
|
||||
}
|
||||
|
||||
} // namespace tt::app::btmanage
|
||||
@@ -0,0 +1,227 @@
|
||||
#include <Tactility/app/btpeersettings/BtPeerSettings.h>
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/LogMessages.h>
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/app/AppContext.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/drivers/bluetooth.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::app::btpeersettings {
|
||||
|
||||
static const auto LOGGER = Logger("BtPeerSettings");
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
void start(const std::string& addrHex) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
bundle->putString("addr", addrHex);
|
||||
app::start(manifest.appId, bundle);
|
||||
}
|
||||
|
||||
class BtPeerSettings : public App {
|
||||
|
||||
bool viewEnabled = false;
|
||||
lv_obj_t* connectButton = nullptr;
|
||||
lv_obj_t* disconnectButton = nullptr;
|
||||
std::string addrHex;
|
||||
std::array<uint8_t, 6> addr = {};
|
||||
int profileId = BT_PROFILE_HID_HOST;
|
||||
bool isCurrentlyConnected() const {
|
||||
for (const auto& p : bluetooth::getPairedPeers()) {
|
||||
if (p.addr == addr) return p.connected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void onPressConnect(lv_event_t* event) {
|
||||
auto* self = static_cast<BtPeerSettings*>(lv_event_get_user_data(event));
|
||||
if (self->profileId == BT_PROFILE_HID_HOST) {
|
||||
bluetooth::hidHostConnect(self->addr);
|
||||
} else {
|
||||
bluetooth::connect(self->addr, self->profileId);
|
||||
}
|
||||
lv_obj_add_state(lv_event_get_target_obj(event), LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
static void onPressDisconnect(lv_event_t* event) {
|
||||
auto* self = static_cast<BtPeerSettings*>(lv_event_get_user_data(event));
|
||||
if (self->profileId == BT_PROFILE_HID_HOST) {
|
||||
bluetooth::hidHostDisconnect();
|
||||
} else {
|
||||
bluetooth::disconnect(self->addr, self->profileId);
|
||||
}
|
||||
lv_obj_add_state(lv_event_get_target_obj(event), LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
static void onPressForget(lv_event_t* event) {
|
||||
std::vector<std::string> choices = { "Yes", "No" };
|
||||
alertdialog::start("Confirmation", "Forget this device?", choices);
|
||||
}
|
||||
|
||||
static void onToggleAutoConnect(lv_event_t* event) {
|
||||
auto* self = static_cast<BtPeerSettings*>(lv_event_get_user_data(event));
|
||||
bool is_on = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED);
|
||||
bluetooth::settings::PairedDevice device;
|
||||
if (bluetooth::settings::load(self->addrHex, device)) {
|
||||
device.autoConnect = is_on;
|
||||
if (!bluetooth::settings::save(device)) {
|
||||
LOGGER.error("Failed to save auto-connect setting");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requestViewUpdate() const {
|
||||
if (viewEnabled) {
|
||||
if (lvgl::lock(1000)) {
|
||||
updateViews();
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateViews() const {
|
||||
if (isCurrentlyConnected()) {
|
||||
lv_obj_remove_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(connectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_state(disconnectButton, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(connectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_state(connectButton, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& app) override {
|
||||
const auto parameters = app.getParameters();
|
||||
check(parameters != nullptr, "Parameters missing");
|
||||
addrHex = parameters->getString("addr");
|
||||
|
||||
// Load addr and profileId from stored settings — avoids manual hex parsing
|
||||
// (std::stoul throws on invalid input and exceptions are disabled).
|
||||
bluetooth::settings::PairedDevice device;
|
||||
if (bluetooth::settings::load(addrHex, device)) {
|
||||
addr = device.addr;
|
||||
profileId = device.profileId;
|
||||
}
|
||||
}
|
||||
|
||||
static void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent /*event*/) {
|
||||
static_cast<BtPeerSettings*>(context)->requestViewUpdate();
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
if (struct Device* dev = bluetooth::findFirstDevice()) {
|
||||
bluetooth_add_event_callback(dev, this, onKernelBtEvent);
|
||||
}
|
||||
|
||||
// Load stored settings (name, autoConnect)
|
||||
bluetooth::settings::PairedDevice device;
|
||||
bool deviceLoaded = bluetooth::settings::load(addrHex, device);
|
||||
std::string title = (deviceLoaded && !device.name.empty()) ? device.name : addrHex;
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lvgl::toolbar_create(parent, title);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
|
||||
connectButton = lv_button_create(wrapper);
|
||||
lv_obj_set_width(connectButton, LV_PCT(100));
|
||||
lv_obj_add_event_cb(connectButton, onPressConnect, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* connect_label = lv_label_create(connectButton);
|
||||
lv_obj_align(connect_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(connect_label, "Connect");
|
||||
|
||||
disconnectButton = lv_button_create(wrapper);
|
||||
lv_obj_set_width(disconnectButton, LV_PCT(100));
|
||||
lv_obj_add_event_cb(disconnectButton, onPressDisconnect, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* disconnect_label = lv_label_create(disconnectButton);
|
||||
lv_obj_align(disconnect_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(disconnect_label, "Disconnect");
|
||||
|
||||
auto* forget_button = lv_button_create(wrapper);
|
||||
lv_obj_set_width(forget_button, LV_PCT(100));
|
||||
lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* forget_label = lv_label_create(forget_button);
|
||||
lv_obj_align(forget_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(forget_label, "Forget");
|
||||
|
||||
// Auto-connect toggle row
|
||||
auto* auto_connect_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_bg_invisible(auto_connect_wrapper);
|
||||
lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* auto_connect_label = lv_label_create(auto_connect_wrapper);
|
||||
lv_label_set_text(auto_connect_label, "Auto-connect");
|
||||
lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper);
|
||||
lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
|
||||
if (deviceLoaded && device.autoConnect) {
|
||||
lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
viewEnabled = true;
|
||||
updateViews();
|
||||
}
|
||||
|
||||
void onHide(AppContext& app) override {
|
||||
if (struct Device* dev = bluetooth::findFirstDevice()) {
|
||||
bluetooth_remove_event_callback(dev, onKernelBtEvent);
|
||||
}
|
||||
viewEnabled = false;
|
||||
}
|
||||
|
||||
void onResult(AppContext& appContext, LaunchId /*launchId*/, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
if (result != Result::Ok || bundle == nullptr) return;
|
||||
if (alertdialog::getResultIndex(*bundle) != 0) return; // 0 = Yes
|
||||
|
||||
// Disconnect first if connected
|
||||
if (isCurrentlyConnected()) {
|
||||
if (profileId == BT_PROFILE_HID_HOST) {
|
||||
bluetooth::hidHostDisconnect();
|
||||
} else {
|
||||
bluetooth::disconnect(addr, profileId);
|
||||
}
|
||||
}
|
||||
|
||||
bluetooth::unpair(addr);
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.appId = "BtPeerSettings",
|
||||
.appName = "BT Device Settings",
|
||||
.appCategory = Category::System,
|
||||
.appFlags = AppManifest::Flags::Hidden,
|
||||
.createApp = create<BtPeerSettings>
|
||||
};
|
||||
|
||||
} // namespace tt::app::btpeersettings
|
||||
Reference in New Issue
Block a user