Add Bluetooth HID remote control support
Integrate BLE keyboard host functionality for page-turner remotes. Adds device pairing, button mapping, preset configurations for Free2/Free3 remotes, and persistent storage of mappings. Migrates from open-x4-sdk to freeink-sdk submodule which includes the BleKeyboardHost library. Implements CPU frequency locking during BLE operations to prevent watchdog timeouts.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "BleInput.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
@@ -22,7 +24,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
||||
bool hasBookmarks) {
|
||||
std::vector<MenuItem> items;
|
||||
items.reserve(12);
|
||||
items.reserve(13);
|
||||
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
||||
if (hasFootnotes) {
|
||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
||||
@@ -36,6 +38,7 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
|
||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
|
||||
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
|
||||
items.push_back({MenuAction::TOGGLE_BLUETOOTH, StrId::STR_TOGGLE_BLUETOOTH});
|
||||
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
||||
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||
@@ -76,6 +79,19 @@ void EpubReaderMenuActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAction == MenuAction::TOGGLE_BLUETOOTH) {
|
||||
// Toggle in place and stay in the menu (no reader re-render needed).
|
||||
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
|
||||
if (SETTINGS.bluetoothEnabled) {
|
||||
bleinput::ensureStarted();
|
||||
} else {
|
||||
bleinput::stop();
|
||||
}
|
||||
SETTINGS.saveToFile();
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
|
||||
finish();
|
||||
return;
|
||||
@@ -125,6 +141,9 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
|
||||
} else if (value == MenuAction::AUTO_PAGE_TURN) {
|
||||
// Render current page turn value on the right edge of the content area.
|
||||
return pageTurnLabels[selectedPageTurnOption];
|
||||
} else if (value == MenuAction::TOGGLE_BLUETOOTH) {
|
||||
// Render current Bluetooth on/off state on the right edge.
|
||||
return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
DISPLAY_QR,
|
||||
GO_HOME,
|
||||
SYNC,
|
||||
DELETE_CACHE
|
||||
DELETE_CACHE,
|
||||
TOGGLE_BLUETOOTH
|
||||
};
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#include "BleButtonMapActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "BleInput.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
// Logical functions offered for binding. Page navigation + confirm cover Free2 /
|
||||
// Free3; the directions are included so a remote can also drive menu navigation.
|
||||
const BleButtonMapActivity::Fn BleButtonMapActivity::kFunctions[] = {
|
||||
{MappedInputManager::Button::PageForward, StrId::STR_BT_PAGE_FORWARD},
|
||||
{MappedInputManager::Button::PageBack, StrId::STR_BT_PAGE_BACK},
|
||||
{MappedInputManager::Button::Confirm, StrId::STR_CONFIRM},
|
||||
{MappedInputManager::Button::Back, StrId::STR_BACK},
|
||||
{MappedInputManager::Button::Up, StrId::STR_DIR_UP},
|
||||
{MappedInputManager::Button::Down, StrId::STR_DIR_DOWN},
|
||||
{MappedInputManager::Button::Left, StrId::STR_DIR_LEFT},
|
||||
{MappedInputManager::Button::Right, StrId::STR_DIR_RIGHT},
|
||||
};
|
||||
const uint8_t BleButtonMapActivity::kFunctionCount =
|
||||
static_cast<uint8_t>(sizeof(kFunctions) / sizeof(kFunctions[0]));
|
||||
|
||||
void BleButtonMapActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
step = Step::WaitForKey;
|
||||
capturedKind = 0xFF;
|
||||
functionIndex = 0;
|
||||
mappedInput.setBleCaptureMode(true);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void BleButtonMapActivity::onExit() {
|
||||
mappedInput.setBleCaptureMode(false);
|
||||
Activity::onExit();
|
||||
}
|
||||
|
||||
bool BleButtonMapActivity::assignCapturedKey(MappedInputManager::Button button) {
|
||||
const uint8_t btn = static_cast<uint8_t>(button);
|
||||
// Update an existing binding for this key, if present.
|
||||
for (auto& e : SETTINGS.bleKeyMap) {
|
||||
if (e.button != 0xFF && e.keyKind == capturedKind && e.keyValue == capturedValue) {
|
||||
e.button = btn;
|
||||
SETTINGS.saveToFile();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Otherwise take a free slot.
|
||||
for (auto& e : SETTINGS.bleKeyMap) {
|
||||
if (e.button == 0xFF || e.keyKind == 0xFF) {
|
||||
e.keyKind = capturedKind;
|
||||
e.keyValue = capturedValue;
|
||||
e.button = btn;
|
||||
SETTINGS.saveToFile();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // table full
|
||||
}
|
||||
|
||||
void BleButtonMapActivity::loop() {
|
||||
// Front Back button exits the mapping screen at any step.
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step == Step::WaitForKey) {
|
||||
uint8_t kind = 0xFF;
|
||||
uint8_t value = 0;
|
||||
if (mappedInput.takeCapturedBleKey(kind, value)) {
|
||||
capturedKind = kind;
|
||||
capturedValue = value;
|
||||
functionIndex = 0;
|
||||
step = Step::SelectFunction;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Step::SelectFunction — pick a logical function for the captured key.
|
||||
buttonNavigator.onNext([this] {
|
||||
functionIndex = ButtonNavigator::nextIndex(functionIndex, kFunctionCount);
|
||||
requestUpdate();
|
||||
});
|
||||
buttonNavigator.onPrevious([this] {
|
||||
functionIndex = ButtonNavigator::previousIndex(functionIndex, kFunctionCount);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (!assignCapturedKey(kFunctions[functionIndex].button)) {
|
||||
// Table full: surface it instead of silently dropping the binding.
|
||||
errorUntil = millis() + 2500;
|
||||
}
|
||||
// Back to capturing so the user can map the next remote button.
|
||||
step = Step::WaitForKey;
|
||||
capturedKind = 0xFF;
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void BleButtonMapActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_BT_MAP_BUTTONS));
|
||||
|
||||
const int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||
|
||||
if (step == Step::WaitForKey) {
|
||||
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
|
||||
tr(STR_BT_PRESS_REMOTE));
|
||||
// Show the current mappings so the user sees progress.
|
||||
int row = 0;
|
||||
for (const auto& e : SETTINGS.bleKeyMap) {
|
||||
if (e.button == 0xFF) continue;
|
||||
char keyName[24];
|
||||
bleinput::describeKey(e.keyKind, e.keyValue, keyName, sizeof(keyName));
|
||||
const char* fnName = "";
|
||||
for (uint8_t i = 0; i < kFunctionCount; i++) {
|
||||
if (static_cast<uint8_t>(kFunctions[i].button) == e.button) {
|
||||
fnName = I18N.get(kFunctions[i].label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
char line[64];
|
||||
snprintf(line, sizeof(line), "%s -> %s", keyName, fnName);
|
||||
GUI.drawHelpText(renderer, Rect{0, topOffset + row * 22, pageWidth, 20}, line);
|
||||
row++;
|
||||
}
|
||||
} else {
|
||||
char captured[24];
|
||||
bleinput::describeKey(capturedKind, capturedValue, captured, sizeof(captured));
|
||||
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
|
||||
captured);
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, topOffset, pageWidth, contentHeight}, kFunctionCount, functionIndex,
|
||||
[this](int i) { return std::string(I18N.get(kFunctions[i].label)); }, nullptr, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
if (errorUntil > millis()) {
|
||||
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20}, tr(STR_BT_MAP_FULL));
|
||||
}
|
||||
|
||||
const char* confirm = step == Step::WaitForKey ? "" : tr(STR_SELECT);
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirm, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
// Capture-then-assign mapping for BLE page-turner buttons. The user presses a
|
||||
// button on the remote; we capture its decoded key identity (via the
|
||||
// MappedInputManager BLE capture mode) and let them bind it to a logical button.
|
||||
// Repeat to map each remote button; Back exits. Mirrors ButtonRemapActivity's
|
||||
// flow, but the input source is the BLE host instead of the front buttons.
|
||||
class BleButtonMapActivity final : public Activity {
|
||||
public:
|
||||
explicit BleButtonMapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("BleButtonMap", renderer, mappedInput) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
// Logical functions a remote button can be bound to.
|
||||
struct Fn {
|
||||
MappedInputManager::Button button;
|
||||
StrId label;
|
||||
};
|
||||
static const Fn kFunctions[];
|
||||
static const uint8_t kFunctionCount;
|
||||
|
||||
enum class Step { WaitForKey, SelectFunction };
|
||||
Step step = Step::WaitForKey;
|
||||
|
||||
uint8_t capturedKind = 0xFF;
|
||||
uint8_t capturedValue = 0;
|
||||
int functionIndex = 0;
|
||||
|
||||
// Transient "mapping table full" banner.
|
||||
unsigned long errorUntil = 0;
|
||||
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
// Bind the captured key to the chosen logical button in SETTINGS.bleKeyMap and
|
||||
// persist. Returns false when the table is full and the key is new.
|
||||
bool assignCapturedKey(MappedInputManager::Button button);
|
||||
};
|
||||
@@ -0,0 +1,313 @@
|
||||
#include "BluetoothSettingsActivity.h"
|
||||
|
||||
#include <BleKeyboardHost.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "BleButtonMapActivity.h"
|
||||
#include "BleInput.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long kBannerMs = 2000;
|
||||
constexpr uint32_t kScanMs = 8000;
|
||||
constexpr unsigned long kForgetHoldMs = 1200; // hold Confirm this long in the Paired view to forget
|
||||
} // namespace
|
||||
|
||||
void BluetoothSettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
view = View::Menu;
|
||||
menuIndex = 0;
|
||||
rebuildMenuRows();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::onExit() {
|
||||
if (BleHid.isScanning()) BleHid.stopScan();
|
||||
Activity::onExit();
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::setBanner(const char* text) {
|
||||
banner = text ? text : "";
|
||||
bannerUntil = millis() + kBannerMs;
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::rebuildMenuRows() {
|
||||
menuRows.clear();
|
||||
menuRows.reserve(8);
|
||||
menuRows.push_back({Action::ToggleBt, StrId::STR_BLUETOOTH});
|
||||
if (SETTINGS.bluetoothEnabled) {
|
||||
menuRows.push_back({Action::Scan, StrId::STR_BT_SCAN_PAIR});
|
||||
if (BleHid.isConnected()) menuRows.push_back({Action::Disconnect, StrId::STR_BT_DISCONNECT});
|
||||
menuRows.push_back({Action::PairedDevices, StrId::STR_BT_PAIRED_DEVICES});
|
||||
menuRows.push_back({Action::MapButtons, StrId::STR_BT_MAP_BUTTONS});
|
||||
menuRows.push_back({Action::PresetFree2, StrId::STR_BT_PRESET_FREE2});
|
||||
menuRows.push_back({Action::PresetFree3, StrId::STR_BT_PRESET_FREE3});
|
||||
menuRows.push_back({Action::ClearMap, StrId::STR_BT_CLEAR_MAP});
|
||||
}
|
||||
if (menuIndex >= static_cast<int>(menuRows.size())) menuIndex = 0;
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::applyPreset(bool free3) {
|
||||
// Starter presets. Page-turner remotes commonly emit PageUp/PageDown (and a
|
||||
// center key on the 3-button Free3); the user can re-map via "Map Remote
|
||||
// Buttons" if their device sends different codes.
|
||||
using Btn = MappedInputManager::Button;
|
||||
for (auto& e : SETTINGS.bleKeyMap) e = CrossPointSettings::BleKeyMapEntry{};
|
||||
auto set = [&](int slot, freeink::SpecialKey key, Btn button) {
|
||||
SETTINGS.bleKeyMap[slot].keyKind = 0; // SpecialKey
|
||||
SETTINGS.bleKeyMap[slot].keyValue = static_cast<uint8_t>(key);
|
||||
SETTINGS.bleKeyMap[slot].button = static_cast<uint8_t>(button);
|
||||
};
|
||||
set(0, freeink::SpecialKey::PageDown, Btn::PageForward);
|
||||
set(1, freeink::SpecialKey::PageUp, Btn::PageBack);
|
||||
if (free3) set(2, freeink::SpecialKey::Enter, Btn::Confirm);
|
||||
SETTINGS.saveToFile();
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::startScanView() {
|
||||
view = View::Scan;
|
||||
scanIndex = 0;
|
||||
awaitingConnect = false;
|
||||
BleHid.startScan(kScanMs);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::handleMenuConfirm() {
|
||||
if (menuRows.empty()) return;
|
||||
const Action action = menuRows[menuIndex].action;
|
||||
switch (action) {
|
||||
case Action::ToggleBt:
|
||||
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
|
||||
if (SETTINGS.bluetoothEnabled) {
|
||||
bleinput::ensureStarted();
|
||||
} else {
|
||||
bleinput::stop();
|
||||
}
|
||||
SETTINGS.saveToFile();
|
||||
rebuildMenuRows();
|
||||
requestUpdate();
|
||||
break;
|
||||
case Action::Scan:
|
||||
startScanView();
|
||||
break;
|
||||
case Action::Disconnect:
|
||||
BleHid.disconnect();
|
||||
setBanner(tr(STR_BT_NOT_CONNECTED));
|
||||
rebuildMenuRows();
|
||||
requestUpdate();
|
||||
break;
|
||||
case Action::PairedDevices:
|
||||
view = View::Paired;
|
||||
pairedIndex = 0;
|
||||
requestUpdate();
|
||||
break;
|
||||
case Action::MapButtons:
|
||||
startActivityForResult(std::make_unique<BleButtonMapActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult&) {
|
||||
rebuildMenuRows();
|
||||
requestUpdate();
|
||||
});
|
||||
break;
|
||||
case Action::PresetFree2:
|
||||
applyPreset(false);
|
||||
setBanner(tr(STR_BT_PRESET_FREE2));
|
||||
requestUpdate();
|
||||
break;
|
||||
case Action::PresetFree3:
|
||||
applyPreset(true);
|
||||
setBanner(tr(STR_BT_PRESET_FREE3));
|
||||
requestUpdate();
|
||||
break;
|
||||
case Action::ClearMap:
|
||||
for (auto& e : SETTINGS.bleKeyMap) e = CrossPointSettings::BleKeyMapEntry{};
|
||||
SETTINGS.saveToFile();
|
||||
setBanner(tr(STR_BT_CLEAR_MAP));
|
||||
requestUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::loop() {
|
||||
// Clear an expired status banner.
|
||||
if (bannerUntil > 0 && millis() > bannerUntil) {
|
||||
banner.clear();
|
||||
bannerUntil = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Watch for an async connect result (from either the scan list or the paired list).
|
||||
if (awaitingConnect) {
|
||||
char reason[48];
|
||||
if (BleHid.isConnected()) {
|
||||
awaitingConnect = false;
|
||||
BleHid.releaseScanResults();
|
||||
view = View::Menu;
|
||||
rebuildMenuRows();
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), tr(STR_BT_CONNECTED_TO), BleHid.connectedName());
|
||||
setBanner(buf);
|
||||
requestUpdate();
|
||||
} else if (BleHid.takeConnectFailure(reason, sizeof(reason))) {
|
||||
awaitingConnect = false;
|
||||
setBanner(reason);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Back returns to the menu from a sub-view, or leaves the screen from the menu.
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
if (view == View::Menu) {
|
||||
finish();
|
||||
} else {
|
||||
if (BleHid.isScanning()) BleHid.stopScan();
|
||||
view = View::Menu;
|
||||
rebuildMenuRows();
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation within the active list.
|
||||
const int count = view == View::Menu ? static_cast<int>(menuRows.size())
|
||||
: view == View::Scan ? BleHid.deviceCount()
|
||||
: BleHid.pairedCount();
|
||||
int* idx = view == View::Menu ? &menuIndex : view == View::Scan ? &scanIndex : &pairedIndex;
|
||||
buttonNavigator.onNext([this, count, idx] {
|
||||
if (count > 0) *idx = ButtonNavigator::nextIndex(*idx, count);
|
||||
requestUpdate();
|
||||
});
|
||||
buttonNavigator.onPrevious([this, count, idx] {
|
||||
if (count > 0) *idx = ButtonNavigator::previousIndex(*idx, count);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
// Paired view: tap Confirm to connect, hold Confirm to forget. Uses release for
|
||||
// connect so a hold can fire forget without also connecting on the same press.
|
||||
if (view == View::Paired) {
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (!pairedActionTaken && mappedInput.getHeldTime() >= kForgetHoldMs && pairedIndex < BleHid.pairedCount()) {
|
||||
const auto& p = BleHid.paired(static_cast<uint8_t>(pairedIndex));
|
||||
BleHid.forget(p.addr);
|
||||
if (pairedIndex > 0) pairedIndex--;
|
||||
setBanner(tr(STR_FORGET_BUTTON));
|
||||
pairedActionTaken = true;
|
||||
rebuildMenuRows();
|
||||
requestUpdate();
|
||||
}
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (!pairedActionTaken && !awaitingConnect && pairedIndex < BleHid.pairedCount()) {
|
||||
const auto& p = BleHid.paired(static_cast<uint8_t>(pairedIndex));
|
||||
awaitingConnect = true;
|
||||
setBanner(tr(STR_CONNECTING));
|
||||
BleHid.connect(p.addr);
|
||||
requestUpdate();
|
||||
}
|
||||
pairedActionTaken = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (view == View::Menu) {
|
||||
handleMenuConfirm();
|
||||
} else if (view == View::Scan) {
|
||||
if (!awaitingConnect && scanIndex < BleHid.deviceCount()) {
|
||||
if (BleHid.isScanning()) BleHid.stopScan();
|
||||
const auto& d = BleHid.device(static_cast<uint8_t>(scanIndex));
|
||||
awaitingConnect = true;
|
||||
setBanner(tr(STR_CONNECTING));
|
||||
BleHid.connect(d.addr);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// The scan list changes as devices are discovered — keep repainting while active.
|
||||
if (view == View::Scan && BleHid.isScanning()) requestUpdate();
|
||||
}
|
||||
|
||||
std::string BluetoothSettingsActivity::deviceLabel(int index) const {
|
||||
if (index >= BleHid.deviceCount()) return "";
|
||||
const auto& d = BleHid.device(static_cast<uint8_t>(index));
|
||||
return std::string(d.name);
|
||||
}
|
||||
|
||||
std::string BluetoothSettingsActivity::pairedLabel(int index) const {
|
||||
if (index >= BleHid.pairedCount()) return "";
|
||||
const auto& p = BleHid.paired(static_cast<uint8_t>(index));
|
||||
return std::string(p.name);
|
||||
}
|
||||
|
||||
void BluetoothSettingsActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
const char* title = tr(STR_BLUETOOTH);
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title);
|
||||
|
||||
// Sub-header: connection status.
|
||||
const char* status = BleHid.isConnected() ? BleHid.connectedName() : tr(STR_BT_NOT_CONNECTED);
|
||||
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
|
||||
status);
|
||||
|
||||
const int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||
const Rect listRect{0, topOffset, pageWidth, contentHeight};
|
||||
|
||||
if (view == View::Menu) {
|
||||
GUI.drawList(
|
||||
renderer, listRect, static_cast<int>(menuRows.size()), menuIndex,
|
||||
[this](int i) { return std::string(I18N.get(menuRows[i].label)); }, nullptr, nullptr,
|
||||
[this](int i) -> std::string {
|
||||
if (menuRows[i].action == Action::ToggleBt) return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF);
|
||||
return "";
|
||||
},
|
||||
true);
|
||||
} else if (view == View::Scan) {
|
||||
const int count = BleHid.deviceCount();
|
||||
if (count == 0) {
|
||||
GUI.drawHelpText(renderer, Rect{0, topOffset + metrics.verticalSpacing, pageWidth, 24},
|
||||
BleHid.isScanning() ? tr(STR_SCANNING) : tr(STR_BT_NO_DEVICES));
|
||||
} else {
|
||||
GUI.drawList(
|
||||
renderer, listRect, count, scanIndex, [this](int i) { return deviceLabel(i); }, nullptr, nullptr, nullptr,
|
||||
false);
|
||||
}
|
||||
} else { // Paired
|
||||
const int count = BleHid.pairedCount();
|
||||
if (count == 0) {
|
||||
GUI.drawHelpText(renderer, Rect{0, topOffset + metrics.verticalSpacing, pageWidth, 24}, tr(STR_BT_NO_PAIRED));
|
||||
} else {
|
||||
GUI.drawList(
|
||||
renderer, listRect, count, pairedIndex, [this](int i) { return pairedLabel(i); }, nullptr, nullptr, nullptr,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
// Transient banner above the hints.
|
||||
if (!banner.empty()) {
|
||||
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20}, banner.c_str());
|
||||
}
|
||||
|
||||
// In the paired list, Confirm connects and a hold forgets — surface the hold hint.
|
||||
if (view == View::Paired && BleHid.pairedCount() > 0 && banner.empty()) {
|
||||
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20},
|
||||
tr(STR_BT_FORGET_PROMPT));
|
||||
}
|
||||
|
||||
// Button hints differ by view (Menu selects; Scan and Paired both connect).
|
||||
const char* confirm = view == View::Menu ? tr(STR_SELECT) : tr(STR_CONNECT);
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirm, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "activities/Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
// Bluetooth page-turner settings. One screen with three views:
|
||||
// Menu — enable/disable BT, scan & pair, disconnect, map buttons, presets.
|
||||
// Scan — live list of discovered BLE HID devices; Confirm connects.
|
||||
// Paired — bonded devices; Confirm forgets the selected one.
|
||||
// All BLE access goes through the FreeInk BleHid singleton; everything no-ops
|
||||
// gracefully when BLE is compiled out (BleHid.begin() returns false).
|
||||
class BluetoothSettingsActivity final : public Activity {
|
||||
public:
|
||||
explicit BluetoothSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("BluetoothSettings", renderer, mappedInput) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
enum class View { Menu, Scan, Paired };
|
||||
|
||||
// Menu row actions.
|
||||
enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices, PresetFree2, PresetFree3, ClearMap };
|
||||
struct MenuRow {
|
||||
Action action;
|
||||
StrId label;
|
||||
};
|
||||
|
||||
View view = View::Menu;
|
||||
std::vector<MenuRow> menuRows;
|
||||
int menuIndex = 0;
|
||||
int scanIndex = 0;
|
||||
int pairedIndex = 0;
|
||||
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
// Transient status banner (connect result, forget confirmation, etc.).
|
||||
std::string banner;
|
||||
unsigned long bannerUntil = 0;
|
||||
|
||||
// Set when a connect() has been issued and we're waiting for the async result.
|
||||
bool awaitingConnect = false;
|
||||
// Guards the Paired view's hold-to-forget so it fires once per hold and suppresses
|
||||
// the tap-to-connect on the same press.
|
||||
bool pairedActionTaken = false;
|
||||
|
||||
void rebuildMenuRows();
|
||||
void handleMenuConfirm();
|
||||
void startScanView();
|
||||
void applyPreset(bool free3);
|
||||
void setBanner(const char* text);
|
||||
|
||||
std::string deviceLabel(int index) const; // scan list row text
|
||||
std::string pairedLabel(int index) const; // paired list row text
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "BluetoothSettingsActivity.h"
|
||||
#include "ButtonRemapActivity.h"
|
||||
#include "ClearCacheActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -59,6 +60,7 @@ void SettingsActivity::rebuildSettingsLists() {
|
||||
// Append device-only ACTION items
|
||||
controlsSettings.insert(controlsSettings.begin(),
|
||||
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
|
||||
controlsSettings.push_back(SettingInfo::Action(StrId::STR_BLUETOOTH, SettingAction::Bluetooth));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
|
||||
@@ -264,6 +266,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
case SettingAction::Language:
|
||||
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::Bluetooth:
|
||||
startActivityForResult(std::make_unique<BluetoothSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::None:
|
||||
// Do nothing
|
||||
break;
|
||||
|
||||
@@ -23,6 +23,7 @@ enum class SettingAction {
|
||||
SdFirmwareUpdate,
|
||||
Language,
|
||||
DownloadFonts,
|
||||
Bluetooth,
|
||||
};
|
||||
|
||||
struct SettingInfo {
|
||||
|
||||
Reference in New Issue
Block a user