Initial public release
This commit is contained in:
@@ -0,0 +1,592 @@
|
||||
#include <M5Unified.h>
|
||||
#include "config.h"
|
||||
#include "settings.h"
|
||||
#include "usb_hid.h"
|
||||
#include "led_ui.h"
|
||||
#include "display_ui.h"
|
||||
#include "macro_storage.h"
|
||||
#include "ble_keystore.h"
|
||||
#include "ble_manager.h"
|
||||
#include "espnow_manager.h"
|
||||
#include "macro_engine.h"
|
||||
#include "serial_protocol.h"
|
||||
#include "live_keystroke.h"
|
||||
|
||||
SettingsManager settingsManager;
|
||||
HIDController hid;
|
||||
LedUI ledUI;
|
||||
DisplayUI display;
|
||||
MacroStorage storage;
|
||||
DebugLog debugLog;
|
||||
BLEKeyStore bleKeyStore;
|
||||
BLEManager bleManager;
|
||||
EspNowManager espnowManager;
|
||||
MacroEngine engine;
|
||||
SerialProtocol protocol;
|
||||
LiveKeystrokeEngine liveEngine;
|
||||
HardwareSerial rs232Serial(1); // UART1 for RS232 via Atomic RS232 Base
|
||||
|
||||
// Passed to SerialProtocol so that when the host-side terminal reconfigures
|
||||
// the RS232 port, any cached baud/config in the engine gets invalidated.
|
||||
void onRS232Reconfig() {
|
||||
engine.resetRS232Cache();
|
||||
}
|
||||
|
||||
int currentMacroIdx = 0;
|
||||
|
||||
// Track the last status string we drew to showLiveMode so we only
|
||||
// re-render when the BLE state actually changes (avoids flicker and
|
||||
// SPI traffic during heavy keystroke streaming).
|
||||
static const char* _liveLastStatus = nullptr;
|
||||
// True while the full-screen live-mode display is up (a host is
|
||||
// connected). Lets us detect the connected->idle transition and
|
||||
// repaint the macro selector exactly once.
|
||||
static bool _liveScreenShown = false;
|
||||
// True while the Bluetooth identify logo is up (host asked us to
|
||||
// identify ourselves so the user can label this device).
|
||||
static bool _liveIdentifyShown = false;
|
||||
// Last device-label version we rendered, so a new label repaints the
|
||||
// live-mode screen even when the status string hasn't changed.
|
||||
static uint32_t _liveLabelVerShown = 0;
|
||||
// Set true if the user cancels the auto-reconnect (power-loss resume) with a
|
||||
// button hold. Suppresses BLE for the rest of this boot; a power cycle
|
||||
// re-enables it.
|
||||
static bool _liveResumeCancelled = false;
|
||||
// True while the "Reconnecting..." screen is up (resume pending, not yet
|
||||
// connected). Lets us repaint the selector exactly once when it clears.
|
||||
static bool _resumeScreenShown = false;
|
||||
// True while the "MESH HUB" screen is up; repainted when the hub's node
|
||||
// roster changes (count shown on screen).
|
||||
static bool _hubScreenShown = false;
|
||||
static uint32_t _hubRosterVerShown = 0;
|
||||
// Latches the 5-second transport-toggle so it fires exactly once per press
|
||||
// (pressedFor() stays true for every update past the threshold). Cleared on
|
||||
// button release.
|
||||
static bool _modeSwitchArmed = false;
|
||||
|
||||
void setup() {
|
||||
// Disable DTR/RTS bootloader reboot ASAP — CDC_ON_BOOT means TinyUSB
|
||||
// is already running before setup(). This prevents a crash-restart cycle
|
||||
// from entering bootloader when the Python app has the port open.
|
||||
USBSerial.enableReboot(false);
|
||||
|
||||
// Initialize M5 WITHOUT touching USB serial
|
||||
auto cfg = M5.config();
|
||||
cfg.serial_baudrate = 0; // Prevent M5.begin() from calling Serial.begin()
|
||||
M5.begin(cfg);
|
||||
|
||||
// Initialize USB composite device (HID + CDC)
|
||||
hid.begin();
|
||||
|
||||
// Short serial timeout so readStringUntil doesn't block the loop
|
||||
Serial.setTimeout(100);
|
||||
|
||||
settingsManager.begin();
|
||||
|
||||
M5.BtnA.setHoldThresh(settingsManager.settings.holdMs);
|
||||
M5.BtnA.setDebounceThresh(DEBOUNCE_MS);
|
||||
|
||||
// Universal binary: M5GFX panel autodetect found an LCD on the AtomS3;
|
||||
// on the AtomS3 Lite there is none and getBoard() reports the Lite.
|
||||
// All screen output is gated inside DisplayUI; user feedback on the
|
||||
// Lite comes from the RGB LED (LedUI, via M5.Led).
|
||||
bool hasDisplay = (M5.getDisplayCount() > 0) &&
|
||||
(M5.getBoard() != m5::board_t::board_M5AtomS3Lite);
|
||||
ledUI.begin(!hasDisplay);
|
||||
display.begin(settingsManager.settings.orientation, hasDisplay, &ledUI);
|
||||
display.showBoot(settingsManager.settings.liveTransport == LIVE_TX_BLE
|
||||
? "Mode: BLE" : "Mode: Mesh");
|
||||
|
||||
if (!storage.begin()) {
|
||||
display.showMessage("FS Error!", TFT_RED);
|
||||
delay(2000);
|
||||
}
|
||||
storage.loadSubIndex();
|
||||
|
||||
// BLE payload-encryption key — load from LittleFS or generate on first boot.
|
||||
// Must come after storage.begin() since LittleFS is mounted there.
|
||||
bleKeyStore.begin();
|
||||
|
||||
debugLog.begin();
|
||||
debugLog.log("Device booted");
|
||||
|
||||
engine.begin(&hid, &display, &storage, &settingsManager, &rs232Serial);
|
||||
|
||||
// ESP-NOW mesh (live-keyboard transport). Radio stays OFF until the
|
||||
// idle loop brings up node listening (or the host app switches us
|
||||
// into hub mode over USB).
|
||||
espnowManager.begin(&settingsManager, &bleKeyStore, &liveEngine, &debugLog);
|
||||
|
||||
protocol.begin(&settingsManager, &storage, &display, &debugLog,
|
||||
&rs232Serial, onRS232Reconfig, &bleKeyStore, &bleManager,
|
||||
&espnowManager);
|
||||
|
||||
// Give USB time to fully enumerate
|
||||
delay(1500);
|
||||
showCurrentMacro();
|
||||
|
||||
// BLE manager — only stores debug log pointer here.
|
||||
// NimBLE is NOT started at boot; it's started on-demand when a
|
||||
// bluetooth node is hit, then shut down after variables are received.
|
||||
// This avoids all BLE/USB radio contention during normal operation.
|
||||
bleManager.begin(&debugLog, &bleKeyStore);
|
||||
engine.setBLEManager(&bleManager);
|
||||
|
||||
// Wire up the live-keystroke engine. The BLE manager pushes events
|
||||
// into it from its NimBLE write callback; we drain in the main loop.
|
||||
liveEngine.begin(&hid);
|
||||
bleManager.setLiveEngine(&liveEngine);
|
||||
|
||||
// Check for saved execution state (power-loss recovery)
|
||||
int resumeSlot = 0, resumeNode = 0;
|
||||
if (engine.checkSavedState(resumeSlot, resumeNode)) {
|
||||
uint16_t delaySeconds = settingsManager.settings.resumeDelay;
|
||||
// resumeSlot is the actual LittleFS slot number (not display index).
|
||||
// Validate it exists by checking if the slot appears in the order array.
|
||||
bool slotValid = false;
|
||||
for (int i = 0; i < storage.macroCount; i++) {
|
||||
if (storage.order[i] == resumeSlot) { slotValid = true; break; }
|
||||
}
|
||||
|
||||
Serial.printf("[BOOT] resume: slot=%d node=%d slotValid=%d delaySec=%u\n",
|
||||
resumeSlot, resumeNode, (int)slotValid, (unsigned)delaySeconds);
|
||||
|
||||
if (delaySeconds > 0 && slotValid) {
|
||||
// Drain stale button state. After a USB-power blip the M5.Btn
|
||||
// driver can latch a "wasClicked" on the first update — if we
|
||||
// peek at it during the countdown we'd cancel the resume the
|
||||
// user is depending on. Burn ~200ms of updates so anything
|
||||
// pending settles before we start watching for real input.
|
||||
for (int i = 0; i < 20; i++) { M5.update(); delay(10); }
|
||||
(void)M5.BtnA.wasClicked();
|
||||
(void)M5.BtnA.wasHold();
|
||||
|
||||
// Countdown with cancel option. A HOLD (long press) cancels; a
|
||||
// single click is ignored — it's too easy to bump the button
|
||||
// accidentally while watching imaging, and that would silently
|
||||
// discard the resume.
|
||||
bool cancelled = false;
|
||||
uint16_t cancelledAt = delaySeconds;
|
||||
for (uint16_t remaining = delaySeconds; remaining > 0; remaining--) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "Resuming in %ds\nHold to cancel", remaining);
|
||||
display.showMessage(msg, TFT_YELLOW);
|
||||
// Poll button every 100ms during each second
|
||||
for (int i = 0; i < 10; i++) {
|
||||
M5.update();
|
||||
if (M5.BtnA.wasHold()) {
|
||||
cancelled = true;
|
||||
cancelledAt = remaining;
|
||||
break;
|
||||
}
|
||||
delay(100);
|
||||
}
|
||||
if (cancelled) break;
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
Serial.println("[BOOT] resume: countdown completed, resuming macro");
|
||||
if (!engine.resumeMacro(resumeSlot, resumeNode,
|
||||
storage.macros[resumeSlot].name)) {
|
||||
Serial.println("[BOOT] resume: resumeMacro() failed, clearing state");
|
||||
engine.clearExecutionState();
|
||||
showCurrentMacro();
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[BOOT] resume: cancelled by hold at %us remaining\n",
|
||||
(unsigned)cancelledAt);
|
||||
engine.clearExecutionState();
|
||||
showCurrentMacro();
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[BOOT] resume: skipped (delay=%u, slotValid=%d) — clearing state\n",
|
||||
(unsigned)delaySeconds, (int)slotValid);
|
||||
engine.clearExecutionState();
|
||||
}
|
||||
} else {
|
||||
Serial.println("[BOOT] no resume state to load");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Live-transport abstraction ---------------------------------------
|
||||
// The device receives a live-keyboard session over exactly one radio,
|
||||
// chosen by settings.liveTransport. These helpers hide which one is active
|
||||
// so the idle loop's screen/reconnect logic is written once for both.
|
||||
|
||||
static bool liveIsBle() {
|
||||
return settingsManager.settings.liveTransport == LIVE_TX_BLE;
|
||||
}
|
||||
|
||||
static bool liveSessionActive() {
|
||||
return liveIsBle()
|
||||
? (bleManager.exchangeKind() == BLEManager::EX_LIVE &&
|
||||
bleManager.isClientConnected())
|
||||
: espnowManager.nodeInSession();
|
||||
}
|
||||
|
||||
static bool liveIdentifyActive() {
|
||||
return liveIsBle() ? bleManager.liveIdentify()
|
||||
: espnowManager.nodeIdentify();
|
||||
}
|
||||
|
||||
static const char* liveStatusStr() {
|
||||
return liveIsBle() ? bleManager.liveStatusText()
|
||||
: espnowManager.nodeStatusText();
|
||||
}
|
||||
|
||||
static const char* liveLabelStr() {
|
||||
return liveIsBle() ? bleManager.liveLabel() : espnowManager.nodeLabel();
|
||||
}
|
||||
|
||||
static uint32_t liveLabelVerNum() {
|
||||
return liveIsBle() ? bleManager.liveLabelVer()
|
||||
: espnowManager.nodeLabelVer();
|
||||
}
|
||||
|
||||
static bool liveResumePending() {
|
||||
return liveIsBle() ? bleManager.liveResumeRequested()
|
||||
: espnowManager.resumeRequestedAtBoot();
|
||||
}
|
||||
|
||||
static void liveConsumeResume() {
|
||||
if (liveIsBle()) bleManager.consumeLiveResume();
|
||||
else espnowManager.consumeResume();
|
||||
}
|
||||
|
||||
// Free both live radios. Safe to call when either/both are already down
|
||||
// (each teardown is idempotent). Used before running a routine, on a
|
||||
// transport toggle, and when a USB host claims the device.
|
||||
static void liveShutdownRadios() {
|
||||
espnowManager.shutdown();
|
||||
bleManager.stopLive();
|
||||
bleManager.shutdown();
|
||||
}
|
||||
|
||||
// Flip the persisted live transport (mesh <-> BLE), tear the current radio
|
||||
// down so the idle loop brings the new one up, and confirm on-screen. Called
|
||||
// from the idle selector when the button is held for MODE_SWITCH_HOLD_MS.
|
||||
static void toggleLiveTransport() {
|
||||
uint8_t next = liveIsBle() ? LIVE_TX_MESH : LIVE_TX_BLE;
|
||||
settingsManager.set("live_tx", next); // persists to NVS
|
||||
|
||||
liveShutdownRadios();
|
||||
// Cancel any pending power-loss auto-reconnect for the old transport and
|
||||
// re-arm listening for the new one.
|
||||
espnowManager.consumeResume();
|
||||
bleManager.consumeLiveResume();
|
||||
_liveResumeCancelled = false;
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
_resumeScreenShown = false;
|
||||
_liveLastStatus = nullptr;
|
||||
|
||||
display.showModeSwitch(next == LIVE_TX_BLE);
|
||||
// Hold the confirmation ~1.2 s. Pump the LED engine (no-op on an LCD
|
||||
// board) so a screenless Lite actually animates its mode-switch burst.
|
||||
// Deliberately do NOT call M5.update() here — the button release must
|
||||
// be left for the main loop to observe so _modeSwitchArmed clears.
|
||||
uint32_t until = millis() + 1200;
|
||||
while ((int32_t)(millis() - until) < 0) {
|
||||
ledUI.tick();
|
||||
delay(20);
|
||||
}
|
||||
showCurrentMacro();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
M5.update();
|
||||
|
||||
// Safety net: re-assert reboot disable every loop iteration
|
||||
USBSerial.enableReboot(false);
|
||||
|
||||
// LED pattern engine (AtomS3 Lite only; no-op with a display). Cheap:
|
||||
// recomputes the current pattern color and writes only on change.
|
||||
ledUI.tick();
|
||||
|
||||
// Keyboard-priority gate: when the HID controller is in the middle
|
||||
// of a synchronous USB-emitting operation (typeText, keyCombo,
|
||||
// mediaKey, probe), defer non-USB-HID housekeeping. These polls
|
||||
// don't touch the keyboard interface themselves, but they share the
|
||||
// main-loop task with engine.tick() — and skipping them keeps the
|
||||
// FreeRTOS scheduling latency on the typing path as low as possible
|
||||
// for any future change that pumps the loop while typing.
|
||||
//
|
||||
// In current code the main loop is already blocked inside tick()
|
||||
// for the duration of a keyboard op, so the flag is normally only
|
||||
// observed BETWEEN ops. We still gate here so the contract holds.
|
||||
bool hidCritical = hid.isCritical();
|
||||
|
||||
if (!hidCritical) {
|
||||
// Print deferred BLE status on the main task (safe for TinyUSB CDC)
|
||||
bleManager.pollStatus();
|
||||
}
|
||||
|
||||
// Drain queued live-mode keystrokes outside the HID-critical window.
|
||||
// The drain itself wraps in beginCritical/endCritical so any other
|
||||
// gated work observes the new in-flight state — but we only START
|
||||
// draining when the previous gate has lifted, so emissions don't
|
||||
// stack on top of each other.
|
||||
if (!hidCritical && liveEngine.isActive()) {
|
||||
if (liveEngine.drainQueue() > 0) {
|
||||
// Screenless boards flicker the LED so the user can see
|
||||
// keystrokes flowing (no-op when a display is present).
|
||||
ledUI.liveActivity();
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the latest absolute-mouse report (BT Keyboard trackpad). Not
|
||||
// cadence-buffered — emitted as soon as the HID-critical window is clear.
|
||||
if (!hidCritical) {
|
||||
liveEngine.drainMouse();
|
||||
}
|
||||
|
||||
bool settingsChanged = protocol.handleSerial();
|
||||
|
||||
if (!hidCritical) {
|
||||
// Drain RS232 RX into the terminal buffer when the host-side terminal is open
|
||||
protocol.pollRS232();
|
||||
}
|
||||
|
||||
if (settingsChanged) {
|
||||
M5.BtnA.setHoldThresh(settingsManager.settings.holdMs);
|
||||
}
|
||||
|
||||
if (protocol.needsRefresh()) {
|
||||
if (currentMacroIdx >= storage.macroCount) {
|
||||
currentMacroIdx = 0;
|
||||
}
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
showCurrentMacro();
|
||||
}
|
||||
|
||||
// ---- ESP-NOW mesh (live-keyboard transport) ----
|
||||
//
|
||||
// The mesh replaced the old per-device live-BLE channel. While idle
|
||||
// the device passively listens on the mesh channel so the hub (the
|
||||
// USB-attached unit the host app drives) can discover and JOIN it
|
||||
// WITHOUT any on-device gesture. Policy mirrors the old BLE one:
|
||||
//
|
||||
// * Profile uploads over USB (isUploadActive) and a USB-connected
|
||||
// host app keep the radio OFF — unless the host explicitly made
|
||||
// us the hub (espnow_hub command), which overrides isHostConnected.
|
||||
// * A running routine owns the device for HID, so the radio is torn
|
||||
// down the moment a routine starts (below). This also guarantees
|
||||
// the on-demand BLE variables exchange never coexists with WiFi.
|
||||
// * 10-second boot grace, skipped when the power-loss resume flag is
|
||||
// set so a host session reconnects without delay. A button hold
|
||||
// cancels the resume for this boot (idle UI below).
|
||||
//
|
||||
// Security: JOIN must decrypt under this device's AES key and the
|
||||
// keystroke stream under the session group key — a radio-local
|
||||
// attacker can never inject input.
|
||||
espnowManager.tick();
|
||||
|
||||
// Bring up whichever live radio settings.liveTransport selects (never
|
||||
// both — BLE and WiFi contend on the S3). The hub path is unaffected:
|
||||
// it's entered only over USB and owns the radio while active.
|
||||
static const uint32_t LIVE_LISTEN_BOOT_GRACE_MS = 10000;
|
||||
bool bleLiveUp = bleManager.isBLEActive() &&
|
||||
bleManager.exchangeKind() == BLEManager::EX_LIVE;
|
||||
bool resumeReq = liveResumePending() && !_liveResumeCancelled;
|
||||
if (espnowManager.isHub()) {
|
||||
// Host-driven bridge. It auto-reverts to OFF if the host app goes
|
||||
// quiet (see espnow_manager.h), so nothing to police here.
|
||||
} else if (protocol.isHostConnected() || protocol.isUploadActive()) {
|
||||
// A USB host app is talking to us — this is the configuring
|
||||
// computer, not a remote-resume scenario. Drop the resume request
|
||||
// and keep both live radios off for the rest of the boot.
|
||||
espnowManager.consumeResume();
|
||||
bleManager.consumeLiveResume();
|
||||
if (espnowManager.isRadioActive() || bleLiveUp) {
|
||||
liveShutdownRadios();
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
_resumeScreenShown = false;
|
||||
}
|
||||
} else if (!engine.isRunning() && !_liveResumeCancelled &&
|
||||
(resumeReq || millis() > LIVE_LISTEN_BOOT_GRACE_MS)) {
|
||||
if (liveIsBle()) {
|
||||
// Ensure the mesh radio is down, then advertise BLE live.
|
||||
if (espnowManager.isRadioActive()) espnowManager.shutdown();
|
||||
bleManager.startLive(); // idempotent
|
||||
} else {
|
||||
// Ensure BLE live is down, then idle-listen on the mesh.
|
||||
if (bleLiveUp) bleManager.stopLive();
|
||||
espnowManager.startNodeListen(); // idempotent
|
||||
}
|
||||
}
|
||||
|
||||
// Don't process button for macro selection while receiving data
|
||||
if (protocol.isBusy()) return;
|
||||
|
||||
if (engine.isRunning()) {
|
||||
engine.tick(settingsManager.settings.typeDelay);
|
||||
|
||||
if (M5.BtnA.wasClicked()) {
|
||||
engine.onButtonClick();
|
||||
}
|
||||
if (M5.BtnA.wasHold()) {
|
||||
engine.onButtonHold();
|
||||
}
|
||||
|
||||
// If engine just finished, return to macro selector
|
||||
if (!engine.isRunning()) {
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
showCurrentMacro();
|
||||
}
|
||||
} else {
|
||||
// Idle. Hub mode first: the host app drives everything over USB;
|
||||
// we only show which unit is the hub and how many nodes it sees.
|
||||
if (espnowManager.isHub()) {
|
||||
if (!_hubScreenShown ||
|
||||
espnowManager.hubNodesVer() != _hubRosterVerShown) {
|
||||
display.showHubMode(espnowManager.hubNodeCount());
|
||||
_hubRosterVerShown = espnowManager.hubNodesVer();
|
||||
_hubScreenShown = true;
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
}
|
||||
(void)M5.BtnA.wasClicked();
|
||||
(void)M5.BtnA.wasHold();
|
||||
return;
|
||||
}
|
||||
if (_hubScreenShown) {
|
||||
_hubScreenShown = false;
|
||||
showCurrentMacro();
|
||||
}
|
||||
|
||||
// Joined a live session (on whichever transport is active): show
|
||||
// the live-mode screen and let the host drive. The only on-device
|
||||
// action is a long-press, which overrides into running the
|
||||
// currently-selected routine.
|
||||
bool liveConnected = liveSessionActive();
|
||||
if (liveConnected) {
|
||||
// Joined — the power-loss resume is satisfied; clear the
|
||||
// one-shot boot request so later grace logic is normal.
|
||||
liveConsumeResume();
|
||||
_resumeScreenShown = false;
|
||||
if (liveIdentifyActive()) {
|
||||
// Host is asking us to identify ourselves so the user can
|
||||
// label this specific device — draw the Bluetooth logo
|
||||
// (blue/white LED flash on a Lite).
|
||||
if (!_liveIdentifyShown) {
|
||||
display.showBluetoothLogo();
|
||||
_liveIdentifyShown = true;
|
||||
_liveScreenShown = false; // force a status repaint after
|
||||
_liveLastStatus = nullptr;
|
||||
}
|
||||
} else {
|
||||
_liveIdentifyShown = false;
|
||||
const char* status = liveStatusStr();
|
||||
uint32_t lblVer = liveLabelVerNum();
|
||||
if (!_liveScreenShown || status != _liveLastStatus ||
|
||||
lblVer != _liveLabelVerShown) {
|
||||
display.showLiveMode(status, liveLabelStr());
|
||||
_liveLastStatus = status;
|
||||
_liveLabelVerShown = lblVer;
|
||||
_liveScreenShown = true;
|
||||
// Screenless mesh nodes: hub silence shows as the
|
||||
// cyan/red "lost hub" pattern instead of live-idle.
|
||||
if (!liveIsBle() && espnowManager.nodeLagging()) {
|
||||
ledUI.lagging();
|
||||
}
|
||||
}
|
||||
}
|
||||
(void)M5.BtnA.wasClicked(); // consumed, ignored while a host drives
|
||||
if (M5.BtnA.wasHold() && storage.macroCount > 0) {
|
||||
// Engine activity overrides a live session: free the radio,
|
||||
// then run the selected routine.
|
||||
liveShutdownRadios();
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
int idx = currentMacroIdx % storage.macroCount;
|
||||
int slot = storage.order[idx];
|
||||
engine.startMacro(slot, storage.macros[slot].name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No session yet. If we're auto-reconnecting after a power-loss
|
||||
// (resume flag set, not cancelled), show the "Reconnecting..."
|
||||
// screen and let a hold cancel it.
|
||||
if (liveResumePending() && !_liveResumeCancelled) {
|
||||
if (!_resumeScreenShown) {
|
||||
display.showReconnecting();
|
||||
_resumeScreenShown = true;
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
}
|
||||
(void)M5.BtnA.wasClicked();
|
||||
if (M5.BtnA.wasHold()) {
|
||||
// Cancel the auto-reconnect: radios off for the rest of
|
||||
// this boot. A power cycle re-enables it.
|
||||
_liveResumeCancelled = true;
|
||||
liveConsumeResume();
|
||||
liveShutdownRadios();
|
||||
_resumeScreenShown = false;
|
||||
showCurrentMacro();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No session. Repaint the selector once if we were just showing
|
||||
// the live screen, then handle the normal gestures.
|
||||
if (_liveScreenShown || _liveIdentifyShown || _resumeScreenShown) {
|
||||
_liveScreenShown = false;
|
||||
_liveIdentifyShown = false;
|
||||
_resumeScreenShown = false;
|
||||
_liveLastStatus = nullptr;
|
||||
showCurrentMacro();
|
||||
}
|
||||
|
||||
// Screen-button gestures (idle selector / listening):
|
||||
// click -> next macro
|
||||
// hold 0.5s .. <5s -> run the selected routine
|
||||
// hold >= 5s -> toggle the live transport (mesh <-> BLE)
|
||||
// The run gesture is classified on RELEASE so a long transport-toggle
|
||||
// hold has room to complete without the routine firing at ~0.5 s.
|
||||
if (M5.BtnA.pressedFor(MODE_SWITCH_HOLD_MS)) {
|
||||
if (!_modeSwitchArmed) {
|
||||
_modeSwitchArmed = true; // fire once for this press
|
||||
toggleLiveTransport();
|
||||
}
|
||||
return; // hold still in progress
|
||||
}
|
||||
if (M5.BtnA.wasReleased()) {
|
||||
_modeSwitchArmed = false; // ready for the next press
|
||||
}
|
||||
|
||||
if (M5.BtnA.wasClicked()) {
|
||||
if (storage.macroCount > 0) {
|
||||
currentMacroIdx = (currentMacroIdx + 1) % storage.macroCount;
|
||||
}
|
||||
showCurrentMacro();
|
||||
}
|
||||
|
||||
if (M5.BtnA.wasReleasedAfterHold() &&
|
||||
!M5.BtnA.wasReleaseFor(MODE_SWITCH_HOLD_MS) &&
|
||||
storage.macroCount > 0) {
|
||||
// Short hold released (not the 5 s toggle): tear both live radios
|
||||
// down so WiFi/BLE is fully off for HID emission (and for any BLE
|
||||
// variables node the routine may hit), then run.
|
||||
liveShutdownRadios();
|
||||
_liveScreenShown = false;
|
||||
int slot = storage.order[currentMacroIdx];
|
||||
engine.startMacro(slot, storage.macros[slot].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showCurrentMacro() {
|
||||
if (storage.macroCount == 0) {
|
||||
display.showMessage("No Macros");
|
||||
return;
|
||||
}
|
||||
if (currentMacroIdx >= storage.macroCount) currentMacroIdx = 0;
|
||||
int slot = storage.order[currentMacroIdx];
|
||||
display.showMacroSelector(slot, storage.macros[slot].name, currentMacroIdx, storage.macroCount,
|
||||
resolveColor(storage.macros[slot].labelColor),
|
||||
settingsManager.settings.liveTransport == LIVE_TX_BLE);
|
||||
}
|
||||
Reference in New Issue
Block a user