diff --git a/.env b/.env index 96573fb6..1d789eee 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ OPENSHOCK_API_DOMAIN=api.shocklink.net OPENSHOCK_FW_VERSION=0.0.0-unknown OPENSHOCK_FW_HOSTNAME=OpenShock -OPENSHOCK_FW_AP_PREFIX=OpenShock- +OPENSHOCK_FW_AP_PREFIX=OpenShock- \ No newline at end of file diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts b/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts index ea360ea5..c0ff31a2 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts +++ b/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts @@ -15,12 +15,17 @@ txPin():number { return this.bb!.readUint8(this.bb_pos); } +keepaliveEnabled():boolean { + return !!this.bb!.readInt8(this.bb_pos + 1); +} + static sizeOf():number { - return 1; + return 2; } -static createRFConfig(builder:flatbuffers.Builder, tx_pin: number):flatbuffers.Offset { - builder.prep(1, 1); +static createRFConfig(builder:flatbuffers.Builder, tx_pin: number, keepalive_enabled: boolean):flatbuffers.Offset { + builder.prep(1, 2); + builder.writeInt8(Number(Boolean(keepalive_enabled))); builder.writeInt8(tx_pin); return builder.offset(); } diff --git a/include/CommandHandler.h b/include/CommandHandler.h index 7ba2867a..294a0ac1 100644 --- a/include/CommandHandler.h +++ b/include/CommandHandler.h @@ -15,5 +15,8 @@ namespace OpenShock::CommandHandler { SetRfPinResultCode SetRfTxPin(std::uint8_t txPin); std::uint8_t GetRfTxPin(); + bool SetKeepAliveEnabled(bool enabled); + bool SetKeepAlivePaused(bool paused); + bool HandleCommand(ShockerModelType shockerModel, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs); } // namespace OpenShock::CommandHandler diff --git a/include/Config.h b/include/Config.h index bf03bc6c..40349629 100644 --- a/include/Config.h +++ b/include/Config.h @@ -10,6 +10,7 @@ namespace OpenShock::Config { // This is a copy of the flatbuffers schema defined in schemas/ConfigFile.fbs struct RFConfig { std::uint8_t txPin; + bool keepAliveEnabled; }; struct WiFiCredentials { std::uint8_t id; @@ -51,6 +52,7 @@ namespace OpenShock::Config { bool SetBackendConfig(const BackendConfig& config); bool SetRFConfigTxPin(std::uint8_t txPin); + bool SetRFConfigKeepAliveEnabled(bool enabled); std::uint8_t AddWiFiCredentials(const std::string& ssid, const std::uint8_t (&bssid)[6], const std::string& password); bool TryGetWiFiCredentialsByID(std::uint8_t id, WiFiCredentials& out); diff --git a/include/radio/RFTransmitter.h b/include/radio/RFTransmitter.h index 819a1e2e..50850177 100644 --- a/include/radio/RFTransmitter.h +++ b/include/radio/RFTransmitter.h @@ -22,7 +22,7 @@ namespace OpenShock { inline bool ok() const { return m_rmtHandle != nullptr && m_queueHandle != nullptr && m_taskHandle != nullptr; } - bool SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs); + bool SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs, bool overwriteExisting = true); void ClearPendingCommands(); private: diff --git a/include/serialization/_fbs/ConfigFile_generated.h b/include/serialization/_fbs/ConfigFile_generated.h index e5148ba4..3f693163 100644 --- a/include/serialization/_fbs/ConfigFile_generated.h +++ b/include/serialization/_fbs/ConfigFile_generated.h @@ -63,6 +63,7 @@ struct BSSID::Traits { FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) RFConfig FLATBUFFERS_FINAL_CLASS { private: uint8_t tx_pin_; + uint8_t keepalive_enabled_; public: struct Traits; @@ -70,16 +71,21 @@ FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) RFConfig FLATBUFFERS_FINAL_CLASS { return "OpenShock.Serialization.Configuration.RFConfig"; } RFConfig() - : tx_pin_(0) { + : tx_pin_(0), + keepalive_enabled_(0) { } - RFConfig(uint8_t _tx_pin) - : tx_pin_(::flatbuffers::EndianScalar(_tx_pin)) { + RFConfig(uint8_t _tx_pin, bool _keepalive_enabled) + : tx_pin_(::flatbuffers::EndianScalar(_tx_pin)), + keepalive_enabled_(::flatbuffers::EndianScalar(static_cast(_keepalive_enabled))) { } uint8_t tx_pin() const { return ::flatbuffers::EndianScalar(tx_pin_); } + bool keepalive_enabled() const { + return ::flatbuffers::EndianScalar(keepalive_enabled_) != 0; + } }; -FLATBUFFERS_STRUCT_END(RFConfig, 1); +FLATBUFFERS_STRUCT_END(RFConfig, 2); struct RFConfig::Traits { using type = RFConfig; diff --git a/platformio.ini b/platformio.ini index d53697e0..4dd98518 100644 --- a/platformio.ini +++ b/platformio.ini @@ -98,6 +98,7 @@ build_flags = -DOPENSHOCK_LED_GPIO=35 -DOPENSHOCK_TX_PIN=15 -DOPENSHOCK_ESTOP_PIN=13 + -DARDUINO_USB_CDC_ON_BOOT=1 ; TODO: ; https://docs.platformio.org/en/latest/boards/espressif32/upesy_wroom.html;upesy-esp32-wroom-devkit diff --git a/schemas/ConfigFile.fbs b/schemas/ConfigFile.fbs index 7fe1f346..c13159c1 100644 --- a/schemas/ConfigFile.fbs +++ b/schemas/ConfigFile.fbs @@ -6,6 +6,7 @@ struct BSSID { struct RFConfig { tx_pin:uint8; + keepalive_enabled:bool; } table WiFiCredentials { diff --git a/src/CommandHandler.cpp b/src/CommandHandler.cpp index ecff4559..1c0b4cf2 100644 --- a/src/CommandHandler.cpp +++ b/src/CommandHandler.cpp @@ -5,30 +5,182 @@ #include "Constants.h" #include "Logging.h" #include "radio/RFTransmitter.h" +#include "Time.h" +#include "util/TaskUtils.h" + +#include +#include +#include #include +#include const char* const TAG = "CommandHandler"; +const std::int64_t KEEP_ALIVE_INTERVAL = 60'000; +const std::uint16_t KEEP_ALIVE_DURATION = 300; + using namespace OpenShock; +template +constexpr T saturate(T value, T min, T max) { + return std::min(std::max(value, min), max); +} +std::uint32_t calculateEepyTime(std::int64_t timeToKeepAlive) { + std::int64_t now = OpenShock::millis(); + return static_cast(saturate(timeToKeepAlive - now, 0LL, KEEP_ALIVE_INTERVAL)); +} + +struct KnownShocker { + ShockerModelType model; + std::uint16_t shockerId; + std::int64_t lastActivityTimestamp; + bool killTask; +}; + +static SemaphoreHandle_t s_rfTransmitterSemaphore = nullptr; static std::unique_ptr s_rfTransmitter = nullptr; +static SemaphoreHandle_t s_keepAliveSemaphore = nullptr; +static QueueHandle_t s_keepAliveQueue = nullptr; +static TaskHandle_t s_keepAliveTaskHandle = nullptr; + +void _keepAliveTask(void* arg) { + std::int64_t timeToKeepAlive = KEEP_ALIVE_INTERVAL; + + // Map of shocker IDs to time of next keep alive + std::unordered_map activityMap; + + while (true) { + // Calculate eepyTime based on the timeToKeepAlive + std::uint32_t eepyTime = calculateEepyTime(timeToKeepAlive); + + KnownShocker cmd; + while (xQueueReceive(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(eepyTime)) == pdTRUE) { + if (cmd.killTask) { + ESP_LOGW(TAG, "Received kill command, exiting keep alive task"); + vTaskDelete(nullptr); + break; // This should never be reached + } + + activityMap[cmd.shockerId] = cmd; + + eepyTime = calculateEepyTime(std::min(timeToKeepAlive, cmd.lastActivityTimestamp + KEEP_ALIVE_INTERVAL)); + } + + // Update the time to now + std::int64_t now = OpenShock::millis(); + + // Keep track of the minimum activity time, so we know when to wake up + timeToKeepAlive = now + KEEP_ALIVE_INTERVAL; + + // For every entry that has a keep alive time less than now, send a keep alive + for (auto it = activityMap.begin(); it != activityMap.end(); ++it) { + auto& cmd = it->second; + + if (cmd.lastActivityTimestamp + KEEP_ALIVE_INTERVAL < now) { + ESP_LOGV(TAG, "Sending keep alive for shocker %u", cmd.shockerId); + + if (s_rfTransmitter == nullptr) { + ESP_LOGW(TAG, "RF Transmitter is not initialized, ignoring keep alive"); + break; + } + + if (!s_rfTransmitter->SendCommand(cmd.model, cmd.shockerId, ShockerCommandType::Vibrate, 0, KEEP_ALIVE_DURATION, false)) { + ESP_LOGW(TAG, "Failed to send keep alive for shocker %u", cmd.shockerId); + } + + cmd.lastActivityTimestamp = now; + } + + timeToKeepAlive = std::min(timeToKeepAlive, cmd.lastActivityTimestamp + KEEP_ALIVE_INTERVAL); + } + } +} + +bool _internalSetKeepAliveEnabled(bool enabled) { + bool wasEnabled = s_keepAliveQueue != nullptr && s_keepAliveTaskHandle != nullptr; + + if (enabled == wasEnabled) { + ESP_LOGV(TAG, "Keep alive task is already %s", enabled ? "enabled" : "disabled"); + return true; + } + + xSemaphoreTake(s_keepAliveSemaphore, portMAX_DELAY); + + if (enabled) { + ESP_LOGV(TAG, "Enabling keep alive task"); + + s_keepAliveQueue = xQueueCreate(32, sizeof(KnownShocker)); + if (s_keepAliveQueue == nullptr) { + ESP_LOGE(TAG, "Failed to create keep-alive task"); + + xSemaphoreGive(s_keepAliveSemaphore); + return false; + } + + if (TaskUtils::TaskCreateExpensive(_keepAliveTask, "KeepAliveTask", 4096, nullptr, 1, &s_keepAliveTaskHandle) != pdPASS) { + ESP_LOGE(TAG, "Failed to create keep-alive task"); + + vQueueDelete(s_keepAliveQueue); + s_keepAliveQueue = nullptr; + + xSemaphoreGive(s_keepAliveSemaphore); + return false; + } + } else { + ESP_LOGV(TAG, "Disabling keep alive task"); + if (s_keepAliveTaskHandle != nullptr && s_keepAliveQueue != nullptr) { + // Wait for the task to stop + KnownShocker cmd {.killTask = true}; + while (eTaskGetState(s_keepAliveTaskHandle) != eDeleted) { + vTaskDelay(pdMS_TO_TICKS(10)); + + // Send nullptr to stop the task gracefully + xQueueSend(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(10)); + } + vQueueDelete(s_keepAliveQueue); + s_keepAliveQueue = nullptr; + } else { + ESP_LOGW(TAG, "Keep alive task is already disabled? Something might be wrong."); + } + } + + xSemaphoreGive(s_keepAliveSemaphore); + + return true; +} + bool CommandHandler::Init() { - std::uint8_t txPin = Config::GetRFConfig().txPin; + if (s_rfTransmitterSemaphore != nullptr) { + ESP_LOGW(TAG, "RF Transmitter is already initialized"); + return true; + } + + auto& rfConfig = Config::GetRFConfig(); + + std::uint8_t txPin = rfConfig.txPin; if (!OpenShock::IsValidOutputPin(txPin)) { ESP_LOGW(TAG, "Clearing invalid RF TX pin"); Config::SetRFConfigTxPin(Constants::GPIO_INVALID); return false; } + s_rfTransmitterSemaphore = xSemaphoreCreateBinary(); + xSemaphoreGive(s_rfTransmitterSemaphore); s_rfTransmitter = std::make_unique(txPin, 32); if (!s_rfTransmitter->ok()) { - ESP_LOGE(TAG, "Failed to initialize RF transmitter"); + ESP_LOGE(TAG, "Failed to initialize RF Transmitter"); s_rfTransmitter = nullptr; return false; } + s_keepAliveSemaphore = xSemaphoreCreateBinary(); + xSemaphoreGive(s_keepAliveSemaphore); + if (rfConfig.keepAliveEnabled) { + _internalSetKeepAliveEnabled(true); + } + return true; } @@ -41,6 +193,8 @@ SetRfPinResultCode CommandHandler::SetRfTxPin(std::uint8_t txPin) { return SetRfPinResultCode::InvalidPin; } + xSemaphoreTake(s_rfTransmitterSemaphore, portMAX_DELAY); + if (s_rfTransmitter != nullptr) { ESP_LOGV(TAG, "Destroying existing RF transmitter"); s_rfTransmitter = nullptr; @@ -50,26 +204,56 @@ SetRfPinResultCode CommandHandler::SetRfTxPin(std::uint8_t txPin) { auto rfxmit = std::make_unique(txPin, 32); if (!rfxmit->ok()) { ESP_LOGE(TAG, "Failed to initialize RF transmitter"); + + xSemaphoreGive(s_rfTransmitterSemaphore); return SetRfPinResultCode::InternalError; } if (!Config::SetRFConfigTxPin(txPin)) { ESP_LOGE(TAG, "Failed to set RF TX pin in config"); + + xSemaphoreGive(s_rfTransmitterSemaphore); return SetRfPinResultCode::InternalError; } s_rfTransmitter = std::move(rfxmit); + xSemaphoreGive(s_rfTransmitterSemaphore); return SetRfPinResultCode::Success; } +bool CommandHandler::SetKeepAliveEnabled(bool enabled) { + if (!_internalSetKeepAliveEnabled(enabled)) { + return false; + } + + if (!Config::SetRFConfigKeepAliveEnabled(enabled)) { + ESP_LOGE(TAG, "Failed to set keep alive enabled in config"); + return false; + } + + return true; +} + +bool CommandHandler::SetKeepAlivePaused(bool paused) { + if (!_internalSetKeepAliveEnabled(!paused)) { + return false; + } + + return true; +} + std::uint8_t CommandHandler::GetRfTxPin() { return Config::GetRFConfig().txPin; } bool CommandHandler::HandleCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs) { + xSemaphoreTake(s_rfTransmitterSemaphore, portMAX_DELAY); + if (s_rfTransmitter == nullptr) { ESP_LOGW(TAG, "RF Transmitter is not initialized, ignoring command"); + + xSemaphoreGive(s_rfTransmitterSemaphore); return false; } @@ -86,5 +270,19 @@ bool CommandHandler::HandleCommand(ShockerModelType model, std::uint16_t shocker ESP_LOGV(TAG, "Command received: %u %u %u %u", model, shockerId, type, intensity); } - return s_rfTransmitter->SendCommand(model, shockerId, type, intensity, durationMs); + bool ok = s_rfTransmitter->SendCommand(model, shockerId, type, intensity, durationMs); + + xSemaphoreGive(s_rfTransmitterSemaphore); + xSemaphoreTake(s_keepAliveSemaphore, portMAX_DELAY); + + if (ok && s_keepAliveQueue != nullptr) { + KnownShocker cmd {.model = model, .shockerId = shockerId, .lastActivityTimestamp = OpenShock::millis() + durationMs}; + if (xQueueSend(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(10)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to send keep-alive command to queue"); + } + } + + xSemaphoreGive(s_keepAliveSemaphore); + + return ok; } diff --git a/src/Config.cpp b/src/Config.cpp index d34cbfea..58f3703b 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -26,7 +26,8 @@ bool ReadFbsConfig(const Serialization::Configuration::RFConfig* fbsConfig) { } _mainConfig.rf = { - .txPin = fbsConfig->tx_pin(), + .txPin = fbsConfig->tx_pin(), + .keepAliveEnabled = fbsConfig->keepalive_enabled(), }; return true; @@ -207,7 +208,7 @@ bool _trySaveConfig() { // Serialize flatbuffers::FlatBufferBuilder builder(1024); - auto rfConfig = Serialization::Configuration::RFConfig(_rf.txPin); + auto rfConfig = Serialization::Configuration::RFConfig(_rf.txPin, _rf.keepAliveEnabled); std::vector> wifiCredentials; for (const auto& cred : _wifi.credentials) { @@ -251,6 +252,7 @@ void Config::Init() { #else .txPin = Constants::GPIO_INVALID, #endif + .keepAliveEnabled = true, }, .wifi = { .apSsid = "", @@ -333,6 +335,11 @@ bool Config::SetRFConfigTxPin(std::uint8_t txPin) { return _trySaveConfig(); } +bool Config::SetRFConfigKeepAliveEnabled(bool enabled) { + _mainConfig.rf.keepAliveEnabled = enabled; + return _trySaveConfig(); +} + std::uint8_t Config::AddWiFiCredentials(const std::string& ssid, const std::uint8_t (&bssid)[6], const std::string& password) { std::uint8_t id = 0; diff --git a/src/EStopManager.cpp b/src/EStopManager.cpp index 2e573375..6edb8331 100644 --- a/src/EStopManager.cpp +++ b/src/EStopManager.cpp @@ -1,5 +1,7 @@ #include "EStopManager.h" +#include "CommandHandler.h" +#include "Config.h" #include "Logging.h" #include "Time.h" #include "VisualStateManager.h" @@ -32,6 +34,7 @@ void _estopManagerTask(TimerHandle_t xTimer) { s_estoppedAt = s_lastEStopButtonStateChange; ESP_LOGI(TAG, "Emergency Stopped!!!"); OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); + OpenShock::CommandHandler::SetKeepAlivePaused(true); } break; case EStopManager::EStopStatus::ESTOPPED_AND_HELD: @@ -55,6 +58,7 @@ void _estopManagerTask(TimerHandle_t xTimer) { s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; ESP_LOGI(TAG, "All clear!"); OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); + OpenShock::CommandHandler::SetKeepAlivePaused(false); } break; @@ -73,9 +77,14 @@ void EStopManager::Init(std::uint16_t updateIntervalMs) { ESP_LOGI(TAG, "Initializing on pin %u", s_estopPin); // Start the repeating task, 10Hz may seem slow, but it's plenty fast for an EStop - if (xTimerCreate(TAG, pdMS_TO_TICKS(updateIntervalMs), pdTRUE, nullptr, _estopManagerTask) == nullptr) { - ESP_LOGE(TAG, "Failed to create timer"); + TimerHandle_t timer = xTimerCreate(TAG, pdMS_TO_TICKS(updateIntervalMs), pdTRUE, nullptr, _estopManagerTask); + if (timer == nullptr) { + ESP_LOGE(TAG, "Failed to create timer!!! Triggering EStop."); + s_estopStatus = EStopManager::EStopStatus::ESTOPPED; + } else { + xTimerStart(timer, 0); } + #else ESP_LOGI(TAG, "EStopManager disabled, no pin defined"); #endif diff --git a/src/GatewayClient.cpp b/src/GatewayClient.cpp index 3b5c4e3d..0392a27a 100644 --- a/src/GatewayClient.cpp +++ b/src/GatewayClient.cpp @@ -1,9 +1,9 @@ #include "GatewayClient.h" -#include "util/CertificateUtils.h" #include "event_handlers/WebSocket.h" #include "Logging.h" #include "Time.h" +#include "util/CertificateUtils.h" #include "serialization/_fbs/DeviceToServerMessage_generated.h" @@ -84,7 +84,7 @@ bool GatewayClient::loop() { } void GatewayClient::_sendKeepAlive() { - ESP_LOGV(TAG, "Sending keep alive message"); + ESP_LOGV(TAG, "Sending Gateway keep-alive message"); // Casting to uint64 here is safe since millis is guaranteed to return a positive value OpenShock::Serialization::KeepAlive keepAlive((std::uint64_t)OpenShock::millis()); diff --git a/src/SerialInputHandler.cpp b/src/SerialInputHandler.cpp index 3afb59a0..e43886ee 100644 --- a/src/SerialInputHandler.cpp +++ b/src/SerialInputHandler.cpp @@ -3,8 +3,8 @@ #include "CommandHandler.h" #include "Config.h" #include "Logging.h" -#include "wifi/WiFiManager.h" #include "util/JsonRoot.h" +#include "wifi/WiFiManager.h" #include #include @@ -21,6 +21,7 @@ const char* const kCommandRestart = "restart"; const char* const kCommandRmtpin = "rmtpin"; const char* const kCommandAuthToken = "authtoken"; const char* const kCommandNetworks = "networks"; +const char* const kCommandKeepAlive = "keepalive"; const char* const kCommandFactoryReset = "factoryreset"; void _handleHelpCommand(char* arg, std::size_t argLength) { @@ -35,6 +36,8 @@ void _handleHelpCommand(char* arg, std::size_t argLength) { Serial.println("authtoken set auth token"); Serial.println("networks get all saved networks"); Serial.println("networks set all saved networks"); + Serial.println("keepalive get shocker keep-alive status"); + Serial.println("keepalive enable/disable shocker keep-alive"); Serial.println("factoryreset reset device to factory defaults and reboot"); return; } @@ -77,6 +80,19 @@ void _handleHelpCommand(char* arg, std::size_t argLength) { return; } + if (strcmp(arg, kCommandKeepAlive) == 0) { + Serial.println("keepalive"); + Serial.println(" Get the shocker keep-alive status."); + Serial.println(); + Serial.println("keepalive []"); + Serial.println(" Enable/disable shocker keep-alive."); + Serial.println(" Arguments:"); + Serial.println(" must be a boolean."); + Serial.println(" Example:"); + Serial.println(" keepalive true"); + return; + } + if (strcmp(arg, kCommandRestart) == 0) { Serial.println(kCommandRestart); Serial.println(" Restart the board"); @@ -114,6 +130,27 @@ void _handleHelpCommand(char* arg, std::size_t argLength) { Serial.println("Command not found"); } +// Checks if the given argument is a boolean +// Returns 0 if false, 1 if true, 255 if invalid +// Valid inputs: true, false, 1, 0, yes, no, y, n +// Case-insensitive +std::uint8_t _argToBool(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength <= 0) { + return 255; + } + + // Convert to lowercase + std::transform(arg, arg + argLength, arg, ::tolower); + + if (strcmp(arg, "true") == 0 || strcmp(arg, "1") == 0 || strcmp(arg, "yes") == 0 || strcmp(arg, "y") == 0) { + return 1; + } else if (strcmp(arg, "false") == 0 || strcmp(arg, "0") == 0 || strcmp(arg, "no") == 0 || strcmp(arg, "n") == 0) { + return 0; + } else { + return 255; + } +} + void _handleVersionCommand(char* arg, std::size_t argLength) { Serial.print("\n"); SerialInputHandler::PrintVersionInfo(); @@ -263,6 +300,26 @@ void _handleNetworksCommand(char* arg, std::size_t argLength) { OpenShock::WiFiManager::RefreshNetworkCredentials(); } +void _handleKeepAliveCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength <= 0) { + // Get keep alive status + Serial.print("$SYS$|Response|KeepAlive|"); + Serial.println(Config::GetRFConfig().keepAliveEnabled ? "true" : "false"); + return; + } + + std::uint8_t enabled = _argToBool(arg, argLength); + + if (enabled == 255) { + Serial.println("$SYS$|Error|Invalid argument (not a boolean)"); + return; + } else { + OpenShock::CommandHandler::SetKeepAliveEnabled(enabled); + } + + Serial.println("$SYS$|Success|Saved config"); +} + static std::unordered_map s_commandHandlers = { { kCommandHelp, _handleHelpCommand}, { kCommandVersion, _handleVersionCommand}, @@ -270,6 +327,7 @@ static std::unordered_map s_commandHa { kCommandRmtpin, _handleRmtpinCommand}, { kCommandAuthToken, _handleAuthtokenCommand}, { kCommandNetworks, _handleNetworksCommand}, + { kCommandKeepAlive, _handleKeepAliveCommand}, {kCommandFactoryReset, _handleFactoryResetCommand}, }; diff --git a/src/radio/RFTransmitter.cpp b/src/radio/RFTransmitter.cpp index 6347e61d..555632f2 100644 --- a/src/radio/RFTransmitter.cpp +++ b/src/radio/RFTransmitter.cpp @@ -16,6 +16,7 @@ struct command_t { std::vector sequence; std::shared_ptr> zeroSequence; std::uint16_t shockerId; + bool overwrite; }; const char* const TAG = "RFTransmitter"; @@ -56,7 +57,7 @@ RFTransmitter::~RFTransmitter() { destroy(); } -bool RFTransmitter::SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs) { +bool RFTransmitter::SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs, bool overwriteExisting) { if (m_queueHandle == nullptr) { ESP_LOGE(TAG, "[pin-%u] Queue is null", m_txPin); return false; @@ -65,7 +66,7 @@ bool RFTransmitter::SendCommand(ShockerModelType model, std::uint16_t shockerId, // Intensity must be between 0 and 99 intensity = std::min(intensity, (std::uint8_t)99); - command_t* cmd = new command_t {.until = OpenShock::millis() + durationMs, .sequence = Rmt::GetSequence(model, shockerId, type, intensity), .zeroSequence = Rmt::GetZeroSequence(model, shockerId), .shockerId = shockerId}; + command_t* cmd = new command_t {.until = OpenShock::millis() + durationMs, .sequence = Rmt::GetSequence(model, shockerId, type, intensity), .zeroSequence = Rmt::GetZeroSequence(model, shockerId), .shockerId = shockerId, .overwrite = overwriteExisting}; // We will use nullptr commands to end the task, if we got a nullptr here, we are out of memory... :( if (cmd == nullptr) { @@ -153,20 +154,27 @@ void RFTransmitter::TransmitTask(void* arg) { } // Replace the command if it already exists - bool replaced = false; + bool existed = false; for (auto it = commands.begin(); it != commands.end(); ++it) { - if ((*it)->shockerId == cmd->shockerId) { - delete *it; - *it = cmd; + auto& existingCmd = *it; + + if (existingCmd->shockerId == cmd->shockerId) { + existed = true; - replaced = true; + // Only replace the command if it should be overwritten + if (existingCmd->overwrite) { + delete *it; + *it = cmd; + } else { + delete cmd; + } break; } } // If the command was not replaced, add it to the queue - if (!replaced) { + if (!existed) { commands.push_back(cmd); } }