Files
EvilCrowRF-V2/include/ButtonCommands.h
Senape3000 e63dd4a344 Add CPU temp offset, button replay & SD root
Introduce CPU telemetry and configurable CPU temperature offset, add hardware-button replay support and SD root pathType, and update app/firmware plumbing.

Key changes:
- Added schematics PDFs to docs.
- Protocol/firmware: BinaryStatus extended with cpuTempDeciC, core0Mhz, core1Mhz; settings payload and sync now include cpuTempOffsetDeciC. HwButton config command supports an extended payload for replay file (pathType + path).
- ConfigManager: persist button replay path/type and cpuTempOffsetDeciC, clamp/range checks, build/save logic and default values updated.
- ButtonCommands: executeAction now receives buttonId; handleButtonConfig accepts optional replay file info; ReplayLast implemented to queue CC1101 transmissions using configured path/type.
- FileCommands: support pathType 5 (SD root) and treat root-based storages appropriately; only create SD directories for dedicated SD folders.
- StateCommands: sendSettingsSync updated to 10-byte payload and include cpu temp offset in logs.
- Mobile app (Flutter): localization for CPU temp offset; BLE provider persists and exposes cpuTempOffsetDeciC and device telemetry (cpuTempC, coreMHz); supports pathType 5 (SD root) and loads temp offset preference; settings provider persists button replay paths/types and exposes setters; firmware protocol helper builds extended hw button command; FilesScreen supports pickMode, SD root selection and enforces allowed extensions when picking; Debug screen adds slider/card to adjust CPU temp offset; NrfScreen uses nrfNotify() for targeted UI updates and visual tweaks; spectrum painter maxLevel adjusted to 100.

These changes enable local replay-from-file via hardware buttons, expose CPU temperature telemetry and offset tuning to the mobile app, and add SD root handling across firmware and app.
2026-02-13 14:31:56 +01:00

263 lines
9.9 KiB
C++

/**
* @file ButtonCommands.h
* @brief BLE command handler for hardware button configuration + polling.
*
* Command IDs:
* 0x40 = HW_BUTTON_CONFIG — Set action for a physical button
* Payload: [buttonId: 1|2][actionId: 0-6]
*
* Button actions (HwButtonAction enum):
* 0 = None
* 1 = Toggle NRF Jammer
* 2 = Toggle SubGhz Recording
* 3 = Replay Last Signal
* 4 = Toggle LED
* 5 = Deep Sleep
* 6 = Reboot
*
* GPIO34 (BUTTON1) and GPIO35 (BUTTON2) are input-only pins on ESP32.
* The polling function checkButtons() should be called from loop().
*/
#ifndef BUTTON_COMMANDS_H
#define BUTTON_COMMANDS_H
#include <Arduino.h>
#include "config.h"
#include "core/ble/CommandHandler.h"
#include "core/ble/ClientsManager.h"
#include "BinaryMessages.h"
#include "ConfigManager.h"
#include "core/device_controls/DeviceControls.h"
#include "modules/nrf/NrfJammer.h"
#include "modules/CC1101_driver/CC1101_Worker.h"
#include "esp_log.h"
/// Available actions for hardware buttons.
/// Must match the Flutter HwButtonAction enum order.
enum class HwButtonAction : uint8_t {
None = 0,
ToggleJammer = 1,
ToggleRecording = 2,
ReplayLast = 3,
ToggleLed = 4,
DeepSleep = 5,
Reboot = 6,
ACTION_COUNT = 7,
};
class ButtonCommands {
public:
static void registerCommands(CommandHandler& handler) {
handler.registerCommand(0x40, handleButtonConfig);
// Load persisted button actions from flash config
loadFromConfig();
ESP_LOGI("ButtonCmd", "HW Button commands registered (0x40), btn1=%d btn2=%d",
(int)button1Action, (int)button2Action);
}
/// Load button actions from ConfigManager (call after ConfigManager::loadSettings)
static void loadFromConfig() {
uint8_t a1 = ConfigManager::settings.button1Action;
uint8_t a2 = ConfigManager::settings.button2Action;
if (a1 < (uint8_t)HwButtonAction::ACTION_COUNT)
button1Action = static_cast<HwButtonAction>(a1);
if (a2 < (uint8_t)HwButtonAction::ACTION_COUNT)
button2Action = static_cast<HwButtonAction>(a2);
}
/// Call from loop() — polls buttons with debounce and executes assigned action.
static void checkButtons() {
static unsigned long lastBtn1Press = 0;
static unsigned long lastBtn2Press = 0;
static bool btn1WasPressed = false;
static bool btn2WasPressed = false;
const unsigned long debounceMs = 300;
unsigned long now = millis();
// BUTTON1 (GPIO34) — active LOW
bool btn1Pressed = (digitalRead(BUTTON1) == LOW);
if (btn1Pressed && !btn1WasPressed && (now - lastBtn1Press > debounceMs)) {
lastBtn1Press = now;
executeAction(button1Action, 1);
}
btn1WasPressed = btn1Pressed;
// BUTTON2 (GPIO35) — active LOW
bool btn2Pressed = (digitalRead(BUTTON2) == LOW);
if (btn2Pressed && !btn2WasPressed && (now - lastBtn2Press > debounceMs)) {
lastBtn2Press = now;
executeAction(button2Action, 2);
}
btn2WasPressed = btn2Pressed;
}
private:
static inline HwButtonAction button1Action = HwButtonAction::None;
static inline HwButtonAction button2Action = HwButtonAction::None;
/// Handle 0x40: Set button action
/// Payload basic: [buttonId (1|2)][actionId (0-6)]
/// Payload extended: [buttonId][actionId][pathType][pathLen][path...]
static bool handleButtonConfig(const uint8_t* data, size_t len) {
if (len < 2) {
ESP_LOGW("ButtonCmd", "Payload too short (need 2 bytes)");
return false;
}
uint8_t buttonId = data[0];
uint8_t actionId = data[1];
if (buttonId < 1 || buttonId > 2) {
ESP_LOGW("ButtonCmd", "Invalid button ID: %u (must be 1 or 2)", buttonId);
return false;
}
if (actionId >= (uint8_t)HwButtonAction::ACTION_COUNT) {
ESP_LOGW("ButtonCmd", "Invalid action ID: %u (max %u)",
actionId, (uint8_t)HwButtonAction::ACTION_COUNT - 1);
return false;
}
HwButtonAction action = static_cast<HwButtonAction>(actionId);
if (buttonId == 1) {
button1Action = action;
ConfigManager::settings.button1Action = actionId;
} else {
button2Action = action;
ConfigManager::settings.button2Action = actionId;
}
// Optional replay file configuration
if (len >= 4) {
uint8_t pathType = data[2];
uint8_t pathLen = data[3];
if (pathType > 5) {
ESP_LOGW("ButtonCmd", "Invalid replay pathType: %u", pathType);
return false;
}
if (pathLen > MAX_BUTTON_SIGNAL_PATH_LEN) {
ESP_LOGW("ButtonCmd", "Replay path too long: %u", pathLen);
return false;
}
if (len < (size_t)(4 + pathLen)) {
ESP_LOGW("ButtonCmd", "Replay payload truncated");
return false;
}
if (buttonId == 1) {
ConfigManager::settings.button1SignalPathType = pathType;
memset(ConfigManager::settings.button1SignalPath, 0, sizeof(ConfigManager::settings.button1SignalPath));
if (pathLen > 0) {
memcpy(ConfigManager::settings.button1SignalPath, data + 4, pathLen);
ConfigManager::settings.button1SignalPath[pathLen] = '\0';
}
} else {
ConfigManager::settings.button2SignalPathType = pathType;
memset(ConfigManager::settings.button2SignalPath, 0, sizeof(ConfigManager::settings.button2SignalPath));
if (pathLen > 0) {
memcpy(ConfigManager::settings.button2SignalPath, data + 4, pathLen);
ConfigManager::settings.button2SignalPath[pathLen] = '\0';
}
}
}
// Persist to flash so the action survives reboot
ConfigManager::saveSettings();
ESP_LOGI("ButtonCmd", "Button %u -> action %u (saved to flash)", buttonId, actionId);
// Send confirmation
uint8_t resp[] = { MSG_COMMAND_SUCCESS, buttonId, actionId };
ClientsManager::getInstance().notifyAllBinary(
NotificationType::SettingsSync, resp, sizeof(resp));
return true;
}
/// Execute the action assigned to a button.
static void executeAction(HwButtonAction action, uint8_t buttonId) {
switch (action) {
case HwButtonAction::None:
break;
case HwButtonAction::ToggleJammer:
if (NrfJammer::isRunning()) {
NrfJammer::stop();
ESP_LOGI("ButtonCmd", "Jammer stopped via button");
} else {
// Start with default mode (Full spectrum sweep)
NrfJammer::start(NRF_JAM_FULL);
ESP_LOGI("ButtonCmd", "Jammer started via button");
}
break;
case HwButtonAction::ToggleRecording:
// TODO: Implement recording toggle when recorder module
// exposes a static start/stop interface.
ESP_LOGI("ButtonCmd", "Toggle recording — not yet implemented");
DeviceControls::ledBlink(2, 100);
break;
case HwButtonAction::ReplayLast:
{
const char* configuredPath = (buttonId == 1)
? ConfigManager::settings.button1SignalPath
: ConfigManager::settings.button2SignalPath;
int configuredPathType = (buttonId == 1)
? ConfigManager::settings.button1SignalPathType
: ConfigManager::settings.button2SignalPathType;
if (configuredPath[0] == '\0') {
ESP_LOGW("ButtonCmd", "Replay requested but no .sub file configured for button %u", buttonId);
DeviceControls::ledBlink(3, 80);
break;
}
int module = CC1101Worker::findFirstIdleModule();
if (module < 0) {
ESP_LOGW("ButtonCmd", "No idle CC1101 module available for replay");
DeviceControls::ledBlink(4, 60);
break;
}
bool queued = CC1101Worker::transmit(module, std::string(configuredPath), 1, configuredPathType);
if (queued) {
ESP_LOGI("ButtonCmd", "Replay queued from button %u: module=%d pathType=%d path=%s",
buttonId, module, configuredPathType, configuredPath);
} else {
ESP_LOGE("ButtonCmd", "Replay queue failed from button %u", buttonId);
DeviceControls::ledBlink(4, 60);
}
}
break;
case HwButtonAction::ToggleLed:
{
static bool ledState = false;
ledState = !ledState;
digitalWrite(LED, ledState ? HIGH : LOW);
ESP_LOGI("ButtonCmd", "LED toggled %s", ledState ? "ON" : "OFF");
}
break;
case HwButtonAction::DeepSleep:
ESP_LOGI("ButtonCmd", "Entering deep sleep via button");
DeviceControls::ledBlink(5, 150);
DeviceControls::goDeepSleep();
break;
case HwButtonAction::Reboot:
ESP_LOGI("ButtonCmd", "Rebooting via button");
DeviceControls::ledBlink(3, 100);
vTaskDelay(pdMS_TO_TICKS(200));
esp_restart();
break;
default:
break;
}
}
};
#endif // BUTTON_COMMANDS_H