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
-3
@@ -1,3 +1,3 @@
|
|||||||
[submodule "open-x4-sdk"]
|
[submodule "freeink-sdk"]
|
||||||
path = open-x4-sdk
|
path = freeink-sdk
|
||||||
url = https://github.com/crosspoint-reader/community-sdk.git
|
url = https://github.com/Free-Ink/freeink-sdk.git
|
||||||
|
|||||||
Submodule
+1
Submodule freeink-sdk added at b02696589f
@@ -285,6 +285,26 @@ STR_HW_BACK_LABEL: "Back (1st button)"
|
|||||||
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
|
||||||
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
STR_HW_LEFT_LABEL: "Left (3rd button)"
|
||||||
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
STR_HW_RIGHT_LABEL: "Right (4th button)"
|
||||||
|
STR_BLUETOOTH: "Bluetooth"
|
||||||
|
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
|
||||||
|
STR_BT_SCAN_PAIR: "Scan & Pair"
|
||||||
|
STR_BT_NO_DEVICES: "No devices found"
|
||||||
|
STR_BT_DISCONNECT: "Disconnect"
|
||||||
|
STR_BT_CONNECTED_TO: "Connected: %s"
|
||||||
|
STR_BT_NOT_CONNECTED: "Not connected"
|
||||||
|
STR_BT_PAIRED_DEVICES: "Paired Devices"
|
||||||
|
STR_BT_NO_PAIRED: "No paired devices"
|
||||||
|
STR_BT_MAP_BUTTONS: "Map Remote Buttons"
|
||||||
|
STR_BT_PRESS_REMOTE: "Press a button on your remote"
|
||||||
|
STR_BT_ASSIGN_PROMPT: "Select what this button does"
|
||||||
|
STR_BT_PRESET_FREE2: "Apply Free2 Preset"
|
||||||
|
STR_BT_PRESET_FREE3: "Apply Free3 Preset"
|
||||||
|
STR_BT_CLEAR_MAP: "Clear Button Mappings"
|
||||||
|
STR_BT_PAIRING_PASSKEY: "Passkey: %u"
|
||||||
|
STR_BT_PAGE_FORWARD: "Page Forward"
|
||||||
|
STR_BT_PAGE_BACK: "Page Back"
|
||||||
|
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
|
||||||
|
STR_BT_MAP_FULL: "Mapping table full - clear mappings first"
|
||||||
STR_GO_TO_PERCENT: "Go to %"
|
STR_GO_TO_PERCENT: "Go to %"
|
||||||
STR_GO_HOME_BUTTON: "Go Home"
|
STR_GO_HOME_BUTTON: "Go Home"
|
||||||
STR_SYNC_PROGRESS: "Sync Progress"
|
STR_SYNC_PROGRESS: "Sync Progress"
|
||||||
|
|||||||
-1
Submodule open-x4-sdk deleted from 198ad26721
+19
-4
@@ -38,6 +38,16 @@ build_flags =
|
|||||||
-Wno-bidi-chars
|
-Wno-bidi-chars
|
||||||
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
|
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
|
||||||
-fno-exceptions
|
-fno-exceptions
|
||||||
|
# FreeInk panel profiles: compile both X3 (792x528/UC8253) and X4 (800x480/SSD1677);
|
||||||
|
# the firmware picks the active one at runtime via HalDisplay setDisplayX3().
|
||||||
|
-DFREEINK_DEVICE_X3=1
|
||||||
|
-DFREEINK_DEVICE_X4=1
|
||||||
|
# BLE HID page-turner host (BleKeyboardHost). NimBLE role/bond config is baked into
|
||||||
|
# the prebuilt arduino-esp32 framework's sdkconfig.h, so we don't redefine it here
|
||||||
|
# (doing so only warns and has no effect). The host only compiles when
|
||||||
|
# FREEINK_CAP_BLE_HID_HOST is set on the env (below); with the capability off,
|
||||||
|
# BleKeyboardHost links stubs and pulls in zero NimBLE code.
|
||||||
|
-DFREEINK_BLE_HID_SHOW_UNNAMED_DEVICES=0
|
||||||
|
|
||||||
build_unflags =
|
build_unflags =
|
||||||
-std=gnu++11
|
-std=gnu++11
|
||||||
@@ -57,10 +67,12 @@ extra_scripts =
|
|||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
lib_deps =
|
lib_deps =
|
||||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
BatteryMonitor=symlink://freeink-sdk/libs/hardware/BatteryMonitor
|
||||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
InputManager=symlink://freeink-sdk/libs/hardware/InputManager
|
||||||
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
|
||||||
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
|
||||||
|
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
|
||||||
|
h2zero/NimBLE-Arduino @ ^2.3.8
|
||||||
bblanchon/ArduinoJson @ 7.4.2
|
bblanchon/ArduinoJson @ 7.4.2
|
||||||
ricmoo/QRCode @ 0.0.1
|
ricmoo/QRCode @ 0.0.1
|
||||||
bitbank2/PNGdec @ 1.1.6
|
bitbank2/PNGdec @ 1.1.6
|
||||||
@@ -74,6 +86,7 @@ build_flags =
|
|||||||
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
|
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
||||||
|
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
|
||||||
|
|
||||||
|
|
||||||
[env:gh_release]
|
[env:gh_release]
|
||||||
@@ -83,6 +96,7 @@ build_flags =
|
|||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=1 ; Set log level to info for release builds
|
-DLOG_LEVEL=1 ; Set log level to info for release builds
|
||||||
|
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
|
||||||
|
|
||||||
[env:gh_release_rc]
|
[env:gh_release_rc]
|
||||||
extends = base
|
extends = base
|
||||||
@@ -91,6 +105,7 @@ build_flags =
|
|||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
||||||
|
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
|
||||||
|
|
||||||
[env:slim]
|
[env:slim]
|
||||||
extends = base
|
extends = base
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#include "BleInput.h"
|
||||||
|
|
||||||
|
#include <HalPowerManager.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace bleinput {
|
||||||
|
|
||||||
|
// NimBLE controller init/deinit hang (interrupt WDT) if run at the 10 MHz low-power
|
||||||
|
// frequency, so force normal CPU speed around both. Centralized here so every caller
|
||||||
|
// (boot restore, settings toggle, reader toggle, sleep) is covered automatically.
|
||||||
|
bool ensureStarted() {
|
||||||
|
HalPowerManager::Lock powerLock;
|
||||||
|
return BleHid.begin(kHostName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full teardown (NimBLE deinit), not just a link drop, so the BLE stack's RAM is
|
||||||
|
// returned to the heap — otherwise memory-hungry work like EPUB inflate can't
|
||||||
|
// allocate even after the user turns Bluetooth off.
|
||||||
|
void stop() {
|
||||||
|
HalPowerManager::Lock powerLock;
|
||||||
|
BleHid.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value) {
|
||||||
|
if (ev.special != freeink::SpecialKey::None) {
|
||||||
|
kind = 0;
|
||||||
|
value = static_cast<uint8_t>(ev.special);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (ev.keycode != 0) {
|
||||||
|
kind = 1;
|
||||||
|
value = ev.keycode;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char* specialName(uint8_t value) {
|
||||||
|
switch (static_cast<freeink::SpecialKey>(value)) {
|
||||||
|
case freeink::SpecialKey::Enter:
|
||||||
|
return "Enter";
|
||||||
|
case freeink::SpecialKey::Backspace:
|
||||||
|
return "Backspace";
|
||||||
|
case freeink::SpecialKey::Tab:
|
||||||
|
return "Tab";
|
||||||
|
case freeink::SpecialKey::Escape:
|
||||||
|
return "Escape";
|
||||||
|
case freeink::SpecialKey::Delete:
|
||||||
|
return "Delete";
|
||||||
|
case freeink::SpecialKey::Left:
|
||||||
|
return "Left";
|
||||||
|
case freeink::SpecialKey::Right:
|
||||||
|
return "Right";
|
||||||
|
case freeink::SpecialKey::Up:
|
||||||
|
return "Up";
|
||||||
|
case freeink::SpecialKey::Down:
|
||||||
|
return "Down";
|
||||||
|
case freeink::SpecialKey::Home:
|
||||||
|
return "Home";
|
||||||
|
case freeink::SpecialKey::End:
|
||||||
|
return "End";
|
||||||
|
case freeink::SpecialKey::PageUp:
|
||||||
|
return "Page Up";
|
||||||
|
case freeink::SpecialKey::PageDown:
|
||||||
|
return "Page Down";
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen) {
|
||||||
|
if (!out || outLen == 0) return;
|
||||||
|
if (kind == 0) {
|
||||||
|
const char* name = specialName(value);
|
||||||
|
if (name) {
|
||||||
|
strncpy(out, name, outLen - 1);
|
||||||
|
out[outLen - 1] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Printable ASCII usage handled as a generic key code; show the raw value.
|
||||||
|
snprintf(out, outLen, "Key 0x%02X", static_cast<unsigned>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace bleinput
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// CrossPoint <-> FreeInk BLE HID host glue.
|
||||||
|
//
|
||||||
|
// Thin, capability-safe helpers around freeink::BleKeyboardHost (the `BleHid`
|
||||||
|
// singleton). When FREEINK_CAP_BLE_HID_HOST is compiled out the SDK links stubs,
|
||||||
|
// so every call here is still valid and simply no-ops / returns false — callers
|
||||||
|
// need no #ifdefs.
|
||||||
|
//
|
||||||
|
// The (kind, value) pair produced by encodeKey() is the stable identity stored in
|
||||||
|
// CrossPointSettings::bleKeyMap. Page-turner remotes emit "special" keys
|
||||||
|
// (PageUp/PageDown/arrows); plain keyboards emit usage codes. We deliberately
|
||||||
|
// ignore modifiers and the printable char for matching (page turners don't use
|
||||||
|
// modifiers), keeping the persisted entry a trivial two-byte comparison.
|
||||||
|
|
||||||
|
#include <BleKeyboardHost.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace bleinput {
|
||||||
|
|
||||||
|
// Advertised central name shown to peripherals during pairing.
|
||||||
|
inline constexpr const char* kHostName = "CrossPoint";
|
||||||
|
|
||||||
|
// Start the BLE HID host (idempotent). Returns false if BLE is compiled out or
|
||||||
|
// NimBLE init failed. Safe to call repeatedly.
|
||||||
|
bool ensureStarted();
|
||||||
|
|
||||||
|
// Drop the active link (e.g. before deep sleep or when the user disables BT).
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
// Encode a decoded key event into the stable (kind, value) identity used by the
|
||||||
|
// settings map. kind: 0 = SpecialKey, 1 = HID usage. Returns false when the event
|
||||||
|
// carries no usable identity (no special key and no usage code).
|
||||||
|
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value);
|
||||||
|
|
||||||
|
// Human-readable name for a stored (kind, value) identity, for the mapping UI.
|
||||||
|
// Writes a null-terminated string into out (e.g. "Page Down", "Key 0x4B").
|
||||||
|
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen);
|
||||||
|
|
||||||
|
} // namespace bleinput
|
||||||
@@ -216,6 +216,22 @@ class CrossPointSettings {
|
|||||||
uint8_t frontButtonConfirm = FRONT_HW_CONFIRM;
|
uint8_t frontButtonConfirm = FRONT_HW_CONFIRM;
|
||||||
uint8_t frontButtonLeft = FRONT_HW_LEFT;
|
uint8_t frontButtonLeft = FRONT_HW_LEFT;
|
||||||
uint8_t frontButtonRight = FRONT_HW_RIGHT;
|
uint8_t frontButtonRight = FRONT_HW_RIGHT;
|
||||||
|
// --- Bluetooth (BLE HID page-turner) ---
|
||||||
|
// Master on/off for the BLE HID host. Persisted; auto-restored on boot/wake.
|
||||||
|
// Managed by BluetoothSettingsActivity and the in-reader "Toggle Bluetooth" menu item.
|
||||||
|
uint8_t bluetoothEnabled = 0;
|
||||||
|
// Remote-button mapping table: each slot binds a decoded BLE key identity to a
|
||||||
|
// logical MappedInputManager::Button. Fixed-capacity POD (no heap), persisted
|
||||||
|
// manually in JsonSettingsIO (like the front-button remap). 0xFF = empty/unassigned.
|
||||||
|
// Headroom for several buttons plus optional presets and rolling-code remotes
|
||||||
|
// (some buttons emit more than one code). Each entry is 3 bytes.
|
||||||
|
static constexpr uint8_t BLE_MAP_CAPACITY = 10;
|
||||||
|
struct BleKeyMapEntry {
|
||||||
|
uint8_t keyKind = 0xFF; // 0 = SpecialKey, 1 = HID usage code; 0xFF = empty slot
|
||||||
|
uint8_t keyValue = 0; // (uint8_t)freeink::SpecialKey, or the raw HID usage id
|
||||||
|
uint8_t button = 0xFF; // (uint8_t)MappedInputManager::Button; 0xFF = unassigned
|
||||||
|
};
|
||||||
|
BleKeyMapEntry bleKeyMap[BLE_MAP_CAPACITY] = {};
|
||||||
// Reader font settings
|
// Reader font settings
|
||||||
uint8_t fontFamily = NOTOSERIF;
|
uint8_t fontFamily = NOTOSERIF;
|
||||||
uint8_t fontSize = MEDIUM;
|
uint8_t fontSize = MEDIUM;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "BookmarkEntry.h"
|
#include "BookmarkEntry.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
#include "OpdsServerStore.h"
|
#include "OpdsServerStore.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "SettingsList.h"
|
#include "SettingsList.h"
|
||||||
@@ -142,6 +143,16 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
|
|||||||
doc["frontButtonConfirm"] = s.frontButtonConfirm;
|
doc["frontButtonConfirm"] = s.frontButtonConfirm;
|
||||||
doc["frontButtonLeft"] = s.frontButtonLeft;
|
doc["frontButtonLeft"] = s.frontButtonLeft;
|
||||||
doc["frontButtonRight"] = s.frontButtonRight;
|
doc["frontButtonRight"] = s.frontButtonRight;
|
||||||
|
// Bluetooth — managed by BluetoothSettingsActivity, not in SettingsList.
|
||||||
|
doc["bluetoothEnabled"] = s.bluetoothEnabled;
|
||||||
|
JsonArray bleMap = doc["bleKeyMap"].to<JsonArray>();
|
||||||
|
for (const auto& e : s.bleKeyMap) {
|
||||||
|
if (e.keyKind == 0xFF || e.button == 0xFF) continue; // skip empty/unassigned slots
|
||||||
|
JsonObject o = bleMap.add<JsonObject>();
|
||||||
|
o["k"] = e.keyKind;
|
||||||
|
o["v"] = e.keyValue;
|
||||||
|
o["b"] = e.button;
|
||||||
|
}
|
||||||
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
|
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
|
||||||
doc["fontFamily"] = s.fontFamily;
|
doc["fontFamily"] = s.fontFamily;
|
||||||
// SD card font family name — not in SettingsList, save manually
|
// SD card font family name — not in SettingsList, save manually
|
||||||
@@ -243,6 +254,23 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
|
|||||||
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
|
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
|
||||||
CrossPointSettings::validateFrontButtonMapping(s);
|
CrossPointSettings::validateFrontButtonMapping(s);
|
||||||
|
|
||||||
|
// Bluetooth — managed by BluetoothSettingsActivity, not in SettingsList.
|
||||||
|
s.bluetoothEnabled = clamp(doc["bluetoothEnabled"] | (uint8_t)0, 2, 0);
|
||||||
|
for (auto& e : s.bleKeyMap) e = CrossPointSettings::BleKeyMapEntry{}; // reset to empty
|
||||||
|
JsonArrayConst bleMap = doc["bleKeyMap"];
|
||||||
|
if (!bleMap.isNull()) {
|
||||||
|
uint8_t slot = 0;
|
||||||
|
for (JsonObjectConst o : bleMap) {
|
||||||
|
if (slot >= CrossPointSettings::BLE_MAP_CAPACITY) break;
|
||||||
|
const uint8_t button = o["b"] | (uint8_t)0xFF;
|
||||||
|
if (button >= MappedInputManager::kButtonCount) continue; // drop invalid mappings
|
||||||
|
s.bleKeyMap[slot].keyKind = o["k"] | (uint8_t)0xFF;
|
||||||
|
s.bleKeyMap[slot].keyValue = o["v"] | (uint8_t)0;
|
||||||
|
s.bleKeyMap[slot].button = button;
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
|
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
|
||||||
const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0;
|
const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0;
|
||||||
s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0);
|
s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include "BleInput.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
|
||||||
bool MappedInputManager::isNavDirectionSwapped() const {
|
bool MappedInputManager::isNavDirectionSwapped() const {
|
||||||
@@ -74,11 +76,105 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
|
bool MappedInputManager::bleEdge(const bool* arr, const Button button) const {
|
||||||
|
// Mirror mapButton()'s composite navigation handling so a BLE key bound to a
|
||||||
|
// physical direction also satisfies the derived NavNext / NavPrevious logical
|
||||||
|
// buttons (used by list navigation), respecting the orientation axis flip.
|
||||||
|
switch (button) {
|
||||||
|
case Button::NavNext:
|
||||||
|
return isNavDirectionSwapped() ? (arr[(int)Button::Up] || arr[(int)Button::Left])
|
||||||
|
: (arr[(int)Button::Down] || arr[(int)Button::Right]);
|
||||||
|
case Button::NavPrevious:
|
||||||
|
return isNavDirectionSwapped() ? (arr[(int)Button::Down] || arr[(int)Button::Right])
|
||||||
|
: (arr[(int)Button::Up] || arr[(int)Button::Left]);
|
||||||
|
default:
|
||||||
|
return arr[(int)button];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
|
bool MappedInputManager::wasPressed(const Button button) const {
|
||||||
|
return mapButton(button, &HalGPIO::wasPressed) || bleEdge(blePressEdge, button);
|
||||||
|
}
|
||||||
|
|
||||||
bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
|
bool MappedInputManager::wasReleased(const Button button) const {
|
||||||
|
return mapButton(button, &HalGPIO::wasReleased) || bleEdge(bleReleaseEdge, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MappedInputManager::isPressed(const Button button) const {
|
||||||
|
// A BLE tap is momentary: report "pressed" only on the press-edge frame.
|
||||||
|
return mapButton(button, &HalGPIO::isPressed) || bleEdge(blePressEdge, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappedInputManager::setBleCaptureMode(const bool on) {
|
||||||
|
bleCaptureMode = on;
|
||||||
|
bleHasCaptured = false;
|
||||||
|
if (on) {
|
||||||
|
// Clear any stale overlay so a held remote key doesn't leak into the UI.
|
||||||
|
for (uint8_t i = 0; i < kButtonCount; i++) {
|
||||||
|
blePressEdge[i] = false;
|
||||||
|
bleReleaseEdge[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MappedInputManager::takeCapturedBleKey(uint8_t& kind, uint8_t& value) {
|
||||||
|
if (!bleHasCaptured) return false;
|
||||||
|
kind = bleCapturedKind;
|
||||||
|
value = bleCapturedValue;
|
||||||
|
bleHasCaptured = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappedInputManager::pollBle() {
|
||||||
|
bleActivityThisFrame = false;
|
||||||
|
// Age last frame's press edges into this frame's release edges (the FreeInk host
|
||||||
|
// surfaces presses + synthetic repeats but never releases), then clear presses.
|
||||||
|
for (uint8_t i = 0; i < kButtonCount; i++) {
|
||||||
|
bleReleaseEdge[i] = blePressEdge[i];
|
||||||
|
blePressEdge[i] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
freeink::KeyEvent ev;
|
||||||
|
while (BleHid.popKey(ev)) {
|
||||||
|
uint8_t kind = 0xFF;
|
||||||
|
uint8_t value = 0;
|
||||||
|
const bool encoded = bleinput::encodeKey(ev, kind, value);
|
||||||
|
// TEMP page-turner bring-up: log every decoded BLE key so the actual keycodes
|
||||||
|
// a remote sends are visible on serial. Remove once mapping is verified.
|
||||||
|
LOG_DBG("BLE", "key ch=%d code=0x%02X special=%u -> kind=%u val=0x%02X enc=%d", ev.ch, ev.keycode,
|
||||||
|
(unsigned)ev.special, kind, value, encoded);
|
||||||
|
if (!encoded) continue;
|
||||||
|
|
||||||
|
if (bleCaptureMode) {
|
||||||
|
bleCapturedKind = kind;
|
||||||
|
bleCapturedValue = value;
|
||||||
|
bleHasCaptured = true;
|
||||||
|
LOG_DBG("BLE", "captured (capture mode) kind=%u val=0x%02X", kind, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the key identity against the persisted mapping table.
|
||||||
|
bool matched = false;
|
||||||
|
for (const auto& e : SETTINGS.bleKeyMap) {
|
||||||
|
if (e.button == 0xFF || e.keyKind != kind || e.keyValue != value) continue;
|
||||||
|
if (e.button < kButtonCount) {
|
||||||
|
blePressEdge[e.button] = true;
|
||||||
|
bleActivityThisFrame = true;
|
||||||
|
matched = true;
|
||||||
|
LOG_DBG("BLE", "matched -> logical button %u", e.button);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
LOG_DBG("BLE", "NO MATCH for kind=%u val=0x%02X; current map:", kind, value);
|
||||||
|
for (uint8_t i = 0; i < CrossPointSettings::BLE_MAP_CAPACITY; i++) {
|
||||||
|
const auto& e = SETTINGS.bleKeyMap[i];
|
||||||
|
if (e.button == 0xFF) continue;
|
||||||
|
LOG_DBG("BLE", " slot %u: kind=%u val=0x%02X -> button %u", i, e.keyKind, e.keyValue, e.button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
|
bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class GfxRenderer;
|
|||||||
class MappedInputManager {
|
class MappedInputManager {
|
||||||
public:
|
public:
|
||||||
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
|
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
|
||||||
|
// Number of values in Button (Back..NavPrevious). Used to size the BLE overlay and
|
||||||
|
// to clamp persisted BLE mappings. Keep in sync with the enum above.
|
||||||
|
static constexpr uint8_t kButtonCount = 11;
|
||||||
|
|
||||||
struct Labels {
|
struct Labels {
|
||||||
const char* btn1;
|
const char* btn1;
|
||||||
@@ -28,6 +31,23 @@ class MappedInputManager {
|
|||||||
// Returns the raw front button index that was pressed this frame (or -1 if none).
|
// Returns the raw front button index that was pressed this frame (or -1 if none).
|
||||||
int getPressedFrontButton() const;
|
int getPressedFrontButton() const;
|
||||||
|
|
||||||
|
// --- BLE page-turner overlay -------------------------------------------------
|
||||||
|
// Drain decoded key events from the FreeInk BLE HID host and translate the ones
|
||||||
|
// bound in SETTINGS.bleKeyMap into per-frame logical-button edges that OR into
|
||||||
|
// wasPressed()/isPressed()/wasReleased(). Call once per main-loop iteration,
|
||||||
|
// right after gpio.update() and BleHid.poll(). No-ops when BLE is compiled out.
|
||||||
|
void pollBle();
|
||||||
|
// True when a mapped BLE key produced an edge this frame — keeps the inactivity
|
||||||
|
// / auto-sleep timer alive while a remote is the only input device in use.
|
||||||
|
bool bleHadActivityThisFrame() const { return bleActivityThisFrame; }
|
||||||
|
// Capture mode: while on, pollBle() stops mapping events and instead stashes the
|
||||||
|
// raw decoded key identity so the button-mapping UI can read it without racing the
|
||||||
|
// live mapping over the single popKey() queue.
|
||||||
|
void setBleCaptureMode(bool on);
|
||||||
|
// Pop a captured (kind, value) key identity grabbed while in capture mode.
|
||||||
|
// Returns false when nothing has been captured since the last call.
|
||||||
|
bool takeCapturedBleKey(uint8_t& kind, uint8_t& value);
|
||||||
|
|
||||||
// True when the control axis is flipped relative to the physical buttons: the user opted into
|
// True when the control axis is flipped relative to the physical buttons: the user opted into
|
||||||
// orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED /
|
// orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED /
|
||||||
// LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting,
|
// LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting,
|
||||||
@@ -44,4 +64,17 @@ class MappedInputManager {
|
|||||||
const GfxRenderer& renderer;
|
const GfxRenderer& renderer;
|
||||||
|
|
||||||
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
|
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
|
||||||
|
// OR-in the BLE overlay for a logical button, mirroring mapButton()'s composite
|
||||||
|
// handling of NavNext/NavPrevious so a remote key bound to Up/Down/Left/Right also
|
||||||
|
// drives list navigation.
|
||||||
|
bool bleEdge(const bool* arr, Button button) const;
|
||||||
|
|
||||||
|
// Per-frame BLE overlay, indexed by (uint8_t)Button.
|
||||||
|
bool blePressEdge[kButtonCount] = {}; // press edge this frame -> wasPressed / isPressed
|
||||||
|
bool bleReleaseEdge[kButtonCount] = {}; // release edge this frame -> wasReleased
|
||||||
|
bool bleActivityThisFrame = false;
|
||||||
|
bool bleCaptureMode = false;
|
||||||
|
bool bleHasCaptured = false;
|
||||||
|
uint8_t bleCapturedKind = 0xFF;
|
||||||
|
uint8_t bleCapturedValue = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "BleInput.h"
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@@ -22,7 +24,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
|||||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
||||||
bool hasBookmarks) {
|
bool hasBookmarks) {
|
||||||
std::vector<MenuItem> items;
|
std::vector<MenuItem> items;
|
||||||
items.reserve(12);
|
items.reserve(13);
|
||||||
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
||||||
if (hasFootnotes) {
|
if (hasFootnotes) {
|
||||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
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::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||||
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
|
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
|
||||||
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
|
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::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
||||||
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
||||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||||
@@ -76,6 +79,19 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
return;
|
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});
|
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
@@ -125,6 +141,9 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
|
|||||||
} else if (value == MenuAction::AUTO_PAGE_TURN) {
|
} else if (value == MenuAction::AUTO_PAGE_TURN) {
|
||||||
// Render current page turn value on the right edge of the content area.
|
// Render current page turn value on the right edge of the content area.
|
||||||
return pageTurnLabels[selectedPageTurnOption];
|
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 {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class EpubReaderMenuActivity final : public Activity {
|
|||||||
DISPLAY_QR,
|
DISPLAY_QR,
|
||||||
GO_HOME,
|
GO_HOME,
|
||||||
SYNC,
|
SYNC,
|
||||||
DELETE_CACHE
|
DELETE_CACHE,
|
||||||
|
TOGGLE_BLUETOOTH
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
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 <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "BluetoothSettingsActivity.h"
|
||||||
#include "ButtonRemapActivity.h"
|
#include "ButtonRemapActivity.h"
|
||||||
#include "ClearCacheActivity.h"
|
#include "ClearCacheActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@@ -59,6 +60,7 @@ void SettingsActivity::rebuildSettingsLists() {
|
|||||||
// Append device-only ACTION items
|
// Append device-only ACTION items
|
||||||
controlsSettings.insert(controlsSettings.begin(),
|
controlsSettings.insert(controlsSettings.begin(),
|
||||||
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
|
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_WIFI_NETWORKS, SettingAction::Network));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
|
||||||
@@ -264,6 +266,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
case SettingAction::Language:
|
case SettingAction::Language:
|
||||||
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
||||||
break;
|
break;
|
||||||
|
case SettingAction::Bluetooth:
|
||||||
|
startActivityForResult(std::make_unique<BluetoothSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
break;
|
||||||
case SettingAction::None:
|
case SettingAction::None:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ enum class SettingAction {
|
|||||||
SdFirmwareUpdate,
|
SdFirmwareUpdate,
|
||||||
Language,
|
Language,
|
||||||
DownloadFonts,
|
DownloadFonts,
|
||||||
|
Bluetooth,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
|
|||||||
+23
-2
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "BleInput.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
@@ -262,6 +263,10 @@ void enterDeepSleep(bool fromTimeout = false) {
|
|||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop any BLE HID link cleanly so the page-turner sees the disconnect promptly.
|
||||||
|
// bluetoothEnabled persists, so the setting is restored on the next wake.
|
||||||
|
bleinput::stop();
|
||||||
|
|
||||||
halTiltSensor.deepSleep();
|
halTiltSensor.deepSleep();
|
||||||
display.deepSleep();
|
display.deepSleep();
|
||||||
LOG_DBG("MAIN", "Entering deep sleep");
|
LOG_DBG("MAIN", "Entering deep sleep");
|
||||||
@@ -478,6 +483,15 @@ void setup() {
|
|||||||
// Ensure we're not still holding the power button before leaving setup
|
// Ensure we're not still holding the power button before leaving setup
|
||||||
waitForPowerRelease();
|
waitForPowerRelease();
|
||||||
allowSleepAt = millis() + 2000;
|
allowSleepAt = millis() + 2000;
|
||||||
|
|
||||||
|
// Restore Bluetooth if it was enabled before sleep/reboot. Deep-sleep wake is a
|
||||||
|
// full chip reset, so NimBLE starts fresh here and auto-reconnects to the bonded
|
||||||
|
// page-turner. Left uninitialised (zero radio/RAM cost) when the setting is off.
|
||||||
|
// ensureStarted() forces normal CPU frequency internally (NimBLE controller init
|
||||||
|
// hangs at the 10 MHz low-power state).
|
||||||
|
if (SETTINGS.bluetoothEnabled) {
|
||||||
|
bleinput::ensureStarted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
@@ -486,6 +500,8 @@ void loop() {
|
|||||||
static unsigned long lastMemPrint = 0;
|
static unsigned long lastMemPrint = 0;
|
||||||
|
|
||||||
gpio.update();
|
gpio.update();
|
||||||
|
BleHid.poll(); // drive BLE auto-reconnect + key auto-repeat (no-op when BT off)
|
||||||
|
mappedInputManager.pollBle(); // drain BLE keys -> logical-button overlay for this frame
|
||||||
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
|
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
|
||||||
|
|
||||||
renderer.setFadingFix(SETTINGS.fadingFix);
|
renderer.setFadingFix(SETTINGS.fadingFix);
|
||||||
@@ -516,7 +532,7 @@ void loop() {
|
|||||||
// Check for any user activity (button press or release) or active background work
|
// Check for any user activity (button press or release) or active background work
|
||||||
static unsigned long lastActivityTime = millis();
|
static unsigned long lastActivityTime = millis();
|
||||||
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
|
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
|
||||||
activityManager.preventAutoSleep()) {
|
mappedInputManager.bleHadActivityThisFrame() || activityManager.preventAutoSleep()) {
|
||||||
lastActivityTime = millis(); // Reset inactivity timer
|
lastActivityTime = millis(); // Reset inactivity timer
|
||||||
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
|
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
|
||||||
}
|
}
|
||||||
@@ -597,11 +613,16 @@ void loop() {
|
|||||||
powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested
|
powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested
|
||||||
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
yield(); // Give FreeRTOS a chance to run tasks, but return immediately
|
||||||
} else {
|
} else {
|
||||||
if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
|
// The BLE controller cannot run at the 10 MHz low-power frequency — NimBLE's
|
||||||
|
// controller reset/maintenance hangs the radio and trips the interrupt WDT (the
|
||||||
|
// same reason WiFi force-disables power saving in HalPowerManager). Keep full CPU
|
||||||
|
// speed whenever Bluetooth is enabled, regardless of input idleness.
|
||||||
|
if (!SETTINGS.bluetoothEnabled && millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
|
||||||
// If we've been inactive for a while, increase the delay to save power
|
// If we've been inactive for a while, increase the delay to save power
|
||||||
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
|
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
|
||||||
delay(50);
|
delay(50);
|
||||||
} else {
|
} else {
|
||||||
|
if (SETTINGS.bluetoothEnabled) powerManager.setPowerSaving(false); // keep the BLE radio stable
|
||||||
// Short delay to prevent tight loop while still being responsive
|
// Short delay to prevent tight loop while still being responsive
|
||||||
delay(10);
|
delay(10);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user