-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathBluetoothHIDManager.h
More file actions
273 lines (253 loc) · 13.4 KB
/
Copy pathBluetoothHIDManager.h
File metadata and controls
273 lines (253 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#pragma once
#include <Arduino.h>
#include <string>
#include <vector>
#include <functional>
#include <map>
#include "DeviceProfiles.h"
// Forward declarations
class NimBLEClient;
class NimBLERemoteCharacteristic;
class NimBLEAdvertisedDevice;
struct BluetoothDevice {
std::string address;
std::string name;
int rssi;
bool isHID = false;
};
struct ConnectedDevice {
std::string address;
std::string name;
NimBLEClient* client = nullptr;
std::vector<NimBLERemoteCharacteristic*> reportChars;
unsigned long connectedTime = 0; // Timestamp when BLE link was established
bool subscribed = false;
unsigned long lastActivityTime = 0; // Timestamp of last HID report received
uint8_t lastHIDKeycode = 0x00; // Track last keycode to detect press/release transitions
unsigned long lastInjectionTime = 0; // Cooldown for button injection to prevent flooding
uint8_t lastInjectedKeycode = 0x00; // Track last injected key for smarter cooldown
uint8_t activeInjectedButton = 0xFF; // Currently held virtual button, if any
bool wasConnected = false; // Track if this device was previously connected for auto-reconnect
bool hasSeenRelease = false; // Ignore startup noise until a release frame is seen
bool lastButtonState = false; // Track button pressed state (from byte[0])
const DeviceProfiles::DeviceProfile* profile = nullptr; // Device-specific HID profile
bool simpleFallbackEnabled = false;
uint8_t simpleForwardKeycode = 0x00;
uint8_t simpleBackKeycode = 0x00;
bool descriptorHasConsumerPage = false;
bool descriptorHasKeyboardPage = false;
uint8_t descriptorSuggestedIndex = 0xFF;
unsigned long lastNormalizedEventMs = 0;
uint8_t lastNormalizedKeycode = 0x00;
bool lastNormalizedPressed = false;
uint8_t lastNormalizedDirection = 0xFF; // 0x00=back, 0x01=forward, 0xFF=unknown
uint16_t lastGameBrickCounter = 0xFFFF; // For counter-freeze detection (button vs joystick)
uint8_t lastGameBrickActiveKey = 0x00; // Latched first key per freeze-window (prevents overshoot misfires)
uint8_t gameBrickCenterPressFrames = 0; // Centered horizontal active-frame streak (LEFT fallback)
bool pendingGameBrickRelease = false; // Delay short A/B release tails so one long hold stays merged
unsigned long pendingGameBrickReleaseMs = 0;
uint8_t pendingGameBrickKeycode = 0x00;
uint8_t pendingGameBrickButton = 0xFF;
};
class BluetoothHIDManager {
public:
// Singleton access
static BluetoothHIDManager& getInstance();
// Lifecycle
bool enable();
bool disable();
bool isEnabled() const { return _enabled; }
// v18.9.9.48: true after a disable() that skipped NimBLE deinit.
// The reader's BT-enable pre-flight should force a silent-restart
// rather than trying to re-enable on top of the stale in-memory state.
bool nimbleStateSkippedTeardown() const { return _nimbleStateSkippedTeardown; }
// Deferred disable. EpubReaderActivity::onExit sets this; the main loop
// drains it after activityManager.loop() releases the render lock — calling
// disable() inline from onExit trips an assertion because NimBLE teardown
// can fire callbacks that call requestUpdateAndWait() while the lock is
// still held.
void requestDisableLater() {
_disableLaterRequested = true;
_enableLaterRequested = false; // a fresh disable cancels any pending (retrying) enable
}
bool isDisableLaterRequested() const { return _disableLaterRequested; }
bool tryDisableIfRequested() {
if (!_disableLaterRequested) return false;
_disableLaterRequested = false;
return disable();
}
// Deferred enable. Paired with requestDisableLater() for the case where we
// temporarily drop BLE around a heap-heavy operation (full chapter
// re-layout under font/margin change) and want it back online afterward.
//
// Drain in the main loop after the heavy operation completes. The drain
// re-enables the stack AND triggers a reconnect to the saved bonded
// remote (using SETTINGS.bleBondedDeviceAddr — the runtime
// _bondedDeviceAddress is not restored across deinit/init), so the user
// doesn't have to press a local button to wake checkAutoReconnect. NimBLE
// init plus connect can block ~2-3 s; safe to call from main loop
// outside any RenderLock.
void requestEnableLater() {
_enableLaterRequested = true;
_disableLaterRequested = false; // a fresh enable cancels any pending disable
// CrumBLE 4.5.6: reset the give-up window each time the caller asks; a
// finished render or chapter change on healthier heap should get a fresh
// budget of retry time, not inherit a stale timestamp.
_enableLaterFirstAttemptMs = 0;
_enableLaterLastAttemptMs = 0;
}
bool tryEnableIfRequested();
// Scanning
void startScan(uint32_t durationMs = 10000);
void stopScan();
bool isScanning() const { return _scanning; }
const std::vector<BluetoothDevice>& getDiscoveredDevices() const { return _discoveredDevices; }
// Connection
bool connectToDevice(const std::string& address);
bool disconnectFromDevice(const std::string& address);
// CrumBLE 4.3: takes const char* (not const std::string&) so calling with
// SETTINGS.bleBondedDeviceAddr (a char[]) doesn't construct a temporary
// std::string. Under post-NimBLE heap pressure on SD-font + BT, that
// string ctor's small-buffer allocation bad_alloc terminated the device
// every reader-loop tick. std::string call sites pass .c_str() instead.
bool isConnected(const char* address) const;
std::vector<std::string> getConnectedDevices() const;
// Input handling
void processInputEvents();
void setInputCallback(std::function<void(uint16_t keycode)> callback);
void setLearnInputCallback(std::function<void(uint8_t keycode, uint8_t reportIndex)> callback);
void setButtonInjector(std::function<void(uint8_t buttonIndex, bool pressed)> injector);
// CrumBLE 4.5.5: rich BLE button map override. Wired by the application at
// boot to consult SETTINGS.bleKeyMap[]. Called from mapKeycodeToButton on
// every HID report; return HalGPIO::BTN_* if (kind, value) is mapped, or
// 0xFF to fall through to device-profile defaults. HAL stays
// CrossPointSettings-free this way.
void setBleKeyMapResolver(std::function<uint8_t(uint8_t kind, uint8_t value)> resolver) {
_bleKeyMapResolver = std::move(resolver);
}
void setReaderContextCallback(std::function<bool()> callback);
void setButtonActivityNotifier(std::function<void(uint8_t buttonIndex)> notifier);
void setDebugCaptureEnabled(bool enabled) { _debugCaptureEnabled = enabled; }
bool isDebugCaptureEnabled() const { return _debugCaptureEnabled; }
void setBondedDevice(const std::string& address, const std::string& name = "");
void updateActivity(); // Call periodically to check inactivity timeout
void checkAutoReconnect(bool userInputDetected = false); // Reconnect bonded device when disconnected
// Clear the auto-reconnect failure backoff. Called on a successful connect
// and whenever the user deliberately acts on BT (enable, scan, pair), so an
// explicit "connect now" is never held off by earlier failures.
void resetAutoReconnectBackoff();
// Check if BLE has had activity recently (within last 4 minutes)
// Used by power manager to prevent sleep during BLE use
bool hasRecentActivity() const;
// v18.9.4: elapsed millis since the newest HID input across all connected
// devices. Returns ULONG_MAX when nothing is connected. Drives the user-
// configurable auto-disconnect timeout in main.cpp.
unsigned long getMillisSinceLastActivity() const;
bool hadRecentFree2Input(unsigned long windowMs = 1500) const;
// State persistence
void saveState();
void loadState();
std::string lastError;
// BLE callbacks (public for NimBLE callbacks)
void onScanResult(NimBLEAdvertisedDevice* advertisedDevice);
// Called from onScanEnd when an async scan finishes; clears _scanning.
void onScanComplete(int reason);
static void onHIDNotify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify);
// CrumBLE: called from the NimBLE disconnect callback with the HCI reason.
// Flags an alert when a link drops on its own shortly after connecting --
// typically the connect spike craters free heap and the controller times the
// link out (HCI 0x08 / reason 520), which otherwise fails silently and leaves
// the user wondering why Bluetooth "didn't connect".
void noteClientDisconnect(int reason);
// Consumes the pending "connection lost" flag (one-shot). The app polls this
// and shows a clear message.
bool takeConnectionLostAlert() {
bool v = _connectionLostAlertPending;
_connectionLostAlertPending = false;
return v;
}
// v18.9.6c: consume the pending "enable-gave-up" flag (one-shot). Set from
// tryEnableIfRequested when its rate-limited retry burns through
// kGiveUpAfterMs of refusals. The reader polls this and can trigger a
// silent-restart-with-EnableBt to recover on a fresh heap.
bool takeEnableGaveUpAlert() {
bool v = _enableGaveUpAlertPending;
_enableGaveUpAlertPending = false;
return v;
}
// CrumBLE 4.4 post-bisect: one-shot auto-reconnect request. Set by
// noteClientDisconnect when an early supervision-timeout drop is
// observed. The reader's loop polls this and calls connectToDevice()
// again to spare the user a manual reconnect. Cleared on consume.
bool takeAutoReconnectRequest() {
bool v = _autoReconnectPending;
_autoReconnectPending = false;
return v;
}
private:
BluetoothHIDManager();
~BluetoothHIDManager();
BluetoothHIDManager(const BluetoothHIDManager&) = delete;
BluetoothHIDManager& operator=(const BluetoothHIDManager&) = delete;
void cleanup();
uint16_t parseHIDReport(uint8_t* data, size_t length);
ConnectedDevice* findConnectedDevice(const std::string& address);
uint8_t mapKeycodeToButton(uint8_t keycode, ConnectedDevice* device);
bool _enabled = false;
// v18.9.9.48 (task #33): set true when disable() skips the crash-prone
// NimBLE deinit. Read by the reader's BT-enable pre-flight check so it
// can force a silent-restart-with-EnableBt instead of trying to
// re-enable on top of stale NimBLE state (which is likely wedged from
// the same corruption that would have crashed deinit).
bool _nimbleStateSkippedTeardown = false;
bool _disableLaterRequested = false;
bool _enableLaterRequested = false;
// CrumBLE 4.5.6: tryEnableIfRequested rate-limit + give-up. Before this,
// a requestEnableLater whose heap floor kept refusing spammed the log
// every ~10 ms forever (once per main-loop tick). Now: no more than one
// retry per 500 ms, and give up entirely after ~6 s of continuous refusal.
// On a book whose steady-state heap can't fit NimBLE, retrying every tick
// can't fix that; it just floods the log. Reset on success or on any new
// requestEnableLater().
unsigned long _enableLaterFirstAttemptMs = 0;
unsigned long _enableLaterLastAttemptMs = 0;
bool _scanning = false;
// CrumBLE: connect-stability tracking for the "couldn't stay connected" alert.
unsigned long _lastConnectMillis = 0; // when a link was last established
bool _intentionalDisconnect = false; // suppress the alert for disconnects we initiate
bool _connectionLostAlertPending = false; // one-shot: a link dropped unexpectedly soon after connecting
bool _enableGaveUpAlertPending = false; // v18.9.6c: one-shot: tryEnableIfRequested burned its retry budget
bool _autoReconnectPending = false; // one-shot: an early drop has fired; reader should retry connect
bool _autoReconnectConsumedThisCycle = false; // gate so we only auto-retry once per enable() cycle
// A drop in the first SETTLE_MS after connect is almost always benign bonding/
// encryption renegotiation -- the link is then re-established cleanly and the
// user never notices unless we surface a (spurious) alert. Drops in
// [SETTLE_MS, EARLY_DISCONNECT_MS] are the real "controller timed the link out
// under heap pressure" case (HCI 0x08 / reason 520) and DO warrant the alert.
// Drops past EARLY_DISCONNECT_MS are treated as a stable link that later
// dropped for an unrelated reason (idle, range, remote off) -- no alert.
static constexpr unsigned long SETTLE_MS = 3000;
static constexpr unsigned long EARLY_DISCONNECT_MS = 10000;
std::vector<BluetoothDevice> _discoveredDevices;
std::vector<ConnectedDevice> _connectedDevices;
std::function<void(uint16_t)> _inputCallback;
std::function<void(uint8_t, uint8_t)> _learnInputCallback;
std::function<void(uint8_t, bool)> _buttonInjector;
std::function<uint8_t(uint8_t, uint8_t)> _bleKeyMapResolver; // 4.5.5: rich-map override
std::function<bool()> _readerContextCallback;
std::function<void(uint8_t)> _buttonActivityNotifier;
bool _debugCaptureEnabled = false;
std::string _bondedDeviceAddress;
// 4.7.4: auto-reconnect backoff. connectToDevice() BLOCKS the main loop for
// 2-3 s when the bonded remote is off or out of range, and the reconnect is
// driven by local button presses -- so a user who owns a page-turner but
// isn't using it right now paid that freeze on button after button. Count
// consecutive failures and push the next attempt out exponentially; a
// successful connect (or a deliberate BT enable) clears it.
uint8_t _reconnectFailures = 0;
unsigned long _nextReconnectAllowedMs = 0;
std::string _bondedDeviceName;
// Inactivity timeout (milliseconds)
static constexpr unsigned long INACTIVITY_TIMEOUT_MS = 300000; // 5 minutes
unsigned long lastMaintenanceCheck = 0;
};