diff --git a/.changes/rgb-led-dithering.md b/.changes/rgb-led-dithering.md new file mode 100644 index 000000000..2022e804a --- /dev/null +++ b/.changes/rgb-led-dithering.md @@ -0,0 +1,10 @@ +--- +kind: added +--- +Smoother RGB LED dimming + +## Release Note +The RGB status LED now dims and blends colors in finer steps. Before, low brightness settings left only a few distinct levels per color, so dimming jumped visibly and nearby colors looked the same; now there are four times as many. + +- The LED takes 8 bits per color; the driver shows 10 by repeating 4 frames that alternate between the two nearest levels, looped by the RMT hardware so it costs no CPU (ESP32 and ESP32-S3; the ESP32-C3 lacks the RMT memory and stays at 8 bits) +- On by default; `CONFIG_OPENSHOCK_RGB_LED_DITHERING` turns it off diff --git a/.changes/status-led-toggle.md b/.changes/status-led-toggle.md new file mode 100644 index 000000000..2b10a9afc --- /dev/null +++ b/.changes/status-led-toggle.md @@ -0,0 +1,13 @@ +--- +kind: added +--- +Turn the status LEDs off or dim them + +## Release Note +The status LEDs can now be set to one of three modes: show every state (the default), light up only for an active E-Stop or a critical error, or stay off entirely. Their brightness can also be lowered, set separately for the built-in LED and the RGB LED, and separately for critical states (E-Stop, errors) and everything else. + +Change these with the `led mode` and `led brightness` serial commands, or in the captive portal's new LEDs section, which also lets you preview each state's LED pattern and try colors on the RGB LED. Only the LEDs your board has are shown. The portal shows what the LEDs are showing right now. Brightness you move but don't save is shown on the LEDs and goes back to the saved value after 10 seconds without changes. A color preview ends 2 seconds after you stop moving the picker. + +- Brightness is stored as 16 bits (0-65535) of the LED's full output; the portal shows it as a percent. Defaults match previous firmware exactly: built-in LED 65535 (100%), RGB LED 5140 (20/255, 7.8%); boards can change them in Kconfig (`OPENSHOCK_LED_*_BRIGHTNESS`) +- Setting the RGB LED above 30% logs a warning +- On boards with both LEDs, the built-in LED now also shows an E-Stop or a critical error, not only "connected" diff --git a/components/captive_portal/CMakeLists.txt b/components/captive_portal/CMakeLists.txt index 1fda09669..ff7a688e7 100644 --- a/components/captive_portal/CMakeLists.txt +++ b/components/captive_portal/CMakeLists.txt @@ -25,4 +25,4 @@ idf_component_register( ) # Match core: IDF compiles application components with -Werror, the legacy build did not. -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error -Werror=switch) # -Wswitch stays fatal: exhaustive enum switches rely on it diff --git a/components/captive_portal/src/captiveportal/Manager.cpp b/components/captive_portal/src/captiveportal/Manager.cpp index 9335cfba7..cc14769b2 100644 --- a/components/captive_portal/src/captiveportal/Manager.cpp +++ b/components/captive_portal/src/captiveportal/Manager.cpp @@ -11,9 +11,13 @@ const char* const TAG = "CaptivePortal"; #include "captiveportal/CaptivePortalConfig.h" #include "captiveportal/CaptivePortalInstance.h" #include "CommandHandler.h" +#include "events/Events.h" +#include "FormatHelpers.h" #include "GatewayConnectionManager.h" #include "Logging.h" +#include "serialization/WSLocal.h" #include "Temporal.h" +#include "visual/LedOverrides.h" #include "wifi/AccessPoint.h" #include "wifi/WiFiConfig.h" @@ -453,6 +457,19 @@ static void captiveportal_managertask(void* arg) } } +// Open portals get the LED settings and status once on connect; a change from anywhere (another client, the serial +// console, an override timing out) is sent to them as it happens. Broadcasting with no portal open is a no-op. +static void handleLedConfigChanged(void*, esp_event_base_t, int32_t, void*) +{ + Serialization::Local::SerializeLedSettingsChangedEvent(CaptivePortal::BroadcastMessageBIN); +} + +static void handleLedStatusChanged(void*, esp_event_base_t, int32_t, void* eventData) +{ + const auto& notice = *static_cast(eventData); + Serialization::Local::SerializeLedStatusEvent(notice, CaptivePortal::BroadcastMessageBIN); +} + // esp_timer callback (runs on the esp_timer task, 3.5KB stack). Must stay trivial: // it only kicks the manager task, which does the real work on its own stack. static void captiveportal_timernotify(void*) @@ -492,6 +509,17 @@ bool CaptivePortal::Init() AppHooks::RegisterCaptivePortal(CaptivePortal::BroadcastMessageBIN, CaptivePortal::SetAlwaysEnabled, CaptivePortal::IsRunning); + esp_err_t err = esp_event_handler_register(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_LED_CONFIG_CHANGED, handleLedConfigChanged, nullptr); + if (err != ESP_OK) { + // Portals still get the settings on connect + OS_LOGE(TAG, "Failed to register for LED settings changes: %s", esp_err_to_name(err)); + } + err = esp_event_handler_register(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_LED_STATUS_CHANGED, handleLedStatusChanged, nullptr); + if (err != ESP_OK) { + // Portals still get the status on connect + OS_LOGE(TAG, "Failed to register for LED status changes: %s", esp_err_to_name(err)); + } + esp_timer_create_args_t args = { .callback = captiveportal_timernotify, .arg = nullptr, @@ -500,8 +528,6 @@ bool CaptivePortal::Init() .skip_unhandled_events = true, }; - esp_err_t err; - err = esp_timer_create(&args, &s_captivePortalUpdateLoopTimer); if (err != ESP_OK) { OS_LOGE(TAG, "Failed to create captive portal update timer"); diff --git a/components/captive_portal/src/captiveportal/WebSocketEndpoint.cpp b/components/captive_portal/src/captiveportal/WebSocketEndpoint.cpp index 6d3612d57..03207f042 100644 --- a/components/captive_portal/src/captiveportal/WebSocketEndpoint.cpp +++ b/components/captive_portal/src/captiveportal/WebSocketEndpoint.cpp @@ -9,6 +9,7 @@ const char* const TAG = "CaptivePortalWs"; #include "message_handlers/WebSocket.h" #include "rfc8908/RFC8908Handler.h" #include "serialization/WSLocal.h" +#include "visual/VisualStateManager.h" #include "wifi/WiFiManager.h" #include "wifi/WiFiScanManager.h" @@ -326,7 +327,7 @@ void CaptivePortal::WebSocketEndpoint::handleWebSocketClientConnected(httpd_req_ connectedNetworkPtr = &connectedNetwork; } - Serialization::Local::SerializeReadyMessage(connectedNetworkPtr, GatewayConnectionManager::IsLinked(), sendBin); + Serialization::Local::SerializeReadyMessage(connectedNetworkPtr, GatewayConnectionManager::IsLinked(), socketId, sendBin); // Send all previously scanned wifi networks auto networks = OpenShock::WiFiManager::GetDiscoveredWiFiNetworks(); @@ -341,6 +342,10 @@ void CaptivePortal::WebSocketEndpoint::handleWebSocketClientConnected(httpd_req_ void CaptivePortal::WebSocketEndpoint::handleWebSocketClientDisconnected(uint8_t socketId) { OS_LOGD(TAG, "WebSocket client #%u disconnected", socketId); + + // A preview or unsaved brightness it left behind ends now rather than at its timeout. On the httpd task, like the + // connect handler, so no new client has taken the id yet. + VisualStateManager::ReleaseLedOverrides(socketId); } // --------------------------------------------------------------------------- diff --git a/components/common/include/enums/StatusLed.h b/components/common/include/enums/StatusLed.h new file mode 100644 index 000000000..539ec3c87 --- /dev/null +++ b/components/common/include/enums/StatusLed.h @@ -0,0 +1,77 @@ +#pragma once + +#include "StringHelpers.h" + +#include +#include +#include + +namespace OpenShock { + // Which states the status LEDs show + enum class LedMode : uint8_t { + All, // Every state + CriticalOnly, // Dark during normal operation; only an E-Stop or a critical error lights them + Off // Always dark + }; + + enum class LedType : uint8_t { + Mono, // The single-color built-in LED + Rgb // The WS2812B RGB LED + }; + + // What a pattern signals: critical is an E-Stop or a critical error, normal is everything else + enum class LedSignal : uint8_t { + Critical, + Normal + }; + + // The name jsonconfig and the serial console use; TryParseLedMode reads it back. + [[nodiscard]] constexpr const char* LedModeName(LedMode mode) + { + switch (mode) { + case LedMode::All: + return "all"; + case LedMode::CriticalOnly: + return "critical"; + case LedMode::Off: + return "off"; + } + return ""; + } + + [[nodiscard]] inline std::optional TryParseLedMode(std::string_view str) + { + if (StringIEquals(str, "all")) { + return LedMode::All; + } + if (StringIEquals(str, "critical")) { + return LedMode::CriticalOnly; + } + if (StringIEquals(str, "off")) { + return LedMode::Off; + } + return std::nullopt; + } + + [[nodiscard]] inline std::optional TryParseLedType(std::string_view str) + { + if (StringIEquals(str, "mono")) { + return LedType::Mono; + } + if (StringIEquals(str, "rgb")) { + return LedType::Rgb; + } + return std::nullopt; + } + + [[nodiscard]] inline std::optional TryParseLedSignal(std::string_view str) + { + if (StringIEquals(str, "critical")) { + return LedSignal::Critical; + } + if (StringIEquals(str, "normal")) { + return LedSignal::Normal; + } + return std::nullopt; + } +} // namespace OpenShock diff --git a/components/common/include/util/PercentUtils.h b/components/common/include/util/PercentUtils.h new file mode 100644 index 000000000..f9e44191f --- /dev/null +++ b/components/common/include/util/PercentUtils.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +namespace OpenShock::PercentUtils { + /// @brief `Percent` (0-100) of an unsigned type's full scale, rounded to nearest. The percent is a template argument + /// so it can be checked with static_assert, and the function is consteval: the floating point math only ever + /// runs in the compiler. + template + consteval T OfMax() + { + static_assert(Percent >= 0.0 && Percent <= 100.0, "a percent is 0-100"); + if constexpr (Percent == 100.0) { + // Exact: a 64-bit max rounds up to 2^64 as a double, which doesn't fit back in the type + return std::numeric_limits::max(); + } else { + return static_cast(Percent * static_cast(std::numeric_limits::max()) / 100.0 + 0.5); + } + } + + static_assert(OfMax() == 0); + static_assert(OfMax() == 65535); + static_assert(OfMax() == 128); + static_assert(OfMax() == std::numeric_limits::max()); +} // namespace OpenShock::PercentUtils diff --git a/components/config/CMakeLists.txt b/components/config/CMakeLists.txt index 5869a1d64..a56de1db9 100644 --- a/components/config/CMakeLists.txt +++ b/components/config/CMakeLists.txt @@ -18,4 +18,4 @@ idf_component_register( PRIV_REQUIRES logging # OS_LOG*/OS_PANIC ) -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error -Werror=switch) # -Wswitch stays fatal: exhaustive enum switches rely on it diff --git a/components/config/host_test/main/CMakeLists.txt b/components/config/host_test/main/CMakeLists.txt index 7c0595da7..56ea0c844 100644 --- a/components/config/host_test/main/CMakeLists.txt +++ b/components/config/host_test/main/CMakeLists.txt @@ -20,6 +20,7 @@ idf_component_register( # Each module's section "${_components}/device_control/src/radio/RFConfig.cpp" "${_components}/device_control/src/estop/EStopConfig.cpp" + "${_components}/device_control/src/visual/LedConfig.cpp" "${_components}/core/src/wifi/WiFiConfig.cpp" "${_components}/core/src/wifi/WiFiCredentials.cpp" "${_components}/core/src/BackendConfig.cpp" @@ -33,7 +34,7 @@ idf_component_register( "${_components}/common/src/StringHelpers.cpp" INCLUDE_DIRS "." "${_components}/config/include" # config/*.h - "${_components}/device_control/include" # radio/RFConfig.h, estop/EStopConfig.h + "${_components}/device_control/include" # radio/RFConfig.h, estop/EStopConfig.h, visual/LedConfig.h "${_components}/core/include" # wifi/WiFiConfig.h, BackendConfig.h, OtaUpdateConfig.h "${_components}/captive_portal/include" # captiveportal/CaptivePortalConfig.h "${_components}/serial_console/include" # serial_console/SerialInputConfig.h diff --git a/components/config/host_test/main/config_test_helpers.cpp b/components/config/host_test/main/config_test_helpers.cpp index 8908ef931..7d35aebbb 100644 --- a/components/config/host_test/main/config_test_helpers.cpp +++ b/components/config/host_test/main/config_test_helpers.cpp @@ -16,7 +16,7 @@ namespace ConfigTest { bool Full::SaveNvs(const char* partition) const { return rf.SaveNvs(partition, nullptr) && wifi.SaveNvs(partition, nullptr) && captivePortal.SaveNvs(partition, nullptr) && backend.SaveNvs(partition, nullptr) && serialInput.SaveNvs(partition, nullptr) && otaUpdate.SaveNvs(partition, nullptr) - && estop.SaveNvs(partition, nullptr); + && estop.SaveNvs(partition, nullptr) && led.SaveNvs(partition, nullptr); } Full MakeSampleConfig() @@ -36,6 +36,7 @@ namespace ConfigTest { config.serialInput = Config::SerialInputConfig(false); config.otaUpdate = Config::OtaUpdateConfig(false, "fw.example.org", OtaUpdateChannel::Develop, true, true, 120, false, true, 4242, OtaUpdateStep::Validating); config.estop = Config::EStopConfig(true, GPIO_NUM_13, true); + config.led = Config::LedConfig(LedMode::CriticalOnly, 40000, 0, 5140, 300); return config; } @@ -89,6 +90,7 @@ namespace ConfigTest { config.serialInput = Config::GetSerialInputConfig(); config.otaUpdate = Config::GetOtaUpdateConfig(); config.estop = Config::GetEStopConfig(); + config.led = Config::GetLedConfig(); return config; } @@ -137,5 +139,11 @@ namespace ConfigTest { TEST_ASSERT_EQUAL_MESSAGE(expected.estop.enabled, actual.estop.enabled, "estop.enabled"); TEST_ASSERT_EQUAL_INT_MESSAGE(expected.estop.gpioPin, actual.estop.gpioPin, "estop.gpioPin"); TEST_ASSERT_EQUAL_MESSAGE(expected.estop.latching, actual.estop.latching, "estop.latching"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(static_cast(expected.led.mode), static_cast(actual.led.mode), "led.mode"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected.led.monoCriticalBrightness, actual.led.monoCriticalBrightness, "led.monoCriticalBrightness"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected.led.monoNormalBrightness, actual.led.monoNormalBrightness, "led.monoNormalBrightness"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected.led.rgbCriticalBrightness, actual.led.rgbCriticalBrightness, "led.rgbCriticalBrightness"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(expected.led.rgbNormalBrightness, actual.led.rgbNormalBrightness, "led.rgbNormalBrightness"); } } // namespace ConfigTest diff --git a/components/config/host_test/main/config_test_helpers.h b/components/config/host_test/main/config_test_helpers.h index 30a28c843..6a9fbaf4f 100644 --- a/components/config/host_test/main/config_test_helpers.h +++ b/components/config/host_test/main/config_test_helpers.h @@ -13,6 +13,7 @@ #include "OtaUpdateConfig.h" #include "radio/RFConfig.h" #include "serial_console/SerialInputConfig.h" +#include "visual/LedConfig.h" #include "wifi/WiFiConfig.h" #include @@ -34,7 +35,8 @@ namespace ConfigTest { inline constexpr const char* kPortal = "portal"; inline constexpr const char* kSerial = "serial"; inline constexpr const char* kOta = "ota"; - inline constexpr const char* kAll[] = {kBackend, kWifi, kWifiCfg, kRf, kEstop, kPortal, kSerial, kOta}; + inline constexpr const char* kLed = "led"; + inline constexpr const char* kAll[] = {kBackend, kWifi, kWifiCfg, kRf, kEstop, kPortal, kSerial, kOta, kLed}; } // namespace Ns namespace Backend { inline constexpr const char* kDomain = "domain"; @@ -67,6 +69,7 @@ namespace ConfigTest { OpenShock::Config::SerialInputConfig serialInput; OpenShock::Config::OtaUpdateConfig otaUpdate; OpenShock::Config::EStopConfig estop; + OpenShock::Config::LedConfig led; // Every section's keys bool SaveNvs(const char* partition) const; diff --git a/components/config/host_test/main/test_defaults.cpp b/components/config/host_test/main/test_defaults.cpp index 8d7c4be26..453581974 100644 --- a/components/config/host_test/main/test_defaults.cpp +++ b/components/config/host_test/main/test_defaults.cpp @@ -59,6 +59,11 @@ TEST_CASE("Default values come from Kconfig and the board descriptor", "[config] TEST_ASSERT_EQUAL_INT(OPENSHOCK_ESTOP_PIN, config.estop.gpioPin); TEST_ASSERT_FALSE(config.estop.enabled); TEST_ASSERT_FALSE(config.estop.latching); // OPENSHOCK_ESTOP_LATCHING is 0 on the host-test board + TEST_ASSERT_EQUAL_INT(static_cast(LedMode::All), static_cast(config.led.mode)); + TEST_ASSERT_EQUAL_UINT16(CONFIG_OPENSHOCK_LED_MONO_CRITICAL_BRIGHTNESS, config.led.monoCriticalBrightness); + TEST_ASSERT_EQUAL_UINT16(CONFIG_OPENSHOCK_LED_MONO_NORMAL_BRIGHTNESS, config.led.monoNormalBrightness); + TEST_ASSERT_EQUAL_UINT16(CONFIG_OPENSHOCK_LED_RGB_CRITICAL_BRIGHTNESS, config.led.rgbCriticalBrightness); + TEST_ASSERT_EQUAL_UINT16(CONFIG_OPENSHOCK_LED_RGB_NORMAL_BRIGHTNESS, config.led.rgbNormalBrightness); } TEST_CASE("An empty store reads as the defaults and Init writes nothing", "[config][defaults]") diff --git a/components/config/host_test/main/test_roundtrip.cpp b/components/config/host_test/main/test_roundtrip.cpp index a435695cc..d1618990a 100644 --- a/components/config/host_test/main/test_roundtrip.cpp +++ b/components/config/host_test/main/test_roundtrip.cpp @@ -1,6 +1,7 @@ // Round trips: a config written by this firmware must read back unchanged after a reboot, through the NVS store // and through the jsonconfig (GetAsJSON / SaveFromJSON) path. #include +#include #include "unity.h" @@ -41,8 +42,14 @@ TEST_CASE("Values set through the Config API survive a reload", "[config][roundt TEST_ASSERT_TRUE(Config::SetOtaUpdateStep(OtaUpdateStep::Updated)); TEST_ASSERT_TRUE(Config::SetEStopGpioPin(GPIO_NUM_13)); TEST_ASSERT_TRUE(Config::SetEStopEnabled(true)); + TEST_ASSERT_TRUE(Config::UpdateLedConfig([](Config::LedConfig& led) { + led.mode = LedMode::Off; + led.rgbNormalBrightness = 60000; // Above 255: survives only as a u16 + return true; + })); const Full before = Snapshot(); + TEST_ASSERT_EQUAL_UINT16(60000, before.led.rgbNormalBrightness); TEST_ASSERT_EQUAL_UINT8_ARRAY(bssid, before.wifi.credentialsList[0].bssid.data(), 6); Reload(); @@ -76,12 +83,40 @@ TEST_CASE("Whole-section setters round trip through the store", "[config][roundt TEST_ASSERT_TRUE(Config::SetSerialInputConfig(sample.serialInput)); TEST_ASSERT_TRUE(Config::SetOtaUpdateConfig(sample.otaUpdate)); TEST_ASSERT_TRUE(Config::SetEStopConfig(sample.estop)); + TEST_ASSERT_TRUE(Config::SetLedConfig(sample.led)); AssertConfigEqual(sample, Snapshot()); Reload(); AssertConfigEqual(sample, Snapshot()); } +TEST_CASE("An unknown LED mode from a caller is reset to the default", "[config][roundtrip]") +{ + InitDefault(); + + // Not one of the LedMode values: a caller can hand the section anything + Config::LedConfig led = Config::GetLedConfig(); + led.mode = static_cast(7); + TEST_ASSERT_TRUE(Config::SetLedConfig(led)); + TEST_ASSERT_EQUAL_UINT8(std::to_underlying(LedMode::All), std::to_underlying(Config::GetLedConfig().mode)); + + TEST_ASSERT_TRUE(Config::UpdateLedConfig([](Config::LedConfig& l) { + l.mode = static_cast(200); + return true; + })); + TEST_ASSERT_EQUAL_UINT8(std::to_underlying(LedMode::All), std::to_underlying(Config::GetLedConfig().mode)); +} + +TEST_CASE("An unknown OTA update channel from a caller is reset to stable", "[config][roundtrip]") +{ + InitDefault(); + + Config::OtaUpdateConfig ota = Config::GetOtaUpdateConfig(); + ota.updateChannel = static_cast(9); + TEST_ASSERT_TRUE(Config::SetOtaUpdateConfig(ota)); + TEST_ASSERT_EQUAL_UINT8(std::to_underlying(OtaUpdateChannel::Stable), std::to_underlying(Config::GetOtaUpdateConfig().updateChannel)); +} + TEST_CASE("Removed credentials are gone after a reload and their ids can be reused", "[config][roundtrip]") { InitDefault(); diff --git a/components/config/host_test/main/test_storage.cpp b/components/config/host_test/main/test_storage.cpp index d53857906..fdfbc59b0 100644 --- a/components/config/host_test/main/test_storage.cpp +++ b/components/config/host_test/main/test_storage.cpp @@ -20,7 +20,7 @@ TEST_CASE("The store is the cfg partition", "[config][storage]") TEST_CASE("Sections link themselves into the registry in document order", "[config][storage]") { - const char* expected[] = {"rf", "wifi", "captivePortal", "backend", "serialInput", "otaUpdate", "estop"}; + const char* expected[] = {"rf", "wifi", "captivePortal", "backend", "serialInput", "otaUpdate", "estop", "led"}; std::size_t i = 0; for (auto* section = Config::SectionBase::First(); section != nullptr; section = section->Next(), ++i) { TEST_ASSERT_LESS_THAN_size_t(std::size(expected), i); diff --git a/components/core/CMakeLists.txt b/components/core/CMakeLists.txt index edb334a0d..794733130 100644 --- a/components/core/CMakeLists.txt +++ b/components/core/CMakeLists.txt @@ -26,4 +26,4 @@ idf_component_register( # applied -Wno-error to our own sources (build_src_flags), so partial designated # initializers and similar intentional patterns don't fail the build. Match that # here; can be tightened later by fixing the individual warnings. -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error -Werror=switch) # -Wswitch stays fatal: exhaustive enum switches rely on it diff --git a/components/core/include/serialization/WSLocal.h b/components/core/include/serialization/WSLocal.h index 57f034a4d..77781d467 100644 --- a/components/core/include/serialization/WSLocal.h +++ b/components/core/include/serialization/WSLocal.h @@ -5,18 +5,27 @@ #include "serialization/_fbs/WifiNetworkEventType_generated.h" +#include #include namespace OpenShock { class WiFiNetwork; } +namespace OpenShock::VisualStateManager { + struct LedStatusNotice; +} + namespace OpenShock::Serialization::Local { bool SerializeErrorMessage(std::string_view message, Common::SerializationCallbackFn callback); - bool SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool accountLinked, Common::SerializationCallbackFn callback); + /// @param clientId The socket id of the client the message goes to, which LED events name as an override's owner + bool SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool accountLinked, uint8_t clientId, Common::SerializationCallbackFn callback); bool SerializeWiFiScanStatusChangedEvent(OpenShock::WiFiScanStatus status, Common::SerializationCallbackFn callback); bool SerializeWiFiNetworkEvent(Types::WifiNetworkEventType eventType, const WiFiNetwork& network, Common::SerializationCallbackFn callback); bool SerializeWiFiNetworksEvent(Types::WifiNetworkEventType eventType, const std::vector& networks, Common::SerializationCallbackFn callback); bool SerializeWiFiGotIpEvent(const char* ip, Common::SerializationCallbackFn callback); bool SerializeAccountLinkStatusEvent(bool linked, Common::SerializationCallbackFn callback); + bool SerializeLedSettingsChangedEvent(Common::SerializationCallbackFn callback); + /// @brief The current LED status, with what `notice` reports beyond it. + bool SerializeLedStatusEvent(const VisualStateManager::LedStatusNotice& notice, Common::SerializationCallbackFn callback); } // namespace OpenShock::Serialization::Local diff --git a/components/core/src/OtaUpdateConfig.cpp b/components/core/src/OtaUpdateConfig.cpp index 3ebff40e5..3eac176d1 100644 --- a/components/core/src/OtaUpdateConfig.cpp +++ b/components/core/src/OtaUpdateConfig.cpp @@ -107,6 +107,21 @@ void OtaUpdateConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSen void OtaUpdateConfig::Normalize() { + // A caller can hand the section any value. Exhaustive with no default: -Werror=switch fails the build when + // OtaUpdateChannel gains a value not listed here. + const bool knownChannel = [this] { + switch (updateChannel) { + case OtaUpdateChannel::Stable: + case OtaUpdateChannel::Beta: + case OtaUpdateChannel::Develop: + return true; + } + return false; + }(); + if (!knownChannel) { + OS_LOGW(TAG, "Unknown OTA update channel %u, using stable", static_cast(std::to_underlying(updateChannel))); + updateChannel = OtaUpdateChannel::Stable; + } if (checkInterval == 0) { OS_LOGW(TAG, "A zero update check interval is not allowed, using the default"); checkInterval = kDefaultCheckInterval; diff --git a/components/core/src/message_handlers/impl/WSLocal.h b/components/core/src/message_handlers/impl/WSLocal.h index 725b4e62c..2d6a31725 100644 --- a/components/core/src/message_handlers/impl/WSLocal.h +++ b/components/core/src/message_handlers/impl/WSLocal.h @@ -11,6 +11,12 @@ namespace OpenShock::MessageHandlers::Local::_Private { using HandlerType = HANDLER_SIG((*)); HANDLER_FN(Common_ShockerCommandList); + HANDLER_FN(LedPreviewPatternMessage); + HANDLER_FN(LedPreviewColorMessage); + HANDLER_FN(LedDraftMessage); + HANDLER_FN(LedOverrideStopMessage); + HANDLER_FN(LedCommitDraftMessage); + HANDLER_FN(LedSetModeMessage); HANDLER_FN(InvalidMessage); } // namespace OpenShock::MessageHandlers::Local::_Private diff --git a/components/core/src/message_handlers/websocket/Local.cpp b/components/core/src/message_handlers/websocket/Local.cpp index 4427cd91a..2154fc170 100644 --- a/components/core/src/message_handlers/websocket/Local.cpp +++ b/components/core/src/message_handlers/websocket/Local.cpp @@ -27,6 +27,12 @@ static std::array s_localHandlers = []() { handlers.fill(Handlers::HandleInvalidMessage); SET_HANDLER(Common_ShockerCommandList); + SET_HANDLER(LedPreviewPatternMessage); + SET_HANDLER(LedPreviewColorMessage); + SET_HANDLER(LedDraftMessage); + SET_HANDLER(LedOverrideStopMessage); + SET_HANDLER(LedCommitDraftMessage); + SET_HANDLER(LedSetModeMessage); return handlers; }(); diff --git a/components/core/src/message_handlers/websocket/local/Led.cpp b/components/core/src/message_handlers/websocket/local/Led.cpp new file mode 100644 index 000000000..a9ed861f5 --- /dev/null +++ b/components/core/src/message_handlers/websocket/local/Led.cpp @@ -0,0 +1,201 @@ +#include "message_handlers/impl/WSLocal.h" + +const char* const TAG = "LocalMessageHandlers"; + +#include "Logging.h" +#include "visual/VisualStateManager.h" + +#include +#include + +namespace Schemas = OpenShock::Serialization::Local; +namespace Types = OpenShock::Serialization::Types; +namespace Vsm = OpenShock::VisualStateManager; +using OpenShock::LedMode; +using OpenShock::LedSignal; +using OpenShock::LedType; + +// The status LED requests. Each ends up in VisualStateManager, which reports its outcome to every portal +// (LedStatusEvent) under the sender's socket id; one this handler can't parse is reported the same way. +// +// The conversions are exhaustive switches with no default on purpose: -Wswitch (IDF builds with -Werror) fails the +// build when the schema gains a value that is not handled here. The value still comes off the wire unchecked (the +// verifier does not check an enum's range), so anything else falls out of the switch as unknown. + +static std::optional toLedPattern(Types::LedPattern pattern) +{ + switch (pattern) { + case Types::LedPattern::WifiDisconnected: + return Vsm::LedPattern::WiFiDisconnected; + case Types::LedPattern::WifiScanning: + return Vsm::LedPattern::WiFiScanning; + case Types::LedPattern::NoBackend: + return Vsm::LedPattern::NoBackend; + case Types::LedPattern::Connected: + return Vsm::LedPattern::Connected; + case Types::LedPattern::EStopped: + return Vsm::LedPattern::EStopped; + case Types::LedPattern::EStopClearing: + return Vsm::LedPattern::EStopClearing; + case Types::LedPattern::EStopAwaitingRelease: + return Vsm::LedPattern::EStopAwaitingRelease; + case Types::LedPattern::CriticalError: + return Vsm::LedPattern::CriticalError; + } + return std::nullopt; +} + +// The LEDs a color preview lights: nullopt for every LED, or one type +static bool toLedTarget(Schemas::LedTarget target, std::optional& only) +{ + switch (target) { + case Schemas::LedTarget::All: + only = std::nullopt; + return true; + case Schemas::LedTarget::Mono: + only = LedType::Mono; + return true; + case Schemas::LedTarget::Rgb: + only = LedType::Rgb; + return true; + } + return false; +} + +static std::optional toLedSignal(Schemas::LedSignal signal) +{ + switch (signal) { + case Schemas::LedSignal::Normal: + return LedSignal::Normal; + case Schemas::LedSignal::Critical: + return LedSignal::Critical; + } + return std::nullopt; +} + +static std::optional toLedOverrideLayer(Types::LedOverrideLayer layer) +{ + switch (layer) { + case Types::LedOverrideLayer::Display: + return Vsm::LedOverrideLayer::Display; + case Types::LedOverrideLayer::Draft: + return Vsm::LedOverrideLayer::Draft; + } + return std::nullopt; +} + +static std::optional toLedMode(Types::LedMode mode) +{ + switch (mode) { + case Types::LedMode::All: + return LedMode::All; + case Types::LedMode::CriticalOnly: + return LedMode::CriticalOnly; + case Types::LedMode::Off: + return LedMode::Off; + } + return std::nullopt; +} + +static void rejectUnknown(uint8_t socketId, uint16_t seq, const char* what, unsigned value) +{ + OS_LOGW(TAG, "Unknown LED %s %u", what, value); + Vsm::ReportLedRequestFailed(socketId, seq, Vsm::LedRequestError::InvalidValue); +} + +void OpenShock::MessageHandlers::Local::_Private::HandleLedPreviewPatternMessage(uint8_t socketId, const Schemas::LocalToHubMessage* msg) +{ + auto preview = msg->payload_as_LedPreviewPatternMessage(); + if (preview == nullptr) { + OS_LOGE(TAG, "Payload cannot be parsed as LedPreviewPatternMessage"); + return; + } + + auto pattern = toLedPattern(preview->pattern()); + if (!pattern) { + return rejectUnknown(socketId, preview->seq(), "preview pattern", std::to_underlying(preview->pattern())); + } + + Vsm::PreviewLedPattern(socketId, preview->seq(), *pattern); +} + +void OpenShock::MessageHandlers::Local::_Private::HandleLedPreviewColorMessage(uint8_t socketId, const Schemas::LocalToHubMessage* msg) +{ + auto preview = msg->payload_as_LedPreviewColorMessage(); + if (preview == nullptr) { + OS_LOGE(TAG, "Payload cannot be parsed as LedPreviewColorMessage"); + return; + } + + std::optional only; + if (!toLedTarget(preview->target(), only)) { + return rejectUnknown(socketId, preview->seq(), "preview target", std::to_underlying(preview->target())); + } + auto signal = toLedSignal(preview->signal()); + if (!signal) { + return rejectUnknown(socketId, preview->seq(), "preview signal", std::to_underlying(preview->signal())); + } + + Vsm::PreviewLedColor(socketId, preview->seq(), preview->red(), preview->green(), preview->blue(), only, *signal); +} + +void OpenShock::MessageHandlers::Local::_Private::HandleLedDraftMessage(uint8_t socketId, const Schemas::LocalToHubMessage* msg) +{ + auto draft = msg->payload_as_LedDraftMessage(); + if (draft == nullptr) { + OS_LOGE(TAG, "Payload cannot be parsed as LedDraftMessage"); + return; + } + + auto toPair = [](const Types::LedBrightness* brightness) -> std::optional { + if (brightness == nullptr) { + return std::nullopt; + } + return Vsm::LedDraftBrightness::Pair {.critical = brightness->critical(), .normal = brightness->normal()}; + }; + + Vsm::SetLedDraft(socketId, draft->seq(), Vsm::LedDraftBrightness {.mono = toPair(draft->mono()), .rgb = toPair(draft->rgb())}); +} + +void OpenShock::MessageHandlers::Local::_Private::HandleLedOverrideStopMessage(uint8_t socketId, const Schemas::LocalToHubMessage* msg) +{ + auto stop = msg->payload_as_LedOverrideStopMessage(); + if (stop == nullptr) { + OS_LOGE(TAG, "Payload cannot be parsed as LedOverrideStopMessage"); + return; + } + + auto layer = toLedOverrideLayer(stop->layer()); + if (!layer) { + return rejectUnknown(socketId, stop->seq(), "override layer", std::to_underlying(stop->layer())); + } + + Vsm::StopLedOverride(socketId, stop->seq(), *layer); +} + +void OpenShock::MessageHandlers::Local::_Private::HandleLedCommitDraftMessage(uint8_t socketId, const Schemas::LocalToHubMessage* msg) +{ + auto commit = msg->payload_as_LedCommitDraftMessage(); + if (commit == nullptr) { + OS_LOGE(TAG, "Payload cannot be parsed as LedCommitDraftMessage"); + return; + } + + Vsm::CommitLedDraft(socketId, commit->seq()); +} + +void OpenShock::MessageHandlers::Local::_Private::HandleLedSetModeMessage(uint8_t socketId, const Schemas::LocalToHubMessage* msg) +{ + auto setMode = msg->payload_as_LedSetModeMessage(); + if (setMode == nullptr) { + OS_LOGE(TAG, "Payload cannot be parsed as LedSetModeMessage"); + return; + } + + auto mode = toLedMode(setMode->mode()); + if (!mode) { + return rejectUnknown(socketId, setMode->seq(), "mode", std::to_underlying(setMode->mode())); + } + + Vsm::SetLedModeFor(socketId, setMode->seq(), *mode); +} diff --git a/components/core/src/serialization/WSGateway.cpp b/components/core/src/serialization/WSGateway.cpp index f9652006f..3597d7074 100644 --- a/components/core/src/serialization/WSGateway.cpp +++ b/components/core/src/serialization/WSGateway.cpp @@ -3,6 +3,7 @@ const char* const TAG = "WSGateway"; #include +#include #include #include "Logging.h" @@ -44,10 +45,20 @@ bool Gateway::SerializePongMessage(Common::SerializationCallbackFn callback) return finishMessage(builder, Gateway::HubToGatewayMessagePayload::Pong, pong, callback); } -// SerializeBootStatusMessage casts between these by value, and the gateway protocol must stay compatible -static_assert(std::to_underlying(OpenShock::FirmwareBootType::Normal) == std::to_underlying(Types::FirmwareBootType::Normal)); -static_assert(std::to_underlying(OpenShock::FirmwareBootType::NewFirmware) == std::to_underlying(Types::FirmwareBootType::NewFirmware)); -static_assert(std::to_underlying(OpenShock::FirmwareBootType::Rollback) == std::to_underlying(Types::FirmwareBootType::Rollback)); +// Exhaustive with no default on purpose: -Werror=switch fails the build when FirmwareBootType gains a value that is not +// mapped here. Any other value (a caller can pass anything) falls out as nullopt. +static std::optional toSchemaBootType(OpenShock::FirmwareBootType bootType) +{ + switch (bootType) { + case OpenShock::FirmwareBootType::Normal: + return Types::FirmwareBootType::Normal; + case OpenShock::FirmwareBootType::NewFirmware: + return Types::FirmwareBootType::NewFirmware; + case OpenShock::FirmwareBootType::Rollback: + return Types::FirmwareBootType::Rollback; + } + return std::nullopt; +} // Empty prerelease/build stay absent from the message rather than going out as "" (both messages the same way) static flatbuffers::Offset createSemVer(flatbuffers::FlatBufferBuilder& builder, const OpenShock::SemVer& version) @@ -68,7 +79,14 @@ bool Gateway::SerializeBootStatusMessage(int32_t updateId, OpenShock::FirmwareBo auto fbsVersion = createSemVer(builder, OpenShock::Constants::FirmwareVersion()); - auto fbsBootStatus = Gateway::CreateBootStatus(builder, static_cast(bootType), fbsVersion, updateId); + auto fbsBootType = toSchemaBootType(bootType); + if (!fbsBootType) { + // Normal is what AppHooks reports when it can't tell; not sending a boot status at all would leave the gateway + // without one + OS_LOGE(TAG, "Unknown firmware boot type %u, reporting normal", static_cast(std::to_underlying(bootType))); + } + + auto fbsBootStatus = Gateway::CreateBootStatus(builder, fbsBootType.value_or(Types::FirmwareBootType::Normal), fbsVersion, updateId); return finishMessage(builder, Gateway::HubToGatewayMessagePayload::BootStatus, fbsBootStatus, callback); } diff --git a/components/core/src/serialization/WSLocal.cpp b/components/core/src/serialization/WSLocal.cpp index 3f7bac123..6163af804 100644 --- a/components/core/src/serialization/WSLocal.cpp +++ b/components/core/src/serialization/WSLocal.cpp @@ -2,17 +2,22 @@ #include "serialization/WSLocal.h" +const char* const TAG = "WSLocal"; + #include "Chipset.h" #include "estop/EStopConfig.h" +#include "Logging.h" #include "OtaUpdateConfig.h" #include "radio/RFConfig.h" #include "util/HexUtils.h" +#include "visual/VisualStateManager.h" #include "wifi/AccessPoint.h" #include "wifi/WiFiConfig.h" #include "wifi/WiFiNetwork.h" #include "serialization/_fbs/HubToLocalMessage_generated.h" +#include #include #include @@ -62,10 +67,20 @@ static flatbuffers::Offset createW return Types::CreateWifiNetworkDirect(builder, network.ssid, bssid.data(), network.channel, network.rssi, authMode, network.IsSaved()); } -// createOtaSettings casts between these by value -static_assert(std::to_underlying(OpenShock::OtaUpdateChannel::Stable) == std::to_underlying(Local::OtaChannel::Stable)); -static_assert(std::to_underlying(OpenShock::OtaUpdateChannel::Beta) == std::to_underlying(Local::OtaChannel::Beta)); -static_assert(std::to_underlying(OpenShock::OtaUpdateChannel::Develop) == std::to_underlying(Local::OtaChannel::Develop)); +// Exhaustive with no default on purpose: -Werror=switch fails the build when OtaUpdateChannel gains a value that is not +// mapped here. Any other value (a caller can pass anything) falls out as nullopt. +static std::optional toSchemaOtaChannel(OpenShock::OtaUpdateChannel channel) +{ + switch (channel) { + case OpenShock::OtaUpdateChannel::Stable: + return Local::OtaChannel::Stable; + case OpenShock::OtaUpdateChannel::Beta: + return Local::OtaChannel::Beta; + case OpenShock::OtaUpdateChannel::Develop: + return Local::OtaChannel::Develop; + } + return std::nullopt; +} static flatbuffers::Offset createOtaSettings(flatbuffers::FlatBufferBuilder& builder) { @@ -73,7 +88,166 @@ static flatbuffers::Offset createOtaSettings(flatbuffers::Fl auto cdnDomain = builder.CreateString(ota.cdnDomain); - return Local::CreateOtaSettings(builder, ota.isEnabled, cdnDomain, static_cast(ota.updateChannel), ota.checkInterval, ota.allowBackendManagement, ota.requireManualApproval); + auto channel = toSchemaOtaChannel(ota.updateChannel); + if (!channel) { + // The hub follows no channel for one it doesn't know (its update check fails). Stable gives the portal a real + // choice to show, and saving it from there repairs the config. + OS_LOGE(TAG, "Unknown OTA update channel %u, reporting stable", static_cast(std::to_underlying(ota.updateChannel))); + } + + return Local::CreateOtaSettings(builder, ota.isEnabled, cdnDomain, channel.value_or(Local::OtaChannel::Stable), ota.checkInterval, ota.allowBackendManagement, ota.requireManualApproval); +} + +// Exhaustive with no default on purpose: -Werror=switch fails the build when LedMode gains a value that is not mapped +// here. Any other value (a caller can pass anything) falls out as nullopt. +static std::optional toSchemaLedMode(OpenShock::LedMode mode) +{ + switch (mode) { + case OpenShock::LedMode::All: + return Types::LedMode::All; + case OpenShock::LedMode::CriticalOnly: + return Types::LedMode::CriticalOnly; + case OpenShock::LedMode::Off: + return Types::LedMode::Off; + } + return std::nullopt; +} + +static flatbuffers::Offset createLedSettings(flatbuffers::FlatBufferBuilder& builder) +{ + namespace Vsm = OpenShock::VisualStateManager; + using OpenShock::LedSignal; + using OpenShock::LedType; + + // Only the LEDs the board has, so the portal shows no option for one that isn't there + auto brightness = [&](LedType type) -> flatbuffers::Offset { + if (!Vsm::HasLed(type)) { + return 0; + } + return Types::CreateLedBrightness(builder, Vsm::GetLedBrightness(type, LedSignal::Critical), Vsm::GetLedBrightness(type, LedSignal::Normal)); + }; + auto mono = brightness(LedType::Mono); + auto rgb = brightness(LedType::Rgb); + + const OpenShock::LedMode ledMode = Vsm::GetLedMode(); + auto mode = toSchemaLedMode(ledMode); + if (!mode) { + // The LEDs treat a mode they don't know like All (every state shown), so report that + OS_LOGE(TAG, "Unknown LED mode %u, reporting all", static_cast(std::to_underlying(ledMode))); + } + + return Local::CreateLedSettings(builder, mode.value_or(Types::LedMode::All), mono, rgb, Vsm::kRgbBrightnessWarn); +} + +// The LED status conversions: exhaustive switches like the ones above, so a new value fails the build until it is +// mapped. A value outside the enum (none is produced) falls out as the first one. + +static Types::LedPattern toSchemaLedPattern(OpenShock::VisualStateManager::LedPattern pattern) +{ + using OpenShock::VisualStateManager::LedPattern; + switch (pattern) { + case LedPattern::WiFiDisconnected: + return Types::LedPattern::WifiDisconnected; + case LedPattern::WiFiScanning: + return Types::LedPattern::WifiScanning; + case LedPattern::NoBackend: + return Types::LedPattern::NoBackend; + case LedPattern::Connected: + return Types::LedPattern::Connected; + case LedPattern::EStopped: + return Types::LedPattern::EStopped; + case LedPattern::EStopClearing: + return Types::LedPattern::EStopClearing; + case LedPattern::EStopAwaitingRelease: + return Types::LedPattern::EStopAwaitingRelease; + case LedPattern::CriticalError: + return Types::LedPattern::CriticalError; + } + return Types::LedPattern::WifiDisconnected; +} + +static Local::LedShowing toSchemaLedShowing(OpenShock::VisualStateManager::LedShowing showing) +{ + using OpenShock::VisualStateManager::LedShowing; + switch (showing) { + case LedShowing::Live: + return Local::LedShowing::Live; + case LedShowing::Hidden: + return Local::LedShowing::Hidden; + case LedShowing::Pattern: + return Local::LedShowing::Pattern; + case LedShowing::Color: + return Local::LedShowing::Color; + case LedShowing::Test: + return Local::LedShowing::Test; + } + return Local::LedShowing::Live; +} + +static Types::LedOverrideLayer toSchemaLedOverrideLayer(OpenShock::VisualStateManager::LedOverrideLayer layer) +{ + using OpenShock::VisualStateManager::LedOverrideLayer; + switch (layer) { + case LedOverrideLayer::Display: + return Types::LedOverrideLayer::Display; + case LedOverrideLayer::Draft: + return Types::LedOverrideLayer::Draft; + } + return Types::LedOverrideLayer::Display; +} + +static Local::LedOverrideEnd toSchemaLedOverrideEnd(OpenShock::VisualStateManager::LedOverrideEnd reason) +{ + using OpenShock::VisualStateManager::LedOverrideEnd; + switch (reason) { + case LedOverrideEnd::Expired: + return Local::LedOverrideEnd::Expired; + case LedOverrideEnd::Stopped: + return Local::LedOverrideEnd::Stopped; + case LedOverrideEnd::Replaced: + return Local::LedOverrideEnd::Replaced; + case LedOverrideEnd::OwnerLeft: + return Local::LedOverrideEnd::OwnerLeft; + case LedOverrideEnd::Committed: + return Local::LedOverrideEnd::Committed; + case LedOverrideEnd::LedTest: + return Local::LedOverrideEnd::LedTest; + } + return Local::LedOverrideEnd::Expired; +} + +static Local::LedRequestError toSchemaLedRequestError(OpenShock::VisualStateManager::LedRequestError error) +{ + using OpenShock::VisualStateManager::LedRequestError; + switch (error) { + case LedRequestError::NoLed: + return Local::LedRequestError::NoLed; + case LedRequestError::LedTestRunning: + return Local::LedRequestError::LedTestRunning; + case LedRequestError::NotHeld: + return Local::LedRequestError::NotHeld; + case LedRequestError::SaveFailed: + return Local::LedRequestError::SaveFailed; + case LedRequestError::InvalidValue: + return Local::LedRequestError::InvalidValue; + } + return Local::LedRequestError::InvalidValue; +} + +static flatbuffers::Offset createLedStatus(flatbuffers::FlatBufferBuilder& builder) +{ + const auto status = OpenShock::VisualStateManager::GetLedStatus(); + + auto layer = [&](const std::optional& info) -> flatbuffers::Offset { + if (!info.has_value()) { + return 0; + } + return Local::CreateLedOverride(builder, info->owner, info->timeoutMs); + }; + auto display = layer(status.display); + auto draft = layer(status.draft); + + return Local::CreateLedStatus(builder, toSchemaLedShowing(status.showing), toSchemaLedPattern(status.pattern), display, draft); } // The wire enum is the core one @@ -103,7 +277,7 @@ bool Local::SerializeErrorMessage(std::string_view message, Common::Serializatio return finishMessage(builder, Local::HubToLocalMessagePayload::ErrorMessage, wrapperOffset, callback); } -bool Local::SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool accountLinked, Common::SerializationCallbackFn callback) +bool Local::SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool accountLinked, uint8_t clientId, Common::SerializationCallbackFn callback) { flatbuffers::FlatBufferBuilder builder(768); @@ -126,12 +300,15 @@ bool Local::SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool acco auto otaOffset = createOtaSettings(builder); auto accessPointOffset = createAccessPointSettings(builder); + auto ledOffset = createLedSettings(builder); + auto ledStatusOffset = createLedStatus(builder); const auto rfTxPin = static_cast(OpenShock::Config::GetRFConfigTxPin()); const auto estop = OpenShock::Config::GetEStopConfig(); - auto readyMessageOffset - = Serialization::Local::CreateReadyMessage(builder, true, fbsNetwork, accountLinked, inputPinsOffset, outputPinsOffset, false, rfTxPin, estop.enabled, static_cast(estop.gpioPin), savedSsidsOffset, otaOffset, accessPointOffset); + auto readyMessageOffset = Serialization::Local::CreateReadyMessage( + builder, true, fbsNetwork, accountLinked, inputPinsOffset, outputPinsOffset, false, rfTxPin, estop.enabled, static_cast(estop.gpioPin), savedSsidsOffset, otaOffset, accessPointOffset, ledOffset, ledStatusOffset, clientId + ); return finishMessage(builder, Local::HubToLocalMessagePayload::ReadyMessage, readyMessageOffset, callback); } @@ -188,3 +365,37 @@ bool Local::SerializeAccountLinkStatusEvent(bool linked, Common::SerializationCa auto eventOffset = Local::CreateAccountLinkStatusEvent(builder, linked); return finishMessage(builder, Local::HubToLocalMessagePayload::AccountLinkStatusEvent, eventOffset, callback); } + +bool Local::SerializeLedSettingsChangedEvent(Common::SerializationCallbackFn callback) +{ + flatbuffers::FlatBufferBuilder builder(128); + + auto ledOffset = createLedSettings(builder); + auto eventOffset = Local::CreateLedSettingsChangedEvent(builder, ledOffset); + return finishMessage(builder, Local::HubToLocalMessagePayload::LedSettingsChangedEvent, eventOffset, callback); +} + +bool Local::SerializeLedStatusEvent(const VisualStateManager::LedStatusNotice& notice, Common::SerializationCallbackFn callback) +{ + using Kind = VisualStateManager::LedStatusNotice::Kind; + + flatbuffers::FlatBufferBuilder builder(128); + + auto statusOffset = createLedStatus(builder); + + flatbuffers::Offset endedOffset = 0; + flatbuffers::Offset failedOffset = 0; + switch (notice.kind) { + case Kind::StatusChanged: + break; + case Kind::Ended: + endedOffset = Local::CreateLedOverrideEnded(builder, toSchemaLedOverrideLayer(notice.layer), toSchemaLedOverrideEnd(notice.reason), notice.owner, notice.seq); + break; + case Kind::Failed: + failedOffset = Local::CreateLedRequestFailed(builder, notice.owner, notice.seq, toSchemaLedRequestError(notice.error)); + break; + } + + auto eventOffset = Local::CreateLedStatusEvent(builder, statusOffset, endedOffset, failedOffset); + return finishMessage(builder, Local::HubToLocalMessagePayload::LedStatusEvent, eventOffset, callback); +} diff --git a/components/device_control/CMakeLists.txt b/components/device_control/CMakeLists.txt index cc8933283..e073b3dfe 100644 --- a/components/device_control/CMakeLists.txt +++ b/components/device_control/CMakeLists.txt @@ -14,7 +14,7 @@ idf_component_register( REQUIRES common esp_hal_gpio esp_driver_rmt freertos # public headers (enums, gpio_types, rmt_tx/encoder, TaskHandle/QueueHandle) config # its config sections (radio/RFConfig.h, estop/EStopConfig.h): config/Section.h PRIV_REQUIRES chipset events led_drivers logging temporal protocols # concurrency/util folded into common (public REQUIRE) - esp_driver_gpio esp_wifi esp_event esp_netif soc + esp_driver_gpio esp_wifi esp_event esp_netif esp_timer soc ) -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error -Werror=switch) # -Wswitch stays fatal: exhaustive enum switches rely on it diff --git a/components/device_control/Kconfig b/components/device_control/Kconfig index 7bfe01fa5..028fd6786 100644 --- a/components/device_control/Kconfig +++ b/components/device_control/Kconfig @@ -59,3 +59,36 @@ menu "OpenShock emergency stop" At most 16 samples fit the debounce history, hence the 5 ms floor. endmenu + +menu "OpenShock status LEDs" + + # Defaults of the led config section, changeable at runtime (serial `led brightness`, the captive portal). + # Brightness is 16-bit, 0-65535 of the LED's full output; the LED drivers reduce it to what the LED shows (10 bits + # on the dithered RGB LED). The RGB defaults are exactly the 20/255 the hub always used (5140 = 20 * 257): a + # WS2812B at full output is blinding. + + config OPENSHOCK_LED_MONO_CRITICAL_BRIGHTNESS + int "Built-in LED brightness for critical patterns (0-65535)" + range 0 65535 + default 65535 + help + Brightness of the single-color LED for an E-Stop or a critical error. + + config OPENSHOCK_LED_MONO_NORMAL_BRIGHTNESS + int "Built-in LED brightness for normal patterns (0-65535)" + range 0 65535 + default 65535 + + config OPENSHOCK_LED_RGB_CRITICAL_BRIGHTNESS + int "RGB LED brightness for critical patterns (0-65535)" + range 0 65535 + default 5140 + help + Brightness of the WS2812B RGB LED for an E-Stop or a critical error. + + config OPENSHOCK_LED_RGB_NORMAL_BRIGHTNESS + int "RGB LED brightness for normal patterns (0-65535)" + range 0 65535 + default 5140 + +endmenu diff --git a/components/device_control/include/visual/LedConfig.h b/components/device_control/include/visual/LedConfig.h new file mode 100644 index 000000000..99c9a0a3a --- /dev/null +++ b/components/device_control/include/visual/LedConfig.h @@ -0,0 +1,49 @@ +#pragma once + +// The `led` config section: which states the status LEDs show and how bright they are. Owned by device_control and +// kept in NVS through the config framework (config/Section.h); namespace "led", keys mode, monoCrit, monoNorm, rgbCrit +// and rgbNorm (u16). + +#include "config/Section.h" +#include "enums/StatusLed.h" +#include "util/FunctionRef.h" + +#include + +namespace OpenShock::JSON { + class JsonView; +} + +namespace OpenShock::Config { + struct LedConfig { + LedConfig(); // The defaults: the one place they are defined + LedConfig(LedMode mode, uint16_t monoCriticalBrightness, uint16_t monoNormalBrightness, uint16_t rgbCriticalBrightness, uint16_t rgbNormalBrightness); + + LedMode mode; + // 16-bit, 0-65535 of the LED's full output + uint16_t monoCriticalBrightness; + uint16_t monoNormalBrightness; + uint16_t rgbCriticalBrightness; + uint16_t rgbNormalBrightness; + + [[nodiscard]] uint16_t& Brightness(LedType type, LedSignal signal); + [[nodiscard]] uint16_t Brightness(LedType type, LedSignal signal) const { return const_cast(this)->Brightness(type, signal); } + + /// @brief Resets a mode that is none of the LedMode values to the default; every uint16 is a valid brightness. The + /// section runs it on every value it takes in, whoever the caller. + void Normalize(); + + /// @brief Reads this section from `partition`; a missing key is its default, a read error fails. + bool LoadNvs(const char* partition); + /// @brief Writes the keys that differ from `previous` (every key when null) to `partition`. + bool SaveNvs(const char* partition, const LedConfig* previous) const; + + bool FromJSON(JSON::JsonView json); + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const; + }; + + [[nodiscard]] LedConfig GetLedConfig(); + bool SetLedConfig(const LedConfig& config); + /// @brief Read-modify-write of the section (config/Section.h Update). + bool UpdateLedConfig(Util::FunctionRef update); +} // namespace OpenShock::Config diff --git a/components/device_control/include/visual/LedOverrides.h b/components/device_control/include/visual/LedOverrides.h new file mode 100644 index 000000000..f48a77a75 --- /dev/null +++ b/components/device_control/include/visual/LedOverrides.h @@ -0,0 +1,132 @@ +#pragma once + +#include "enums/StatusLed.h" + +#include +#include + +// Overrides: what a portal client puts on the status LEDs without saving it. There are two independent layers, each +// held by one client at a time and timed by the hub, so a client that goes quiet or away never leaves the LEDs stuck: +// +// - Display: a previewed pattern or solid color, shown instead of the hub's state. +// - Draft: unsaved brightness, applied to whatever shows (the hub's state or a preview) until the owner saves it +// (CommitLedDraft) or stops sending it. +// +// Every change to what the LEDs show, every override ending, and every failed request posts +// OPENSHOCK_EVENT_LED_STATUS_CHANGED with an LedStatusNotice; GetLedStatus() reads the state it reports. + +namespace OpenShock::VisualStateManager { + enum class LedPattern : uint8_t { + WiFiDisconnected, + WiFiScanning, + NoBackend, // Network up, gateway not connected + Connected, + EStopped, + EStopClearing, + EStopAwaitingRelease, + CriticalError, + }; + + enum class LedOverrideLayer : uint8_t { + Display, + Draft, + }; + + enum class LedOverrideEnd : uint8_t { + Expired, // The owner sent nothing for it within its timeout + Stopped, // The owner ended it + Replaced, // Another client took the layer over + OwnerLeft, // The owner disconnected + Committed, // Saved (a draft only) + LedTest, // The LED test took over the LEDs + }; + + enum class LedRequestError : uint8_t { + NoLed, // The board has none of the LEDs the request is for + LedTestRunning, // The LED test holds the LEDs + NotHeld, // The client holds no override on that layer + SaveFailed, // The settings could not be saved + InvalidValue, // A value the hub doesn't know + }; + + enum class LedShowing : uint8_t { + Live, // The hub's state (LedStatus::pattern) + Hidden, // The hub's state, but the LED mode keeps the LEDs dark + Pattern, // A previewed pattern (LedStatus::pattern) + Color, // A previewed color + Test, // The LED test + }; + + // Display timeouts: a color ends this long after its owner's last one; a pattern shows about two of its cycles, + // within these bounds. A draft ends this long after its owner's last one. + inline constexpr uint16_t kColorPreviewTimeoutMs = 2'000; + inline constexpr uint16_t kPatternPreviewMinMs = 3'000; + inline constexpr uint16_t kPatternPreviewMaxMs = 12'000; + inline constexpr uint16_t kDraftBrightnessTimeoutMs = 10'000; + + struct LedOverrideInfo { + uint8_t owner; + uint16_t timeoutMs; + + bool operator==(const LedOverrideInfo&) const = default; + }; + + struct LedStatus { + LedShowing showing; + LedPattern pattern; // For Live and Pattern + std::optional display; + std::optional draft; + + bool operator==(const LedStatus&) const = default; + }; + + // The event data of OPENSHOCK_EVENT_LED_STATUS_CHANGED: what the event reports beyond the current status + struct LedStatusNotice { + enum class Kind : uint8_t { + StatusChanged, // Only the status (GetLedStatus) changed + Ended, // An override ended: layer, reason, owner, seq + Failed, // A request failed: error, owner, seq + }; + Kind kind; + LedOverrideLayer layer; + LedOverrideEnd reason; + LedRequestError error; + uint8_t owner; + uint16_t seq; + }; + + /// @brief Brightness for each LED, 16-bit (0-65535); unset for an LED the draft leaves at its saved values. + struct LedDraftBrightness { + struct Pair { + uint16_t critical; + uint16_t normal; + bool operator==(const Pair&) const = default; + }; + std::optional mono; + std::optional rgb; + bool operator==(const LedDraftBrightness&) const = default; + }; + + [[nodiscard]] LedStatus GetLedStatus(); + + /// @brief Shows `pattern` whatever the LED mode, for about two of its cycles. + void PreviewLedPattern(uint8_t owner, uint16_t seq, LedPattern pattern); + /// @brief Shows a solid color: the RGB LED in `red`/`green`/`blue` (16 bits per channel, 0-65535; the driver dithers + /// what the LED's 8 bits can't show), the mono LED on unless the color is black. Ends kColorPreviewTimeoutMs + /// after the last call. + /// @param only Light only this LED; the other stays dark. All LEDs when unset. + /// @param signal Which brightness it shows at: the draft's if one is held, else the saved one. + void PreviewLedColor(uint8_t owner, uint16_t seq, uint16_t red, uint16_t green, uint16_t blue, std::optional only, LedSignal signal); + /// @brief Replaces `owner`'s draft brightness. Ends kDraftBrightnessTimeoutMs after the last call. + void SetLedDraft(uint8_t owner, uint16_t seq, const LedDraftBrightness& draft); + /// @brief Ends `owner`'s override on `layer`. + void StopLedOverride(uint8_t owner, uint16_t seq, LedOverrideLayer layer); + /// @brief Saves `owner`'s draft to the `led` config section. + void CommitLedDraft(uint8_t owner, uint16_t seq); + /// @brief Saves the LED mode, reporting a failure to `owner`. + void SetLedModeFor(uint8_t owner, uint16_t seq, LedMode mode); + /// @brief Reports a request the caller could not even parse (an unknown enum value) as failed. + void ReportLedRequestFailed(uint8_t owner, uint16_t seq, LedRequestError error); + /// @brief Ends every override `owner` holds, for a client that disconnected. + void ReleaseLedOverrides(uint8_t owner); +} // namespace OpenShock::VisualStateManager diff --git a/components/device_control/include/visual/VisualStateManager.h b/components/device_control/include/visual/VisualStateManager.h index 445fc85b1..9b48c029a 100644 --- a/components/device_control/include/visual/VisualStateManager.h +++ b/components/device_control/include/visual/VisualStateManager.h @@ -1,13 +1,47 @@ #pragma once +#include "enums/StatusLed.h" +#include "util/FunctionRef.h" +#include "util/PercentUtils.h" +#include "visual/LedOverrides.h" // Previews and unsaved brightness + #include +namespace OpenShock::Config { + struct LedConfig; +} + namespace OpenShock::VisualStateManager { [[nodiscard]] bool Init(); + /// @brief Whether this board has an LED of `type`. False for both before Init. + [[nodiscard]] bool HasLed(LedType type); + /// @brief Whether this board has any status LED. + [[nodiscard]] inline bool HasAnyLed() + { + return HasLed(LedType::Mono) || HasLed(LedType::Rgb); + } + + // The setters save to the `led` config section and apply the change at once. A save that raises an RGB brightness + // above kRgbBrightnessWarn logs a warning: a WS2812B is glaring and runs warm well before full output. + + inline constexpr uint16_t kRgbBrightnessWarn = PercentUtils::OfMax(); + + [[nodiscard]] LedMode GetLedMode(); + bool SetLedMode(LedMode mode); + + /// @brief 16-bit, 0-65535 of the LED's full output, for patterns of `signal`. The drivers reduce it to what the LED + /// shows (10 bits on the dithered RGB LED). + [[nodiscard]] uint16_t GetLedBrightness(LedType type, LedSignal signal); + bool SetLedBrightness(LedType type, LedSignal signal, uint16_t brightness); + + /// @brief Several LED settings in one config write (Config::UpdateLedConfig). + bool UpdateLedConfig(Util::FunctionRef update); + void SetCriticalError(); void SetScanningStarted(); - /// Cycles through all LED patterns for visual verification. Blocks for ~20s. + /// Cycles through all LED patterns for visual verification. Blocks for ~20s. Ends every override, and turns new ones + /// down until it is done. void RunLedTest(); } // namespace OpenShock::VisualStateManager diff --git a/components/device_control/src/visual/LedConfig.cpp b/components/device_control/src/visual/LedConfig.cpp new file mode 100644 index 000000000..5038c8eab --- /dev/null +++ b/components/device_control/src/visual/LedConfig.cpp @@ -0,0 +1,153 @@ +#include + +#include "visual/LedConfig.h" + +const char* const TAG = "Config::LedConfig"; + +#include "config/Nvs.h" +#include "config/Parse.h" +#include "Logging.h" + +#include "json/Json.h" + +#include + +using namespace OpenShock::Config; +using OpenShock::LedMode; +using OpenShock::LedSignal; +using OpenShock::LedType; + +static constexpr const char* kNamespace = "led"; +static constexpr const char* kKeyMode = "mode"; +static constexpr const char* kKeyMonoCrit = "monoCrit"; +static constexpr const char* kKeyMonoNorm = "monoNorm"; +static constexpr const char* kKeyRgbCrit = "rgbCrit"; +static constexpr const char* kKeyRgbNorm = "rgbNorm"; + +static constexpr LedMode kDefaultMode = LedMode::All; +static constexpr uint16_t kDefaultMonoCritBright = CONFIG_OPENSHOCK_LED_MONO_CRITICAL_BRIGHTNESS; +static constexpr uint16_t kDefaultMonoNormBright = CONFIG_OPENSHOCK_LED_MONO_NORMAL_BRIGHTNESS; +static constexpr uint16_t kDefaultRgbCritBright = CONFIG_OPENSHOCK_LED_RGB_CRITICAL_BRIGHTNESS; +static constexpr uint16_t kDefaultRgbNormBright = CONFIG_OPENSHOCK_LED_RGB_NORMAL_BRIGHTNESS; + +static constexpr const char* kNamespaces[] = {kNamespace}; +static Section s_section({.name = "led", .order = 7, .namespaces = kNamespaces}); + +LedConfig::LedConfig() + : mode(kDefaultMode) + , monoCriticalBrightness(kDefaultMonoCritBright) + , monoNormalBrightness(kDefaultMonoNormBright) + , rgbCriticalBrightness(kDefaultRgbCritBright) + , rgbNormalBrightness(kDefaultRgbNormBright) +{ +} + +LedConfig::LedConfig(LedMode mode, uint16_t monoCriticalBrightness, uint16_t monoNormalBrightness, uint16_t rgbCriticalBrightness, uint16_t rgbNormalBrightness) + : mode(mode) + , monoCriticalBrightness(monoCriticalBrightness) + , monoNormalBrightness(monoNormalBrightness) + , rgbCriticalBrightness(rgbCriticalBrightness) + , rgbNormalBrightness(rgbNormalBrightness) +{ +} + +uint16_t& LedConfig::Brightness(LedType type, LedSignal signal) +{ + if (type == LedType::Mono) { + return signal == LedSignal::Critical ? monoCriticalBrightness : monoNormalBrightness; + } + return signal == LedSignal::Critical ? rgbCriticalBrightness : rgbNormalBrightness; +} + +void LedConfig::Normalize() +{ + // Exhaustive with no default: -Werror=switch fails the build when LedMode gains a value not listed here + switch (mode) { + case LedMode::All: + case LedMode::CriticalOnly: + case LedMode::Off: + return; + } + OS_LOGW(TAG, "Unknown LED mode %u, using the default", static_cast(std::to_underlying(mode))); + mode = kDefaultMode; +} + +bool LedConfig::FromJSON(JSON::JsonView json) +{ + if (auto done = Parse::ObjectOrDefault(*this, json, TAG)) { + return *done; + } + + // The mode is strict like a boolean: taking the default for a bad value would turn LEDs back on that were turned off + if (!Parse::JsonStrParsed(mode, json, "mode", OpenShock::TryParseLedMode, kDefaultMode)) { + OS_LOGE(TAG, "mode is not one of all, critical or off"); + return false; + } + + monoCriticalBrightness = json["monoCriticalBrightness"].tryGetU16().value_or(kDefaultMonoCritBright); + monoNormalBrightness = json["monoNormalBrightness"].tryGetU16().value_or(kDefaultMonoNormBright); + rgbCriticalBrightness = json["rgbCriticalBrightness"].tryGetU16().value_or(kDefaultRgbCritBright); + rgbNormalBrightness = json["rgbNormalBrightness"].tryGetU16().value_or(kDefaultRgbNormBright); + + return true; +} + +void LedConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const +{ + (void)withSensitiveData; + JSON::objBegin(gen, name); + JSON::objSetString(gen, "mode", OpenShock::LedModeName(mode)); + json_gen_obj_set_int(gen, "monoCriticalBrightness", monoCriticalBrightness); + json_gen_obj_set_int(gen, "monoNormalBrightness", monoNormalBrightness); + json_gen_obj_set_int(gen, "rgbCriticalBrightness", rgbCriticalBrightness); + json_gen_obj_set_int(gen, "rgbNormalBrightness", rgbNormalBrightness); + JSON::objEnd(gen, name); +} + +bool LedConfig::LoadNvs(const char* partition) +{ + auto handle = Nvs::Handle::OpenForLoad(partition, kNamespace); + if (!handle) { + return false; + } + + uint8_t storedMode = std::to_underlying(kDefaultMode); + bool ok = Nvs::Load(*handle, kKeyMode, storedMode, storedMode) && Nvs::Load(*handle, kKeyMonoCrit, monoCriticalBrightness, kDefaultMonoCritBright) && Nvs::Load(*handle, kKeyMonoNorm, monoNormalBrightness, kDefaultMonoNormBright) + && Nvs::Load(*handle, kKeyRgbCrit, rgbCriticalBrightness, kDefaultRgbCritBright) && Nvs::Load(*handle, kKeyRgbNorm, rgbNormalBrightness, kDefaultRgbNormBright); + if (!ok) { + return false; + } + + mode = storedMode <= std::to_underlying(LedMode::Off) ? static_cast(storedMode) : kDefaultMode; + return true; +} + +bool LedConfig::SaveNvs(const char* partition, const LedConfig* previous) const +{ + auto handle = Nvs::Handle::OpenForWrite(partition, kNamespace); + if (!handle) { + return false; + } + return Nvs::Saver(*handle, *this, previous) + .Set(kKeyMode, &LedConfig::mode) + .Set(kKeyMonoCrit, &LedConfig::monoCriticalBrightness) + .Set(kKeyMonoNorm, &LedConfig::monoNormalBrightness) + .Set(kKeyRgbCrit, &LedConfig::rgbCriticalBrightness) + .Set(kKeyRgbNorm, &LedConfig::rgbNormalBrightness) + .Commit(); +} + +LedConfig OpenShock::Config::GetLedConfig() +{ + return s_section.Get(); +} + +bool OpenShock::Config::SetLedConfig(const LedConfig& config) +{ + return s_section.Set(config); +} + +bool OpenShock::Config::UpdateLedConfig(Util::FunctionRef update) +{ + return s_section.Update(update); +} diff --git a/components/device_control/src/visual/LedOverrideState.h b/components/device_control/src/visual/LedOverrideState.h new file mode 100644 index 000000000..044631e6c --- /dev/null +++ b/components/device_control/src/visual/LedOverrideState.h @@ -0,0 +1,122 @@ +#pragma once + +#include "visual/LedOverrides.h" + +#include +#include + +// The two override layers and their deadlines, with no LED, timer or lock of its own: VisualStateManager holds it +// under its apply mutex, passes the time in, and turns what it returns into LED updates and status notices. + +namespace OpenShock::VisualStateManager { + struct LedDisplayContent { + bool isColor; + LedPattern pattern; // Pattern preview + uint16_t red, green, blue; // Color preview: 16 bits per channel + std::optional only; + LedSignal signal; + }; + + struct LedOverrideEnded { + LedOverrideLayer layer; + LedOverrideEnd reason; + uint8_t owner; + uint16_t seq; + }; + + class LedOverrideState { + public: + /// @brief Starts or refreshes the display override. Returns another client's override it replaced. + std::optional SetDisplay(uint8_t owner, uint16_t seq, const LedDisplayContent& content, uint16_t timeoutMs, int64_t nowUs) { return set(m_display, m_displayContent, LedOverrideLayer::Display, owner, seq, content, timeoutMs, nowUs); } + /// @brief Starts or refreshes the draft. Returns another client's draft it replaced. + std::optional SetDraft(uint8_t owner, uint16_t seq, const LedDraftBrightness& draft, uint16_t timeoutMs, int64_t nowUs) { return set(m_draft, m_draftContent, LedOverrideLayer::Draft, owner, seq, draft, timeoutMs, nowUs); } + + [[nodiscard]] bool IsHeldBy(LedOverrideLayer layer, uint8_t owner) const + { + const Slot& slot = this->slot(layer); + return slot.active && slot.owner == owner; + } + + /// @brief Ends the override on `layer`, if any. `seq` replaces the owner's last seq in what is returned (the seq of + /// the request that ended it). + std::optional End(LedOverrideLayer layer, LedOverrideEnd reason, std::optional seq = std::nullopt) + { + Slot& slot = this->slot(layer); + if (!slot.active) { + return std::nullopt; + } + slot.active = false; + if (layer == LedOverrideLayer::Display) { + m_displayContent.reset(); + } else { + m_draftContent.reset(); + } + return LedOverrideEnded {.layer = layer, .reason = reason, .owner = slot.owner, .seq = seq.value_or(slot.seq)}; + } + + /// @brief Ends each override whose deadline has passed, calling `onEnded` for it. + template + void Expire(int64_t nowUs, F&& onEnded) + { + for (LedOverrideLayer layer : {LedOverrideLayer::Display, LedOverrideLayer::Draft}) { + const Slot& slot = this->slot(layer); + if (slot.active && slot.deadlineUs <= nowUs) { + onEnded(*End(layer, LedOverrideEnd::Expired)); + } + } + } + + /// @brief The earliest deadline of the active overrides. + [[nodiscard]] std::optional NextDeadlineUs() const + { + std::optional next; + for (const Slot* slot : {&m_display, &m_draft}) { + if (slot->active && (!next || slot->deadlineUs < *next)) { + next = slot->deadlineUs; + } + } + return next; + } + + [[nodiscard]] std::optional Info(LedOverrideLayer layer) const + { + const Slot& slot = this->slot(layer); + if (!slot.active) { + return std::nullopt; + } + return LedOverrideInfo {.owner = slot.owner, .timeoutMs = slot.timeoutMs}; + } + + [[nodiscard]] const std::optional& Display() const { return m_displayContent; } + [[nodiscard]] const std::optional& Draft() const { return m_draftContent; } + + private: + struct Slot { + bool active = false; + uint8_t owner = 0; + uint16_t seq = 0; + uint16_t timeoutMs = 0; + int64_t deadlineUs = 0; + }; + + template + static std::optional set(Slot& slot, std::optional& content, LedOverrideLayer layer, uint8_t owner, uint16_t seq, const T& value, uint16_t timeoutMs, int64_t nowUs) + { + std::optional replaced; + if (slot.active && slot.owner != owner) { + replaced = LedOverrideEnded {.layer = layer, .reason = LedOverrideEnd::Replaced, .owner = slot.owner, .seq = slot.seq}; + } + slot = Slot {.active = true, .owner = owner, .seq = seq, .timeoutMs = timeoutMs, .deadlineUs = nowUs + static_cast(timeoutMs) * 1000}; + content = value; + return replaced; + } + + Slot& slot(LedOverrideLayer layer) { return layer == LedOverrideLayer::Display ? m_display : m_draft; } + const Slot& slot(LedOverrideLayer layer) const { return layer == LedOverrideLayer::Display ? m_display : m_draft; } + + Slot m_display; + Slot m_draft; + std::optional m_displayContent; + std::optional m_draftContent; + }; +} // namespace OpenShock::VisualStateManager diff --git a/components/device_control/src/visual/VisualStateManager.cpp b/components/device_control/src/visual/VisualStateManager.cpp index eb051df49..847108b1d 100644 --- a/components/device_control/src/visual/VisualStateManager.cpp +++ b/components/device_control/src/visual/VisualStateManager.cpp @@ -11,14 +11,20 @@ const char* const TAG = "VisualStateManager"; #include "events/Events.h" #include "led_drivers/MonoLedDriver.h" #include "led_drivers/RgbLedDriver.h" +#include "LedOverrideState.h" // Private, next to this file #include "Logging.h" #include "SimpleMutex.h" +#include "visual/LedConfig.h" #include +#include #include +#include +#include #include #include +#include // Status LED pins come from the board descriptor stamped into the image (BoardDescriptor.h); // -1 (== GPIO_NUM_NC) means the board has no such LED. @@ -35,6 +41,9 @@ const uint32_t kWiFiScanningFlag = 1 << 7; // Bitmask of when the system is running normally const uint32_t kStatusOKMask = kWebSocketConnectedFlag | kHasIpAddressFlag | kWiFiConnectedFlag; +// Critical states: shown in LedMode::CriticalOnly and at the critical brightness. Their patterns win over every other +const uint32_t kCriticalMask = kCriticalErrorFlag | kEmergencyStoppedFlag | kEmergencyStopActiveClearingFlag | kEmergencyStopAwaitingReleaseFlag; + // 32-bit so the atomic is lock-free. Both need seq_cst so a state change during the LED test isn't lost. static std::atomic s_stateFlags = 0; static std::atomic s_ledTestActive = false; // While set, the LED test owns the LEDs; flags keep tracking live state @@ -42,6 +51,30 @@ static OpenShock::SimpleMutex s_applyMutex; // Serializes reading the fl static std::unique_ptr s_monoLedDriver; static std::unique_ptr s_rgbLedDriver; +// The `led` section's settings, loaded in Init. Each is atomic on its own; a setter re-applies after storing, so a mix +// of old and new values read mid-update is replaced at once. +static std::atomic s_ledMode = OpenShock::LedMode::All; +static std::atomic s_monoCriticalBrightness = CONFIG_OPENSHOCK_LED_MONO_CRITICAL_BRIGHTNESS; // 0-65535 of full output +static std::atomic s_monoNormalBrightness = CONFIG_OPENSHOCK_LED_MONO_NORMAL_BRIGHTNESS; +static std::atomic s_rgbCriticalBrightness = CONFIG_OPENSHOCK_LED_RGB_CRITICAL_BRIGHTNESS; +static std::atomic s_rgbNormalBrightness = CONFIG_OPENSHOCK_LED_RGB_NORMAL_BRIGHTNESS; + +static std::atomic& brightnessSetting(OpenShock::LedType type, OpenShock::LedSignal signal) +{ + bool critical = signal == OpenShock::LedSignal::Critical; + if (type == OpenShock::LedType::Mono) { + return critical ? s_monoCriticalBrightness : s_monoNormalBrightness; + } + return critical ? s_rgbCriticalBrightness : s_rgbNormalBrightness; +} + +// The 16-bit brightness (0-65535) to the mono driver's 0-255 scale, rounded. The RGB driver takes it as is and +// reduces it to its 10-bit output. +static inline uint8_t toDriverBrightness(uint16_t brightness) +{ + return static_cast((static_cast(brightness) * 255 + 32767) / 65535); +} + // Sets or clears `flag` and returns whether that changed it. Atomic per flag, so concurrent setters (the event loop, // SetCriticalError, SetScanningStarted) each see their own change and none is missed. static inline bool setStateFlag(uint32_t flag, bool state) @@ -185,88 +218,333 @@ const MonoLedDriver::State kSolidOnPattern[] = { {true, 100'000} }; -const MonoLedDriver::State kSolidOffPattern[] = { - {false, 100'000} -}; +template +static constexpr uint32_t cycleMs(const State (&pattern)[N]) +{ + uint32_t total = 0; + for (const State& step : pattern) { + total += step.duration; + } + return total; +} -template -static inline void updateVisualStateGPIO(const MonoLedDriver::State (&override)[N]) +// The pattern the state flags show. Critical states first: their patterns win over every other. +static VisualStateManager::LedPattern patternForFlags(uint32_t flags) { - s_monoLedDriver->SetPattern(override); + using VisualStateManager::LedPattern; + if ((flags & kCriticalErrorFlag) != 0) return LedPattern::CriticalError; + if ((flags & kEmergencyStopAwaitingReleaseFlag) != 0) return LedPattern::EStopAwaitingRelease; + if ((flags & kEmergencyStopActiveClearingFlag) != 0) return LedPattern::EStopClearing; + if ((flags & kEmergencyStoppedFlag) != 0) return LedPattern::EStopped; + if ((flags & kWebSocketConnectedFlag) != 0) return LedPattern::Connected; + if ((flags & kHasIpAddressFlag) != 0) return LedPattern::NoBackend; + if ((flags & kWiFiScanningFlag) != 0) return LedPattern::WiFiScanning; + return LedPattern::WiFiDisconnected; } -// Check-Set-Return pattern for setting a pattern based on a flag -#define CSR_PATTERN(manager, flag, pattern) \ - if ((flags & (flag)) != 0) { \ - manager->SetPattern(pattern); \ - return; \ +static bool isCriticalPattern(VisualStateManager::LedPattern pattern) +{ + using VisualStateManager::LedPattern; + switch (pattern) { + case LedPattern::CriticalError: + case LedPattern::EStopped: + case LedPattern::EStopClearing: + case LedPattern::EStopAwaitingRelease: + return true; + case LedPattern::WiFiDisconnected: + case LedPattern::WiFiScanning: + case LedPattern::NoBackend: + case LedPattern::Connected: + return false; } + return false; +} -static void updateVisualStateGPIO(uint32_t flags) +// Calls `fn` with the mono and RGB tables of `pattern` +template +static void withPatternTables(VisualStateManager::LedPattern pattern, F&& fn) { - CSR_PATTERN(s_monoLedDriver, kCriticalErrorFlag, kCriticalErrorPattern); - CSR_PATTERN(s_monoLedDriver, kEmergencyStopAwaitingReleaseFlag, kEmergencyStopAwaitingReleasePattern); - CSR_PATTERN(s_monoLedDriver, kEmergencyStopActiveClearingFlag, kEmergencyStopActiveClearingPattern); - CSR_PATTERN(s_monoLedDriver, kEmergencyStoppedFlag, kEmergencyStoppedPattern); - CSR_PATTERN(s_monoLedDriver, kWebSocketConnectedFlag, kWebSocketConnectedPattern); - CSR_PATTERN(s_monoLedDriver, kHasIpAddressFlag, kWiFiConnectedWithoutWSPattern); - CSR_PATTERN(s_monoLedDriver, kWiFiScanningFlag, kWiFiScanningPattern); - - s_monoLedDriver->SetPattern(kWiFiDisconnectedPattern); + using VisualStateManager::LedPattern; + switch (pattern) { + case LedPattern::CriticalError: + fn(kCriticalErrorPattern, kCriticalErrorRGBPattern); + return; + case LedPattern::EStopAwaitingRelease: + fn(kEmergencyStopAwaitingReleasePattern, kEmergencyStopAwaitingReleaseRGBPattern); + return; + case LedPattern::EStopClearing: + fn(kEmergencyStopActiveClearingPattern, kEmergencyStopActiveClearingRGBPattern); + return; + case LedPattern::EStopped: + fn(kEmergencyStoppedPattern, kEmergencyStoppedRGBPattern); + return; + case LedPattern::Connected: + fn(kWebSocketConnectedPattern, kWebSocketConnectedRGBPattern); + return; + case LedPattern::NoBackend: + fn(kWiFiConnectedWithoutWSPattern, kWiFiConnectedWithoutWSRGBPattern); + return; + case LedPattern::WiFiScanning: + fn(kWiFiScanningPattern, kWiFiScanningRGBPattern); + return; + case LedPattern::WiFiDisconnected: + fn(kWiFiDisconnectedPattern, kWiFiDisconnectedRGBPattern); + return; + } + fn(kWiFiDisconnectedPattern, kWiFiDisconnectedRGBPattern); // Unknown: the pattern with nothing going on } -static void updateVisualStateRGB(uint32_t flags) +// How long a preview of `pattern` lasts: about two cycles of what the board shows it on +static uint16_t patternPreviewMs(VisualStateManager::LedPattern pattern) { - CSR_PATTERN(s_rgbLedDriver, kCriticalErrorFlag, kCriticalErrorRGBPattern); - CSR_PATTERN(s_rgbLedDriver, kEmergencyStopAwaitingReleaseFlag, kEmergencyStopAwaitingReleaseRGBPattern); - CSR_PATTERN(s_rgbLedDriver, kEmergencyStopActiveClearingFlag, kEmergencyStopActiveClearingRGBPattern); - CSR_PATTERN(s_rgbLedDriver, kEmergencyStoppedFlag, kEmergencyStoppedRGBPattern); - CSR_PATTERN(s_rgbLedDriver, kWebSocketConnectedFlag, kWebSocketConnectedRGBPattern); - CSR_PATTERN(s_rgbLedDriver, kHasIpAddressFlag, kWiFiConnectedWithoutWSRGBPattern); - CSR_PATTERN(s_rgbLedDriver, kWiFiScanningFlag, kWiFiScanningRGBPattern); + uint32_t cycle = 0; + withPatternTables(pattern, [&cycle](const auto& mono, const auto& rgb) { cycle = s_rgbLedDriver != nullptr ? cycleMs(rgb) : cycleMs(mono); }); + return static_cast(std::clamp(cycle * 2, VisualStateManager::kPatternPreviewMinMs, VisualStateManager::kPatternPreviewMaxMs)); +} + +// Overrides and the status they report. All guarded by s_applyMutex. +static VisualStateManager::LedOverrideState s_overrides; +static std::optional s_postedStatus; // The status the last notice reported +static esp_timer_handle_t s_overrideTimer = nullptr; // Fires at the next override deadline - s_rgbLedDriver->SetPattern(kWiFiDisconnectedRGBPattern); +// The brightness `type` shows `signal` patterns at: the draft's while one is held, else the saved one +static uint16_t effectiveBrightness(LedType type, LedSignal signal) +{ + const auto& draft = s_overrides.Draft(); + if (draft.has_value()) { + const auto& pair = type == LedType::Mono ? draft->mono : draft->rgb; + if (pair.has_value()) { + return signal == LedSignal::Critical ? pair->critical : pair->normal; + } + } + return brightnessSetting(type, signal).load(); } -static void applyVisualState(uint32_t flags) +// `monoSolid`: with both LEDs, the mono LED stays on solid for this pattern (the hub is fully up) rather than dark. +// `showAll` ignores the LED mode, for previews and the LED test; the brightness still applies. +static void applyPattern(VisualStateManager::LedPattern pattern, bool monoSolid, bool showAll) { bool gpioActive = s_monoLedDriver != nullptr; bool rgbActive = s_rgbLedDriver != nullptr; - if (gpioActive && rgbActive) { - if (flags == kStatusOKMask) { - updateVisualStateGPIO(kSolidOnPattern); - } else { - updateVisualStateGPIO(kSolidOffPattern); + bool critical = isCriticalPattern(pattern); + LedMode mode = showAll ? LedMode::All : s_ledMode.load(); + if (mode == LedMode::Off || (mode == LedMode::CriticalOnly && !critical)) { + // Off for real: the drivers rest their hardware rather than keep writing black + if (gpioActive) { + s_monoLedDriver->Sleep(); + } + if (rgbActive) { + s_rgbLedDriver->Sleep(); } - updateVisualStateRGB(flags); return; } + // Before SetPattern: the pattern task applies the brightness with each step, and SetPattern restarts the pattern, + // so even a long solid step picks up a new brightness at once + LedSignal signal = critical ? LedSignal::Critical : LedSignal::Normal; if (gpioActive) { - updateVisualStateGPIO(flags); - return; + s_monoLedDriver->SetBrightness(toDriverBrightness(effectiveBrightness(LedType::Mono, signal))); } - if (rgbActive) { - updateVisualStateRGB(flags); - return; + s_rgbLedDriver->SetBrightness16(effectiveBrightness(LedType::Rgb, signal)); } - OS_LOGW(TAG, "Trying to update visual state, but no LED is active!"); + withPatternTables(pattern, [&](const auto& monoPattern, const auto& rgbPattern) { + if (gpioActive && rgbActive) { + if (critical) { + s_monoLedDriver->SetPattern(monoPattern); // E-Stop and critical errors on both LEDs, at their critical brightness + } else if (monoSolid) { + s_monoLedDriver->SetPattern(kSolidOnPattern); + } else { + s_monoLedDriver->Sleep(); // The RGB LED shows the other states + } + s_rgbLedDriver->SetPattern(rgbPattern); + } else if (gpioActive) { + s_monoLedDriver->SetPattern(monoPattern); + } else if (rgbActive) { + s_rgbLedDriver->SetPattern(rgbPattern); + } else { + OS_LOGW(TAG, "Trying to update visual state, but no LED is active!"); + } + }); } -static void updateVisualState() +static void applyVisualState(uint32_t flags, bool showAll = false) { - OpenShock::ScopedLock lock__(&s_applyMutex); + applyPattern(patternForFlags(flags), flags == kStatusOKMask, showAll); +} + +static void applyColorPreview(const VisualStateManager::LedDisplayContent& preview) +{ + bool monoLit = !preview.only.has_value() || *preview.only == LedType::Mono; + bool rgbLit = !preview.only.has_value() || *preview.only == LedType::Rgb; + + if (s_monoLedDriver != nullptr) { + s_monoLedDriver->SetBrightness(toDriverBrightness(effectiveBrightness(LedType::Mono, preview.signal))); + bool on = monoLit && (preview.red | preview.green | preview.blue) != 0; + if (on) { + s_monoLedDriver->SetPattern(kSolidOnPattern); + } else { + s_monoLedDriver->Sleep(); + } + } + + if (s_rgbLedDriver != nullptr) { + if (rgbLit) { + s_rgbLedDriver->SetBrightness16(effectiveBrightness(LedType::Rgb, preview.signal)); + const RgbLedDriver::RGBState16 color[] = { + {preview.red, preview.green, preview.blue, 100'000} + }; + s_rgbLedDriver->SetPattern(color); + } else { + s_rgbLedDriver->Sleep(); + } + } +} +// Caller holds s_applyMutex +static void applyCurrentLocked() +{ if (s_ledTestActive.load()) { return; // Re-applied from the live flags when the test finishes } + const auto& display = s_overrides.Display(); + if (display.has_value()) { + if (display->isColor) { + applyColorPreview(*display); + } else { + applyPattern(display->pattern, display->pattern == VisualStateManager::LedPattern::Connected, true); + } + return; + } + applyVisualState(s_stateFlags.load()); } +// Caller holds s_applyMutex +static VisualStateManager::LedStatus statusLocked() +{ + using namespace VisualStateManager; + + LedStatus status { + .showing = LedShowing::Live, + .pattern = patternForFlags(s_stateFlags.load()), + .display = s_overrides.Info(LedOverrideLayer::Display), + .draft = s_overrides.Info(LedOverrideLayer::Draft), + }; + + const auto& display = s_overrides.Display(); + if (s_ledTestActive.load()) { + status.showing = LedShowing::Test; + } else if (display.has_value()) { + status.showing = display->isColor ? LedShowing::Color : LedShowing::Pattern; + if (!display->isColor) { + status.pattern = display->pattern; + } + } else { + LedMode mode = s_ledMode.load(); + if (mode == LedMode::Off || (mode == LedMode::CriticalOnly && !isCriticalPattern(status.pattern))) { + status.showing = LedShowing::Hidden; + } + } + return status; +} + +// Caller holds s_applyMutex. Points the timer at the next override deadline. +static void rearmOverrideTimerLocked() +{ + if (s_overrideTimer == nullptr) { + return; + } + esp_timer_stop(s_overrideTimer); // ESP_ERR_INVALID_STATE when not running: fine + auto deadline = s_overrides.NextDeadlineUs(); + if (!deadline.has_value()) { + return; + } + int64_t waitUs = std::max(*deadline - esp_timer_get_time(), 1000); + esp_err_t err = esp_timer_start_once(s_overrideTimer, static_cast(waitUs)); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to start the LED override timer: %s", esp_err_to_name(err)); + } +} + +namespace { + // Notices gathered under s_applyMutex, posted once it is released: a portal handler reads the status back + struct PendingNotices { + std::array items; + std::size_t count = 0; + + void ended(const VisualStateManager::LedOverrideEnded& ended) { add({.kind = VisualStateManager::LedStatusNotice::Kind::Ended, .layer = ended.layer, .reason = ended.reason, .error = {}, .owner = ended.owner, .seq = ended.seq}); } + void ended(const std::optional& ended) + { + if (ended.has_value()) { + this->ended(*ended); + } + } + void failed(uint8_t owner, uint16_t seq, VisualStateManager::LedRequestError error) { add({.kind = VisualStateManager::LedStatusNotice::Kind::Failed, .layer = {}, .reason = {}, .error = error, .owner = owner, .seq = seq}); } + void add(const VisualStateManager::LedStatusNotice& notice) + { + if (count < items.size()) { + items[count++] = notice; + } + } + }; +} // namespace + +static void postNotices(const PendingNotices& notices) +{ + for (std::size_t i = 0; i < notices.count; ++i) { + esp_err_t err = esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_LED_STATUS_CHANGED, ¬ices.items[i], sizeof(notices.items[i]), pdMS_TO_TICKS(10)); + if (err != ESP_OK) { + OS_LOGW(TAG, "Failed to post the LED status: %s", esp_err_to_name(err)); + } + } +} + +// Every change to the LEDs goes through here: `change` edits the state under s_applyMutex, then the LEDs, the override +// timer and the portals are brought up to date. A status change with nothing else to report posts StatusChanged. +template +static void updateLeds(F&& change) +{ + PendingNotices notices; + { + OpenShock::ScopedLock lock__(&s_applyMutex); + change(notices); + applyCurrentLocked(); + rearmOverrideTimerLocked(); + + auto status = statusLocked(); + bool changed = s_postedStatus != status; + s_postedStatus = status; + if (changed && notices.count == 0) { + notices.add({.kind = VisualStateManager::LedStatusNotice::Kind::StatusChanged, .layer = {}, .reason = {}, .error = {}, .owner = 0, .seq = 0}); + } + } + postNotices(notices); +} + +static void updateVisualState() +{ + updateLeds([](PendingNotices&) { }); +} + +// esp_timer callback (esp_timer task, small stack): only hands the expiry to the event loop, which does the LED work. +// The expiry is a sweep of whatever is due, so a late or extra tick is harmless; a lost one is retried. +static void handleOverrideTimer(void* arg) +{ + (void)arg; + if (esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_LED_OVERRIDE_DUE, nullptr, 0, 0) != ESP_OK) { + esp_timer_start_once(s_overrideTimer, 50'000); + } +} + +static void expireOverrides() +{ + updateLeds([](PendingNotices& notices) { s_overrides.Expire(esp_timer_get_time(), [¬ices](const VisualStateManager::LedOverrideEnded& ended) { notices.ended(ended); }); }); +} + static void handleEspWiFiEvent(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { (void)event_handler_arg; @@ -358,6 +636,13 @@ static void handleOpenShockEvent(void* event_handler_arg, esp_event_base_t event case OPENSHOCK_EVENT_WIFI_STATE_CHANGED: // WiFi state is tracked from the IDF WIFI_EVENT/IP_EVENT handlers above. return; + case OPENSHOCK_EVENT_LED_OVERRIDE_DUE: + expireOverrides(); + return; + case OPENSHOCK_EVENT_DEVICE_ACTIVITY: + case OPENSHOCK_EVENT_LED_CONFIG_CHANGED: // Posted from here + case OPENSHOCK_EVENT_LED_STATUS_CHANGED: // Posted from here + return; default: OS_LOGW(TAG, "Received unknown event ID: %d", static_cast(event_id)); return; @@ -368,12 +653,27 @@ static void handleOpenShockEvent(void* event_handler_arg, esp_event_base_t event } } +// Copies the `led` section into the atomics applyVisualState reads +static void loadLedConfig() +{ + const Config::LedConfig ledConfig = Config::GetLedConfig(); + s_ledMode.store(ledConfig.mode); + for (LedType type : {LedType::Mono, LedType::Rgb}) { + for (LedSignal signal : {LedSignal::Critical, LedSignal::Normal}) { + brightnessSetting(type, signal).store(ledConfig.Brightness(type, signal)); + } + } +} + bool VisualStateManager::Init() { bool ledActive = false; const BoardDescriptor& board = Board::Get(); + // Before the drivers, so they start at the configured brightness + loadLedConfig(); + // The pins come from the stamped descriptor, so they are data, not something the build checked. A pin this // chip cannot drive leaves that LED off rather than failing Init: that would restart-loop a normal boot and // roll back an OTA boot over an indicator. ValidateBoardDescriptor() has already logged the specifics. @@ -381,7 +681,7 @@ bool VisualStateManager::Init() if (!OpenShock::IsValidOutputPin(board.ledGpio)) { OS_LOGW(TAG, "Built-in LED pin %hhi is not usable on this board, leaving the LED disabled", board.ledGpio); } else { - s_monoLedDriver = std::make_unique(static_cast(board.ledGpio)); + s_monoLedDriver = std::make_unique(static_cast(board.ledGpio), toDriverBrightness(s_monoNormalBrightness.load())); if (!s_monoLedDriver->IsValid()) { OS_LOGE(TAG, "Failed to initialize built-in LED manager"); return false; @@ -394,12 +694,11 @@ bool VisualStateManager::Init() if (!OpenShock::IsValidOutputPin(board.ws2812bGpio)) { OS_LOGW(TAG, "RGB LED pin %hhi is not usable on this board, leaving the LED disabled", board.ws2812bGpio); } else { - s_rgbLedDriver = std::make_unique(static_cast(board.ws2812bGpio)); + s_rgbLedDriver = std::make_unique(static_cast(board.ws2812bGpio), s_rgbNormalBrightness.load()); if (!s_rgbLedDriver->IsValid()) { OS_LOGE(TAG, "Failed to initialize RGB LED manager"); return false; } - s_rgbLedDriver->SetBrightness(20); ledActive = true; } } @@ -409,7 +708,19 @@ bool VisualStateManager::Init() return true; } - esp_err_t err; + const esp_timer_create_args_t overrideTimerArgs = { + .callback = handleOverrideTimer, + .arg = nullptr, + .dispatch_method = ESP_TIMER_TASK, + .name = "LedOverride", + .skip_unhandled_events = true, + }; + esp_err_t err = esp_timer_create(&overrideTimerArgs, &s_overrideTimer); + if (err != ESP_OK) { + // Overrides only (they are turned down without it); the LEDs still show the hub's state + OS_LOGE(TAG, "Failed to create the LED override timer: %s", esp_err_to_name(err)); + s_overrideTimer = nullptr; + } err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, handleEspWiFiEvent, nullptr); if (err != ESP_OK) { @@ -435,6 +746,213 @@ bool VisualStateManager::Init() return true; } +LedMode VisualStateManager::GetLedMode() +{ + return s_ledMode.load(); +} + +bool VisualStateManager::SetLedMode(LedMode mode) +{ + return UpdateLedConfig([&](Config::LedConfig& led) { + if (led.mode == mode) { + return false; + } + led.mode = mode; + return true; + }); +} + +uint16_t VisualStateManager::GetLedBrightness(LedType type, LedSignal signal) +{ + return brightnessSetting(type, signal).load(); +} + +bool VisualStateManager::SetLedBrightness(LedType type, LedSignal signal, uint16_t value) +{ + return UpdateLedConfig([&](Config::LedConfig& led) { + uint16_t& brightness = led.Brightness(type, signal); + if (brightness == value) { + return false; + } + brightness = value; // The section caps it (LedConfig::Normalize) + return true; + }); +} + +bool VisualStateManager::UpdateLedConfig(Util::FunctionRef update) +{ + if (!Config::UpdateLedConfig(update)) { + OS_LOGE(TAG, "Failed to save the LED settings to config"); + return false; + } + + // Under s_applyMutex so two updates can't store their snapshots out of order + updateLeds([](PendingNotices&) { + uint16_t rgbBefore[] = {s_rgbCriticalBrightness.load(), s_rgbNormalBrightness.load()}; + loadLedConfig(); + uint16_t rgbAfter[] = {s_rgbCriticalBrightness.load(), s_rgbNormalBrightness.load()}; + for (std::size_t i = 0; i < std::size(rgbAfter); ++i) { + if (s_rgbLedDriver != nullptr && rgbAfter[i] > VisualStateManager::kRgbBrightnessWarn && rgbAfter[i] != rgbBefore[i]) { + OS_LOGW(TAG, "RGB LED %s brightness set to %u/65535: above %u a WS2812B is glaring and runs warm", i == 0 ? "critical" : "normal", rgbAfter[i], VisualStateManager::kRgbBrightnessWarn); + } + } + }); + + // Tells open portals, which otherwise only get the settings on connect. Posted after s_applyMutex is released: a + // handler reads the settings back. + esp_err_t err = esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_LED_CONFIG_CHANGED, nullptr, 0, pdMS_TO_TICKS(10)); + if (err != ESP_OK) { + OS_LOGW(TAG, "Failed to post the LED settings change: %s", esp_err_to_name(err)); + } + return true; +} + +bool VisualStateManager::HasLed(LedType type) +{ + return type == LedType::Mono ? s_monoLedDriver != nullptr : s_rgbLedDriver != nullptr; +} + +VisualStateManager::LedStatus VisualStateManager::GetLedStatus() +{ + OpenShock::ScopedLock lock__(&s_applyMutex); + return statusLocked(); +} + +// Caller holds s_applyMutex. Whether an override may start now; reports why not. +static bool canOverrideLocked(PendingNotices& notices, uint8_t owner, uint16_t seq) +{ + using VisualStateManager::LedRequestError; + if (!VisualStateManager::HasAnyLed() || s_overrideTimer == nullptr) { + notices.failed(owner, seq, LedRequestError::NoLed); // No timer: nothing would end it + return false; + } + if (s_ledTestActive.load()) { + notices.failed(owner, seq, LedRequestError::LedTestRunning); + return false; + } + return true; +} + +void VisualStateManager::PreviewLedPattern(uint8_t owner, uint16_t seq, LedPattern pattern) +{ + updateLeds([&](PendingNotices& notices) { + if (!canOverrideLocked(notices, owner, seq)) return; + LedDisplayContent content {.isColor = false, .pattern = pattern, .red = 0, .green = 0, .blue = 0, .only = std::nullopt, .signal = LedSignal::Normal}; + notices.ended(s_overrides.SetDisplay(owner, seq, content, patternPreviewMs(pattern), esp_timer_get_time())); + }); +} + +void VisualStateManager::PreviewLedColor(uint8_t owner, uint16_t seq, uint16_t red, uint16_t green, uint16_t blue, std::optional only, LedSignal signal) +{ + updateLeds([&](PendingNotices& notices) { + if (!canOverrideLocked(notices, owner, seq)) return; + LedDisplayContent content {.isColor = true, .pattern = LedPattern::WiFiDisconnected, .red = red, .green = green, .blue = blue, .only = only, .signal = signal}; + notices.ended(s_overrides.SetDisplay(owner, seq, content, kColorPreviewTimeoutMs, esp_timer_get_time())); + }); +} + +void VisualStateManager::SetLedDraft(uint8_t owner, uint16_t seq, const LedDraftBrightness& requested) +{ + // Only the LEDs the board has; the portal never shows the others + LedDraftBrightness draft = requested; + if (!HasLed(LedType::Mono)) draft.mono.reset(); + if (!HasLed(LedType::Rgb)) draft.rgb.reset(); + + updateLeds([&](PendingNotices& notices) { + if (!canOverrideLocked(notices, owner, seq)) return; + if (!draft.mono.has_value() && !draft.rgb.has_value()) { + notices.failed(owner, seq, LedRequestError::NoLed); + return; + } + notices.ended(s_overrides.SetDraft(owner, seq, draft, kDraftBrightnessTimeoutMs, esp_timer_get_time())); + }); +} + +void VisualStateManager::StopLedOverride(uint8_t owner, uint16_t seq, LedOverrideLayer layer) +{ + updateLeds([&](PendingNotices& notices) { + if (!s_overrides.IsHeldBy(layer, owner)) { + notices.failed(owner, seq, LedRequestError::NotHeld); + return; + } + notices.ended(s_overrides.End(layer, LedOverrideEnd::Stopped, seq)); + }); +} + +void VisualStateManager::CommitLedDraft(uint8_t owner, uint16_t seq) +{ + std::optional draft; + { + OpenShock::ScopedLock lock__(&s_applyMutex); + if (s_overrides.IsHeldBy(LedOverrideLayer::Draft, owner)) { + draft = s_overrides.Draft(); + } + } + if (!draft.has_value()) { + ReportLedRequestFailed(owner, seq, LedRequestError::NotHeld); + return; + } + + // The draft stays applied while saving, so the LEDs don't flicker back to the old brightness in between + bool saved = UpdateLedConfig([&](Config::LedConfig& led) { + bool changed = false; + auto apply = [&](LedType type, const std::optional& pair) { + if (!pair.has_value()) return; + for (LedSignal signal : {LedSignal::Critical, LedSignal::Normal}) { + uint16_t value = signal == LedSignal::Critical ? pair->critical : pair->normal; + uint16_t& field = led.Brightness(type, signal); + if (field != value) { + field = value; // The section caps it (LedConfig::Normalize) + changed = true; + } + } + }; + apply(LedType::Mono, draft->mono); + apply(LedType::Rgb, draft->rgb); + return changed; + }); + if (!saved) { + ReportLedRequestFailed(owner, seq, LedRequestError::SaveFailed); + return; + } + + // Ends whatever draft the owner holds by now: anything it sent while saving starts a new one with its next message + updateLeds([&](PendingNotices& notices) { + if (s_overrides.IsHeldBy(LedOverrideLayer::Draft, owner)) { + notices.ended(s_overrides.End(LedOverrideLayer::Draft, LedOverrideEnd::Committed, seq)); + } + }); +} + +void VisualStateManager::SetLedModeFor(uint8_t owner, uint16_t seq, LedMode mode) +{ + if (!HasAnyLed()) { + ReportLedRequestFailed(owner, seq, LedRequestError::NoLed); + return; + } + if (!SetLedMode(mode)) { + ReportLedRequestFailed(owner, seq, LedRequestError::SaveFailed); + } +} + +void VisualStateManager::ReportLedRequestFailed(uint8_t owner, uint16_t seq, LedRequestError error) +{ + PendingNotices notices; + notices.failed(owner, seq, error); + postNotices(notices); +} + +void VisualStateManager::ReleaseLedOverrides(uint8_t owner) +{ + updateLeds([&](PendingNotices& notices) { + for (LedOverrideLayer layer : {LedOverrideLayer::Display, LedOverrideLayer::Draft}) { + if (s_overrides.IsHeldBy(layer, owner)) { + notices.ended(s_overrides.End(layer, LedOverrideEnd::OwnerLeft)); + } + } + }); +} + void VisualStateManager::SetCriticalError() { if (setStateFlag(kCriticalErrorFlag, true)) { @@ -452,13 +970,21 @@ void VisualStateManager::SetScanningStarted() void VisualStateManager::RunLedTest() { // Take over the LEDs; state events keep updating s_stateFlags but don't touch the LEDs until the test ends. - // Under s_applyMutex so an update already in flight finishes before the test starts. - { - ScopedLock lock__(&s_applyMutex); + // Under s_applyMutex so an update already in flight finishes before the test starts. Overrides end, and new ones are + // turned down until the test is done. + bool alreadyRunning = false; + updateLeds([&alreadyRunning](PendingNotices& notices) { if (s_ledTestActive.exchange(true)) { - OS_LOGW(TAG, "LED test already running"); + alreadyRunning = true; return; } + for (VisualStateManager::LedOverrideLayer layer : {VisualStateManager::LedOverrideLayer::Display, VisualStateManager::LedOverrideLayer::Draft}) { + notices.ended(s_overrides.End(layer, VisualStateManager::LedOverrideEnd::LedTest)); + } + }); + if (alreadyRunning) { + OS_LOGW(TAG, "LED test already running"); + return; } OS_LOGI(TAG, "=== LED Test Started ==="); @@ -484,16 +1010,12 @@ void VisualStateManager::RunLedTest() vTaskDelay(pdMS_TO_TICKS(20)); } - // Restore default brightness - s_monoLedDriver->SetBrightness(255); s_monoLedDriver->ClearPattern(); vTaskDelay(pdMS_TO_TICKS(500)); } // --- Phase 2: RGB LED color test --- if (s_rgbLedDriver != nullptr) { - uint8_t savedBrightness = 20; // default from Init() - s_rgbLedDriver->SetBrightness(255); OS_LOGI(TAG, " [RGB] Red"); @@ -553,7 +1075,6 @@ void VisualStateManager::RunLedTest() vTaskDelay(pdMS_TO_TICKS(20)); } - s_rgbLedDriver->SetBrightness(savedBrightness); s_rgbLedDriver->ClearPattern(); vTaskDelay(pdMS_TO_TICKS(500)); } @@ -578,12 +1099,11 @@ void VisualStateManager::RunLedTest() OS_LOGI(TAG, " State patterns:"); for (const auto& step : steps) { OS_LOGI(TAG, " %s", step.name); - applyVisualState(step.flags); + applyVisualState(step.flags, true); vTaskDelay(pdMS_TO_TICKS(step.durationMs)); } // Show the live state again (including anything that changed during the test, e.g. an E-Stop) OS_LOGI(TAG, "=== LED Test Complete, restoring state ==="); - s_ledTestActive.store(false); - updateVisualState(); + updateLeds([](PendingNotices&) { s_ledTestActive.store(false); }); } diff --git a/components/events/include/events/Events.h b/components/events/include/events/Events.h index df827937c..1e7eb2e4c 100644 --- a/components/events/include/events/Events.h +++ b/components/events/include/events/Events.h @@ -13,6 +13,9 @@ enum { OPENSHOCK_EVENT_GATEWAY_CLIENT_STATE_CHANGED, // Event for when the gateway connection state changes OPENSHOCK_EVENT_WIFI_STATE_CHANGED, // Event for when the WiFi station connectivity state changes OPENSHOCK_EVENT_DEVICE_ACTIVITY, // A shocker command was accepted (no payload) + OPENSHOCK_EVENT_LED_CONFIG_CHANGED, // The LED settings were saved and applied (no payload) + OPENSHOCK_EVENT_LED_STATUS_CHANGED, // What the LEDs show changed, an override ended, or an LED request failed (VisualStateManager::LedStatusNotice) + OPENSHOCK_EVENT_LED_OVERRIDE_DUE, // An LED override deadline passed; VisualStateManager's own timer tick (no payload) }; // OPENSHOCK_EVENT_DEVICE_ACTIVITY is posted at most this often, so a listener's view of the last activity can be this diff --git a/components/flatbuffers/flatbuffers b/components/flatbuffers/flatbuffers index 03fffb25e..b8431fbcd 160000 --- a/components/flatbuffers/flatbuffers +++ b/components/flatbuffers/flatbuffers @@ -1 +1 @@ -Subproject commit 03fffb25e2d777462b719cb4964249c30b19d58f +Subproject commit b8431fbcd7a5c71817f314e18b332c0648554efa diff --git a/components/led_drivers/Kconfig b/components/led_drivers/Kconfig new file mode 100644 index 000000000..4b7d27d91 --- /dev/null +++ b/components/led_drivers/Kconfig @@ -0,0 +1,17 @@ +menu "OpenShock LED drivers" + + config OPENSHOCK_RGB_LED_DITHERING + bool "10-bit dithering on the RGB LED" + default y + help + The WS2812B takes 8 bits per color. With dithering the driver shows 10: it sends + 4 frames that each round a channel to one of the two nearest 8-bit levels, and the + RMT hardware repeats them on its own while the color holds, so the average lands + on the quarter step in between at no CPU cost. + + The looped frames need several RMT memory blocks. Chips without a transmit channel + to spare for the RF transmitter after that (ESP32-C3) round to 8 bits regardless. + + Off, each color is rounded to the nearest 8-bit level. + +endmenu diff --git a/components/led_drivers/include/led_drivers/MonoLedDriver.h b/components/led_drivers/include/led_drivers/MonoLedDriver.h index 24562fd02..b0e378ea3 100644 --- a/components/led_drivers/include/led_drivers/MonoLedDriver.h +++ b/components/led_drivers/include/led_drivers/MonoLedDriver.h @@ -22,7 +22,8 @@ namespace OpenShock { public: using State = MonoLedState; - MonoLedDriver(gpio_num_t gpioPin); + /// @param brightness The starting brightness (0-255), until SetBrightness. + MonoLedDriver(gpio_num_t gpioPin, uint8_t brightness); ~MonoLedDriver() override; bool IsValid() const { return m_gpioPin != GPIO_NUM_NC; } @@ -31,6 +32,8 @@ namespace OpenShock { protected: void apply(const State& state) override; + bool enterSleep() override; + void exitSleep() override; private: gpio_num_t m_gpioPin; diff --git a/components/led_drivers/include/led_drivers/PatternPlayer.h b/components/led_drivers/include/led_drivers/PatternPlayer.h index ad3ff0417..eab591989 100644 --- a/components/led_drivers/include/led_drivers/PatternPlayer.h +++ b/components/led_drivers/include/led_drivers/PatternPlayer.h @@ -16,7 +16,8 @@ namespace OpenShock { /// SetPattern() swaps the pattern and wakes the task through a task notification, so a change takes effect right /// away and the caller (often the default event loop) never waits for a task to be torn down and recreated. /// `State` must have a `uint32_t duration` (milliseconds); the derived driver writes a state to the hardware in - /// apply(). Derived destructors must call stopTask() before releasing anything apply() uses. + /// apply(), and puts it to rest in enterSleep() / brings it back in exitSleep(). All three run on the pattern task. + /// Derived destructors must call stopTask() before releasing anything those use. template class PatternPlayer { DISABLE_COPY(PatternPlayer); @@ -28,6 +29,7 @@ namespace OpenShock { { ScopedLock lock__(&m_patternMutex); m_pattern.assign(pattern, pattern + patternLength); + m_sleep = false; ++m_generation; } wake(); @@ -41,11 +43,32 @@ namespace OpenShock { /// @brief Stops playing; the LED keeps its last state. void ClearPattern() { SetPattern(nullptr, 0); } + /// @brief Turns the LED off and lets its hardware rest (enterSleep), with the task blocked and not waking, until + /// the next SetPattern brings it back (exitSleep). + void Sleep() + { + { + ScopedLock lock__(&m_patternMutex); + m_pattern.clear(); + m_sleep = true; + ++m_generation; + } + wake(); + } + protected: PatternPlayer() = default; virtual ~PatternPlayer() { stopTask(); } virtual void apply(const State& state) = 0; + /// @brief Turns the LED off and stops what drives it, so the hardware rests. False when the LED may still be lit: + /// the hardware stays awake and enterSleep() is tried again. + virtual bool enterSleep() = 0; + /// @brief Undoes enterSleep(); the next apply() then shows a state as usual. + virtual void exitSleep() = 0; + + /// @brief Whether the hardware is resting. Pattern task only, or after stopTask() (a destructor deciding what to undo). + bool isAsleep() const { return m_asleep; } bool startTask(const char* name, uint32_t stackSize) { @@ -61,16 +84,29 @@ namespace OpenShock { { std::vector pattern; uint32_t generation = 0; + bool sleep = false; while (!m_task.stopRequested()) { { ScopedLock lock__(&m_patternMutex); if (generation != m_generation) { pattern = m_pattern; + sleep = m_sleep; generation = m_generation; } } + if (sleep && !m_asleep) { + if (!enterSleep()) { + TaskUtils::WaitForNotify(pdMS_TO_TICKS(kSleepRetryMs)); // Or sooner, for a new pattern or a stop + continue; + } + m_asleep = true; + } else if (!pattern.empty() && m_asleep) { + exitSleep(); + m_asleep = false; + } + if (pattern.empty()) { TaskUtils::WaitForNotify(portMAX_DELAY); // Nothing to play until the next SetPattern / stop continue; @@ -87,9 +123,13 @@ namespace OpenShock { } } + static constexpr uint32_t kSleepRetryMs = 100; + SimpleMutex m_patternMutex; - std::vector m_pattern; // Guarded by m_patternMutex - uint32_t m_generation = 0; // Guarded by m_patternMutex; bumped on every SetPattern + std::vector m_pattern; // Guarded by m_patternMutex + uint32_t m_generation = 0; // Guarded by m_patternMutex; bumped on every SetPattern and Sleep + bool m_sleep = false; // Guarded by m_patternMutex; Sleep() asked for the hardware to rest + bool m_asleep = false; // Pattern task only: enterSleep() ran and exitSleep() has not ManagedTask m_task {"PatternPlayer", "LED pattern task"}; }; diff --git a/components/led_drivers/include/led_drivers/RgbLedDriver.h b/components/led_drivers/include/led_drivers/RgbLedDriver.h index 869cd24e9..e8057fe55 100644 --- a/components/led_drivers/include/led_drivers/RgbLedDriver.h +++ b/components/led_drivers/include/led_drivers/RgbLedDriver.h @@ -10,9 +10,11 @@ #include #include +#include #include namespace OpenShock { + // 8 bits per channel: what the pattern tables are written in struct RgbLedState { uint8_t red; uint8_t green; @@ -20,32 +22,67 @@ namespace OpenShock { uint32_t duration; }; - class RgbLedDriver : public PatternPlayer { + // 16 bits per channel (0-65535). The WS2812B takes 8; the driver shows 10 by dithering over 4 frames (see apply()). + struct RgbLedState16 { + uint16_t red; + uint16_t green; + uint16_t blue; + uint32_t duration; + }; + + class RgbLedDriver : public PatternPlayer { DISABLE_DEFAULT(RgbLedDriver); DISABLE_COPY(RgbLedDriver); DISABLE_MOVE(RgbLedDriver); public: - using RGBState = RgbLedState; + using RGBState = RgbLedState; + using RGBState16 = RgbLedState16; - RgbLedDriver(gpio_num_t gpioPin); + /// @param brightness The starting brightness (0-65535), until SetBrightness16. + RgbLedDriver(gpio_num_t gpioPin, uint16_t brightness); ~RgbLedDriver() override; bool IsValid() const { return m_gpioPin != GPIO_NUM_NC; } - // Range: 0-255 - void SetBrightness(uint8_t brightness) { m_brightness.store(brightness); } + // 16-bit patterns + using PatternPlayer::SetPattern; + + // 8-bit patterns, widened to 16 bits per channel (255 -> 65535) + void SetPattern(const RgbLedState* pattern, std::size_t patternLength); + template + inline void SetPattern(const RgbLedState (&pattern)[N]) + { + SetPattern(pattern, N); + } + + /// @brief 0-255; the same as SetBrightness16(brightness * 257). + void SetBrightness(uint8_t brightness) { SetBrightness16(static_cast(brightness * 257)); } + /// @brief 0-65535. Takes effect with the next SetPattern, like the 8-bit form. + void SetBrightness16(uint16_t brightness) { m_brightness.store(brightness); } protected: - void apply(const RGBState& state) override; + void apply(const RGBState16& state) override; + bool enterSleep() override; + void exitSleep() override; private: + static constexpr std::size_t kSymbolsPerFrame = 24; // 24 bits per LED (8 bits per color * 3 colors) + static constexpr std::size_t kDitherFrames = 4; // Steps between two 8-bit levels: 4 is 2 bits more, 10-bit output + + void encodeFrame(std::size_t index, uint8_t red, uint8_t green, uint8_t blue, bool latchGap); + bool transmit(std::size_t frames, bool loop); // False when the frames may not have gone out + void abortAndLatch(); // Stops the channel mid-transmission, then lets the LED latch + gpio_num_t m_gpioPin; bool m_swapRg; // Board wants RGB rather than GRB wire order; decided once, apply() runs on the pattern task - std::atomic m_brightness; + std::atomic m_brightness; rmt_channel_handle_t m_rmtChannel; rmt_encoder_handle_t m_rmtEncoder; - // The frame RMT reads from. A member, not a local: after a timed-out wait the transmission may still be reading it. - std::array m_frame; // 24 bits per LED (8 bits per color * 3 colors) + bool m_canLoop; // The channel holds all dither frames in RMT memory, so the hardware can repeat them + bool m_looping; // Pattern task only; a looped transmission is running and must be stopped before the next one + // The frames RMT reads from. A member, not a local: after a timed-out wait the transmission may still be reading it. + std::array m_frames; + rmt_symbol_word_t m_latchSymbol; // The line held low, sent after an abort; a member for the same reason }; } // namespace OpenShock diff --git a/components/led_drivers/src/MonoLedDriver.cpp b/components/led_drivers/src/MonoLedDriver.cpp index cd981361b..a1f85bc55 100644 --- a/components/led_drivers/src/MonoLedDriver.cpp +++ b/components/led_drivers/src/MonoLedDriver.cpp @@ -21,9 +21,9 @@ const char* const TAG = "MonoLedDriver"; using namespace OpenShock; -MonoLedDriver::MonoLedDriver(gpio_num_t gpioPin) +MonoLedDriver::MonoLedDriver(gpio_num_t gpioPin, uint8_t brightness) : m_gpioPin(GPIO_NUM_NC) - , m_brightness(255) + , m_brightness(brightness) { if (gpioPin == GPIO_NUM_NC) { OS_LOGE(TAG, "Pin is not set"); @@ -78,7 +78,32 @@ MonoLedDriver::~MonoLedDriver() // Before the LEDC channel goes away stopTask(); - ledc_stop(OS_LEDC_SPEED, OS_LEDC_CHANNEL, 0); // TODO: Error handling + ledc_stop(OS_LEDC_SPEED, OS_LEDC_CHANNEL, 0); // TODO: Error handling + if (isAsleep()) { + ledc_timer_resume(OS_LEDC_SPEED, OS_LEDC_TIMER); // Leave the timer as it was configured + } +} + +bool MonoLedDriver::enterSleep() +{ + // Output off and held low, then the timer's counter stopped: nothing toggles until exitSleep + esp_err_t err = ledc_stop(OS_LEDC_SPEED, OS_LEDC_CHANNEL, 0); + if (err != ESP_OK) { + OS_LOGW(TAG, "Failed to stop the LED output for sleep: %s", esp_err_to_name(err)); + return false; + } + err = ledc_timer_pause(OS_LEDC_SPEED, OS_LEDC_TIMER); + if (err != ESP_OK) { + OS_LOGW(TAG, "Failed to pause the LED timer for sleep: %s", esp_err_to_name(err)); + return false; + } + return true; +} + +void MonoLedDriver::exitSleep() +{ + // The output comes back with the next apply(): ledc_update_duty enables it again + ledc_timer_resume(OS_LEDC_SPEED, OS_LEDC_TIMER); } void MonoLedDriver::apply(const State& state) diff --git a/components/led_drivers/src/RgbLedDriver.cpp b/components/led_drivers/src/RgbLedDriver.cpp index 6b12ff49a..d1eb3f37c 100644 --- a/components/led_drivers/src/RgbLedDriver.cpp +++ b/components/led_drivers/src/RgbLedDriver.cpp @@ -9,24 +9,44 @@ const char* const TAG = "RGBLedDriver"; #include "Logging.h" #include +#include // RMT_LL_TX_CANDIDATES_PER_INST #include +#include #include +#include using namespace OpenShock; +// Dithering: the WS2812B takes 8 bits per color. The driver shows more by sending kDitherFrames frames that each round +// a channel down or up to a neighbouring 8-bit level, so their average lands on the step in between (kDitherFrames = 4: +// quarter steps, 10-bit output), and has the RMT hardware repeat them on its own (a looped transmission) for as long +// as the color holds: no CPU work after apply(). + +// Low time after each frame so the LED latches it: over 280 us for the WS2812B-V5 (older parts need 50 us) +static constexpr uint16_t kLatchTicks = 3000; // 300 us at 10 MHz + // Currently this assumes a single WS2812B LED // TODO: Support multiple LEDs ? // TODO: Support other LED types ? -RgbLedDriver::RgbLedDriver(gpio_num_t gpioPin) +RgbLedDriver::RgbLedDriver(gpio_num_t gpioPin, uint16_t brightness) : m_gpioPin(GPIO_NUM_NC) , m_swapRg((Board::Get().flags & OPENSHOCK_BOARD_FLAG_LED_SWAP_RG) != 0) - , m_brightness(255) + , m_brightness(brightness) , m_rmtChannel(nullptr) , m_rmtEncoder(nullptr) - , m_frame() + , m_canLoop(false) + , m_looping(false) + , m_frames() + , m_latchSymbol() { + // Low for the latch time. Both halves low; a duration of 0 would mark the end of the data. + m_latchSymbol.level0 = 0; + m_latchSymbol.duration0 = kLatchTicks; + m_latchSymbol.level1 = 0; + m_latchSymbol.duration1 = 1; + if (gpioPin == GPIO_NUM_NC) { OS_LOGE(TAG, "Pin is not set"); return; @@ -47,7 +67,28 @@ RgbLedDriver::RgbLedDriver(gpio_num_t gpioPin) .intr_priority = 0, // 0 => driver picks a low/medium priority .flags = {}, }; - esp_err_t err = rmt_new_tx_channel(&channelConfig, &m_rmtChannel); + // A looped transmission must fit in the channel's RMT memory with an end marker, so the channel takes several memory + // blocks. Blocks are contiguous and channels are taken lowest first, so this channel (created before the RF + // transmitter's) occupies the blocks of the channels after it; that leaves the RF transmitter a channel only on chips + // with more transmit channels than those blocks. On the others (ESP32-C3) the LED rounds to 8 bits. + constexpr std::size_t kLoopSymbols = kSymbolsPerFrame * kDitherFrames + 1; // The frames and the end marker + constexpr std::size_t kLoopMemBlocks = (kLoopSymbols + SOC_RMT_MEM_WORDS_PER_CHANNEL - 1) / SOC_RMT_MEM_WORDS_PER_CHANNEL; + constexpr bool kLoopDitherFits = CONFIG_OPENSHOCK_RGB_LED_DITHERING && kLoopMemBlocks < RMT_LL_TX_CANDIDATES_PER_INST; + + esp_err_t err = ESP_FAIL; + if constexpr (kLoopDitherFits) { + // Enough memory blocks for the dither frames; on failure, fall back to one block and 8-bit output + channelConfig.mem_block_symbols = kLoopMemBlocks * SOC_RMT_MEM_WORDS_PER_CHANNEL; + err = rmt_new_tx_channel(&channelConfig, &m_rmtChannel); + m_canLoop = err == ESP_OK; + if (!m_canLoop) { + OS_LOGW(TAG, "No room for the dithered RMT channel (%s), the RGB LED shows 8 bits per color", esp_err_to_name(err)); + channelConfig.mem_block_symbols = SOC_RMT_MEM_WORDS_PER_CHANNEL; + } + } + if (!m_canLoop) { + err = rmt_new_tx_channel(&channelConfig, &m_rmtChannel); + } if (err != ESP_OK) { OS_LOGE(TAG, "Failed to create RMT TX channel for pin %hhi: %s", gpioPin, esp_err_to_name(err)); m_rmtChannel = nullptr; @@ -92,7 +133,9 @@ RgbLedDriver::~RgbLedDriver() stopTask(); // before the RMT channel goes away if (m_rmtChannel != nullptr) { - rmt_disable(m_rmtChannel); + if (!isAsleep()) { // Asleep, the channel is already disabled + rmt_disable(m_rmtChannel); + } rmt_del_channel(m_rmtChannel); m_rmtChannel = nullptr; } @@ -102,18 +145,91 @@ RgbLedDriver::~RgbLedDriver() } } -void RgbLedDriver::apply(const RGBState& state) +void RgbLedDriver::SetPattern(const RgbLedState* pattern, std::size_t patternLength) +{ + std::vector wide; + wide.reserve(patternLength); + for (std::size_t i = 0; i < patternLength; ++i) { + const RgbLedState& s = pattern[i]; + // x * 257 maps 0-255 onto 0-65535 exactly (255 * 257 = 65535) + wide.push_back({static_cast(s.red * 257), static_cast(s.green * 257), static_cast(s.blue * 257), s.duration}); + } + PatternPlayer::SetPattern(wide.data(), wide.size()); +} + +void RgbLedDriver::apply(const RGBState16& state) +{ + const uint32_t brightness = m_brightness.load(); + const uint16_t channels[] = {state.red, state.green, state.blue}; + + // Each channel in 1/kDitherFrames steps of the LED's 8-bit levels: the 8-bit level and a fraction of kDitherFrames + constexpr uint32_t kSteps = kDitherFrames; + std::array steps; + bool fractional = false; + for (std::size_t i = 0; i < steps.size(); ++i) { + const uint32_t value = channels[i] * brightness / 65535; // 0-65535 + steps[i] = (value * (255 * kSteps) + 32767) / 65535; + fractional |= (steps[i] % kSteps) != 0; + } + + if (!m_canLoop || !fractional) { + // A whole 8-bit color (or no room to loop): one frame, the nearest level + auto level = [](uint32_t step) { + return static_cast(std::min((step + kSteps / 2) / kSteps, 255)); + }; + encodeFrame(0, level(steps[0]), level(steps[1]), level(steps[2]), false); + transmit(1, false); + return; + } + + // Spread each channel's fraction over the frames: with 4, 1/4 is one frame a level up, 2/4 every other frame, 3/4 three + std::array error = {}; + for (std::size_t frame = 0; frame < kDitherFrames; ++frame) { + std::array out; + for (std::size_t i = 0; i < out.size(); ++i) { + error[i] += steps[i] % kSteps; + const uint32_t up = error[i] >= kSteps ? 1 : 0; + error[i] -= up * kSteps; + out[i] = static_cast(steps[i] / kSteps + up); // At most 254 + 1: a whole 255 has no fraction + } + encodeFrame(frame, out[0], out[1], out[2], true); + } + transmit(kDitherFrames, true); +} + +bool RgbLedDriver::enterSleep() { - auto& led_data = m_frame; + // One black frame so the LED latches off (it holds its last color), then the channel disabled: that stops any looped + // transmission and releases the RMT's clock and power management lock until exitSleep. Without the black frame the + // channel stays enabled, so the retry can send it. + encodeFrame(0, 0, 0, 0, false); + if (!transmit(1, false)) { + OS_LOGW(TAG, "The black frame for sleep did not go out"); + return false; + } + esp_err_t err = rmt_disable(m_rmtChannel); + if (err != ESP_OK) { + OS_LOGW(TAG, "Failed to disable the RMT channel for sleep: %s", esp_err_to_name(err)); + return false; + } + return true; +} + +void RgbLedDriver::exitSleep() +{ + esp_err_t err = rmt_enable(m_rmtChannel); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to enable the RMT channel after sleep: %s", esp_err_to_name(err)); + } +} + +void RgbLedDriver::encodeFrame(std::size_t index, uint8_t r, uint8_t g, uint8_t b, bool latchGap) +{ + rmt_symbol_word_t* led_data = m_frames.data() + index * kSymbolsPerFrame; // WS2812B usually takes commands in GRB order // https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf - Page 5 // But some actually expect RGB! - const uint16_t brightness = m_brightness.load(); - - uint8_t r = static_cast(static_cast(state.red) * brightness / 255); - uint8_t g = static_cast(static_cast(state.green) * brightness / 255); - uint8_t b = static_cast(static_cast(state.blue) * brightness / 255); // Board setting (descriptor flag), decided once in the constructor so this path stays a single branch. if (m_swapRg) { std::swap(r, g); @@ -122,7 +238,7 @@ void RgbLedDriver::apply(const RGBState& state) const uint32_t colors = (static_cast(g) << 16) | (static_cast(r) << 8) | static_cast(b); // Encode the data - for (std::size_t bit = 0; bit < 24; bit++) { + for (std::size_t bit = 0; bit < kSymbolsPerFrame; bit++) { if (colors & (1 << (23 - bit))) { led_data[bit].level0 = 1; led_data[bit].duration0 = 8; @@ -136,17 +252,60 @@ void RgbLedDriver::apply(const RGBState& state) } } - // Send the data (blocking, wait up to 10ms per frame) + // Looped frames run back to back: hold the line low after the last bit so the LED latches each one + if (latchGap) { + led_data[kSymbolsPerFrame - 1].duration1 = kLatchTicks; + } +} + +// Aborts whatever the channel is sending, then holds the line low long enough for the LED to latch. The latch is the +// point: a transmission cut off mid-frame, or in the low gap after a frame before the LED latched it, would otherwise +// run straight into the next frame's bits. The LED takes those as the rest of the frame it already has, so it shows a +// stale frame and passes the new color on to an LED that isn't there. With a looped dither, at low brightness, that +// stale frame is a whole 8-bit level off the color (often black, or several times too bright), and it stays until the +// next change. +void RgbLedDriver::abortAndLatch() +{ + rmt_disable(m_rmtChannel); + rmt_enable(m_rmtChannel); + m_looping = false; + rmt_transmit_config_t txConfig = {}; txConfig.flags.eot_level = 0; - esp_err_t err = rmt_transmit(m_rmtChannel, m_rmtEncoder, led_data.data(), led_data.size() * sizeof(rmt_symbol_word_t), &txConfig); + esp_err_t err = rmt_transmit(m_rmtChannel, m_rmtEncoder, &m_latchSymbol, sizeof(m_latchSymbol), &txConfig); + if (err == ESP_OK) { + err = rmt_tx_wait_all_done(m_rmtChannel, 10); + } + if (err != ESP_OK) { + OS_LOGW(TAG, "Failed to latch the RGB LED after an abort: %s", esp_err_to_name(err)); + } +} + +bool RgbLedDriver::transmit(std::size_t frames, bool loop) +{ + // A looped transmission never finishes on its own; stopping the channel ends it, at any point in a frame + if (m_looping) { + abortAndLatch(); + } + + rmt_transmit_config_t txConfig = {}; + txConfig.loop_count = loop ? -1 : 0; // -1: repeat until stopped + txConfig.flags.eot_level = 0; + esp_err_t err = rmt_transmit(m_rmtChannel, m_rmtEncoder, m_frames.data(), frames * kSymbolsPerFrame * sizeof(rmt_symbol_word_t), &txConfig); if (err != ESP_OK) { OS_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(err)); - return; + return false; } + if (loop) { + m_looping = true; + return true; + } + + // Blocking, wait up to 10ms for the frame if (rmt_tx_wait_all_done(m_rmtChannel, 10) != ESP_OK) { // Abort the frame still in flight before the buffer is rewritten for the next one - rmt_disable(m_rmtChannel); - rmt_enable(m_rmtChannel); + abortAndLatch(); + return false; } + return true; } diff --git a/components/ota/CMakeLists.txt b/components/ota/CMakeLists.txt index 313d12ea5..c47c48221 100644 --- a/components/ota/CMakeLists.txt +++ b/components/ota/CMakeLists.txt @@ -18,4 +18,4 @@ idf_component_register( ) # Match core: IDF compiles application components with -Werror, the legacy build did not. -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error -Werror=switch) # -Wswitch stays fatal: exhaustive enum switches rely on it diff --git a/components/serial_console/CMakeLists.txt b/components/serial_console/CMakeLists.txt index 4ca64d73a..586f3ea2f 100644 --- a/components/serial_console/CMakeLists.txt +++ b/components/serial_console/CMakeLists.txt @@ -15,4 +15,4 @@ idf_component_register( common logging temporal osjson chipset # used directly; previously only reachable through core's public REQUIRES ) -target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error -Werror=switch) # -Wswitch stays fatal: exhaustive enum switches rely on it diff --git a/components/serial_console/include/serial_console/command_handlers/index.h b/components/serial_console/include/serial_console/command_handlers/index.h index d0a1a53ed..920adfb3a 100644 --- a/components/serial_console/include/serial_console/command_handlers/index.h +++ b/components/serial_console/include/serial_console/command_handlers/index.h @@ -22,6 +22,7 @@ namespace OpenShock::SerialCmds::CommandHandlers { OpenShock::SerialCmds::CommandGroup RfTransmitHandler(); OpenShock::SerialCmds::CommandGroup FactoryResetHandler(); OpenShock::SerialCmds::CommandGroup LedTestHandler(); + OpenShock::SerialCmds::CommandGroup LedHandler(); inline std::vector AllCommandHandlers() { @@ -44,6 +45,7 @@ namespace OpenShock::SerialCmds::CommandHandlers { RfTransmitHandler(), FactoryResetHandler(), LedTestHandler(), + LedHandler(), }; // clang-format on } diff --git a/components/serial_console/src/command_handlers/led.cpp b/components/serial_console/src/command_handlers/led.cpp new file mode 100644 index 000000000..0762386c4 --- /dev/null +++ b/components/serial_console/src/command_handlers/led.cpp @@ -0,0 +1,109 @@ +#include + +#include "serial_console/command_handlers/common.h" + +#include "Convert.h" +#include "enums/StatusLed.h" +#include "StringHelpers.h" +#include "visual/VisualStateManager.h" + +using OpenShock::LedSignal; +using OpenShock::LedType; + +static void handleLedModeCommand(std::string_view arg, bool isAutomated) +{ + (void)isAutomated; + + if (!OpenShock::VisualStateManager::HasAnyLed()) { + SERPR_ERROR("This board has no status LED"); + return; + } + + arg = OpenShock::StringTrim(arg); + if (arg.empty()) { + SERPR_RESPONSE("LedMode|%s", OpenShock::LedModeName(OpenShock::VisualStateManager::GetLedMode())); + return; + } + + auto mode = OpenShock::TryParseLedMode(arg); + if (!mode) { + SERPR_ERROR("Invalid argument (must be all, critical or off)"); + return; + } + + OpenShock::SerialCmds::ReportSaved(OpenShock::VisualStateManager::SetLedMode(*mode)); +} + +static void handleLedBrightnessCommand(std::string_view arg, bool isAutomated) +{ + (void)isAutomated; + + using OpenShock::VisualStateManager::GetLedBrightness; + using OpenShock::VisualStateManager::HasLed; + + arg = OpenShock::StringTrim(arg); + if (arg.empty()) { + // Only the LEDs the board has + if (!OpenShock::VisualStateManager::HasAnyLed()) { + SERPR_ERROR("This board has no status LED"); + return; + } + for (LedType type : {LedType::Mono, LedType::Rgb}) { + if (!HasLed(type)) { + continue; + } + const char* name = type == LedType::Mono ? "mono" : "rgb"; + SERPR_RESPONSE("LedBrightness|%s|critical|%u", name, GetLedBrightness(type, LedSignal::Critical)); + SERPR_RESPONSE("LedBrightness|%s|normal|%u", name, GetLedBrightness(type, LedSignal::Normal)); + } + return; + } + + // Token by token, trimming in between, so runs of spaces between the arguments are fine + auto [typeStr, afterType] = OpenShock::StringSplitByFirst(arg, ' '); + auto [signalStr, afterSignal] = OpenShock::StringSplitByFirst(OpenShock::StringTrimLeft(afterType), ' '); + auto valueStr = OpenShock::StringTrim(afterSignal); + if (typeStr.empty() || signalStr.empty() || valueStr.empty()) { + SERPR_ERROR("Invalid arguments (expected <0-65535>)"); + return; + } + + auto type = OpenShock::TryParseLedType(typeStr); + auto signal = OpenShock::TryParseLedSignal(signalStr); + auto value = OpenShock::Convert::ToUint16(valueStr); + if (!type) { + SERPR_ERROR("Invalid LED type (must be mono or rgb)"); + return; + } + if (!HasLed(*type)) { + SERPR_ERROR("This board has no %s LED", *type == LedType::Mono ? "mono" : "rgb"); + return; + } + if (!signal) { + SERPR_ERROR("Invalid signal (must be critical or normal)"); + return; + } + if (!value) { + SERPR_ERROR("Invalid brightness (must be 0-65535)"); + return; + } + + OpenShock::SerialCmds::ReportSaved(OpenShock::VisualStateManager::SetLedBrightness(*type, *signal, *value)); +} + +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::LedHandler() +{ + auto group = OpenShock::SerialCmds::CommandGroup("led"); + + group.addCommand("mode", "Get which states the status LEDs show", handleLedModeCommand); + auto& setModeCommand = group.addCommand("mode", "Set which states the status LEDs show: all, critical (only an E-Stop or a critical error) or off", handleLedModeCommand); + setModeCommand.addArgument("mode", "all, critical or off", "critical"); + + group.addCommand("brightness", "Get the status LED brightness, per LED type and signal", handleLedBrightnessCommand); + auto& setBrightnessCommand = group.addCommand("brightness", "Set the brightness of one LED type for critical (E-Stop, critical error) or normal patterns", handleLedBrightnessCommand); + setBrightnessCommand.addArgument("type", "mono or rgb", "rgb"); + setBrightnessCommand.addArgument("signal", "critical or normal", "normal"); + setBrightnessCommand.addArgument("brightness", "0-65535, of the LED's full output; a high RGB brightness logs a warning", "5140"); + + return group; +} diff --git a/components/serialization/include/serialization/_fbs/HubToLocalMessage_generated.h b/components/serialization/include/serialization/_fbs/HubToLocalMessage_generated.h index 6275e8206..353de986c 100644 --- a/components/serialization/include/serialization/_fbs/HubToLocalMessage_generated.h +++ b/components/serialization/include/serialization/_fbs/HubToLocalMessage_generated.h @@ -13,6 +13,7 @@ static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && FLATBUFFERS_VERSION_REVISION == 19, "Non-compatible flatbuffers version included"); +#include "StatusLed_generated.h" #include "WifiNetwork_generated.h" #include "WifiNetworkEventType_generated.h" #include "WifiScanStatus_generated.h" @@ -27,6 +28,21 @@ struct OtaSettingsBuilder; struct AccessPointSettings; struct AccessPointSettingsBuilder; +struct LedSettings; +struct LedSettingsBuilder; + +struct LedOverride; +struct LedOverrideBuilder; + +struct LedStatus; +struct LedStatusBuilder; + +struct LedOverrideEnded; +struct LedOverrideEndedBuilder; + +struct LedRequestFailed; +struct LedRequestFailedBuilder; + struct ReadyMessage; struct ReadyMessageBuilder; @@ -45,6 +61,12 @@ struct WifiGotIpEventBuilder; struct WifiLostIpEvent; struct WifiLostIpEventBuilder; +struct LedSettingsChangedEvent; +struct LedSettingsChangedEventBuilder; + +struct LedStatusEvent; +struct LedStatusEventBuilder; + struct AccountLinkStatusEvent; struct AccountLinkStatusEventBuilder; @@ -117,6 +139,143 @@ inline const char *EnumNameAccessPointProtection(AccessPointProtection e) { return EnumNamesAccessPointProtection()[index]; } +/// What the status LEDs show right now. +enum class LedShowing : uint8_t { + /// The hub's state, in LedStatus.pattern + Live = 0, + /// The hub's state, but the LED mode hides it, so the LEDs are dark + Hidden = 1, + /// A previewed pattern, in LedStatus.pattern + Pattern = 2, + /// A previewed color + Color = 3, + /// The LED test (serial console); overrides wait until it is done + Test = 4, + MIN = Live, + MAX = Test +}; + +inline const LedShowing (&EnumValuesLedShowing())[5] { + static const LedShowing values[] = { + LedShowing::Live, + LedShowing::Hidden, + LedShowing::Pattern, + LedShowing::Color, + LedShowing::Test + }; + return values; +} + +inline const char * const *EnumNamesLedShowing() { + static const char * const names[6] = { + "Live", + "Hidden", + "Pattern", + "Color", + "Test", + nullptr + }; + return names; +} + +inline const char *EnumNameLedShowing(LedShowing e) { + if (::flatbuffers::IsOutRange(e, LedShowing::Live, LedShowing::Test)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedShowing()[index]; +} + +enum class LedOverrideEnd : uint8_t { + /// Its owner sent nothing for it within its timeout + Expired = 0, + /// Its owner ended it (LedOverrideStopMessage) + Stopped = 1, + /// Another client took the layer over + Replaced = 2, + /// Its owner disconnected + OwnerLeft = 3, + /// Saved (a draft only) + Committed = 4, + /// The LED test took over the LEDs + LedTest = 5, + MIN = Expired, + MAX = LedTest +}; + +inline const LedOverrideEnd (&EnumValuesLedOverrideEnd())[6] { + static const LedOverrideEnd values[] = { + LedOverrideEnd::Expired, + LedOverrideEnd::Stopped, + LedOverrideEnd::Replaced, + LedOverrideEnd::OwnerLeft, + LedOverrideEnd::Committed, + LedOverrideEnd::LedTest + }; + return values; +} + +inline const char * const *EnumNamesLedOverrideEnd() { + static const char * const names[7] = { + "Expired", + "Stopped", + "Replaced", + "OwnerLeft", + "Committed", + "LedTest", + nullptr + }; + return names; +} + +inline const char *EnumNameLedOverrideEnd(LedOverrideEnd e) { + if (::flatbuffers::IsOutRange(e, LedOverrideEnd::Expired, LedOverrideEnd::LedTest)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedOverrideEnd()[index]; +} + +enum class LedRequestError : uint8_t { + /// The board has none of the LEDs the request is for + NoLed = 0, + /// The LED test holds the LEDs + LedTestRunning = 1, + /// Nothing to commit or stop: the client holds no override on that layer + NotHeld = 2, + /// The settings could not be saved + SaveFailed = 3, + /// A value the hub doesn't know + InvalidValue = 4, + MIN = NoLed, + MAX = InvalidValue +}; + +inline const LedRequestError (&EnumValuesLedRequestError())[5] { + static const LedRequestError values[] = { + LedRequestError::NoLed, + LedRequestError::LedTestRunning, + LedRequestError::NotHeld, + LedRequestError::SaveFailed, + LedRequestError::InvalidValue + }; + return values; +} + +inline const char * const *EnumNamesLedRequestError() { + static const char * const names[6] = { + "NoLed", + "LedTestRunning", + "NotHeld", + "SaveFailed", + "InvalidValue", + nullptr + }; + return names; +} + +inline const char *EnumNameLedRequestError(LedRequestError e) { + if (::flatbuffers::IsOutRange(e, LedRequestError::NoLed, LedRequestError::InvalidValue)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedRequestError()[index]; +} + enum class HubToLocalMessagePayload : uint8_t { NONE = 0, ReadyMessage = 1, @@ -126,11 +285,13 @@ enum class HubToLocalMessagePayload : uint8_t { WifiGotIpEvent = 5, WifiLostIpEvent = 6, AccountLinkStatusEvent = 7, + LedSettingsChangedEvent = 8, + LedStatusEvent = 9, MIN = NONE, - MAX = AccountLinkStatusEvent + MAX = LedStatusEvent }; -inline const HubToLocalMessagePayload (&EnumValuesHubToLocalMessagePayload())[8] { +inline const HubToLocalMessagePayload (&EnumValuesHubToLocalMessagePayload())[10] { static const HubToLocalMessagePayload values[] = { HubToLocalMessagePayload::NONE, HubToLocalMessagePayload::ReadyMessage, @@ -139,13 +300,15 @@ inline const HubToLocalMessagePayload (&EnumValuesHubToLocalMessagePayload())[8] HubToLocalMessagePayload::WifiNetworkEvent, HubToLocalMessagePayload::WifiGotIpEvent, HubToLocalMessagePayload::WifiLostIpEvent, - HubToLocalMessagePayload::AccountLinkStatusEvent + HubToLocalMessagePayload::AccountLinkStatusEvent, + HubToLocalMessagePayload::LedSettingsChangedEvent, + HubToLocalMessagePayload::LedStatusEvent }; return values; } inline const char * const *EnumNamesHubToLocalMessagePayload() { - static const char * const names[9] = { + static const char * const names[11] = { "NONE", "ReadyMessage", "ErrorMessage", @@ -154,13 +317,15 @@ inline const char * const *EnumNamesHubToLocalMessagePayload() { "WifiGotIpEvent", "WifiLostIpEvent", "AccountLinkStatusEvent", + "LedSettingsChangedEvent", + "LedStatusEvent", nullptr }; return names; } inline const char *EnumNameHubToLocalMessagePayload(HubToLocalMessagePayload e) { - if (::flatbuffers::IsOutRange(e, HubToLocalMessagePayload::NONE, HubToLocalMessagePayload::AccountLinkStatusEvent)) return ""; + if (::flatbuffers::IsOutRange(e, HubToLocalMessagePayload::NONE, HubToLocalMessagePayload::LedStatusEvent)) return ""; const size_t index = static_cast(e); return EnumNamesHubToLocalMessagePayload()[index]; } @@ -197,6 +362,14 @@ template<> struct HubToLocalMessagePayloadTraits struct HubToLocalMessagePayloadTraits { + static const HubToLocalMessagePayload enum_value = HubToLocalMessagePayload::LedSettingsChangedEvent; +}; + +template<> struct HubToLocalMessagePayloadTraits { + static const HubToLocalMessagePayload enum_value = HubToLocalMessagePayload::LedStatusEvent; +}; + template bool VerifyHubToLocalMessagePayload(::flatbuffers::VerifierTemplate &verifier, const void *obj, HubToLocalMessagePayload type); template @@ -414,6 +587,397 @@ inline ::flatbuffers::Offset CreateAccessPointSettingsDirec protection); } +/// The status LED settings the portal shows and edits. Only the LEDs the board has are present, so the portal shows +/// no option for an LED that isn't there. +struct LedSettings FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedSettingsBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedSettings"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_MODE = 4, + VT_MONO = 6, + VT_RGB = 8, + VT_RGB_WARN_ABOVE = 10 + }; + OpenShock::Serialization::Types::LedMode mode() const { + return static_cast(GetField(VT_MODE, 0)); + } + /// The single-color built-in LED; absent when the board has none. + const OpenShock::Serialization::Types::LedBrightness *mono() const { + return GetPointer(VT_MONO); + } + /// The WS2812B RGB LED; absent when the board has none. + const OpenShock::Serialization::Types::LedBrightness *rgb() const { + return GetPointer(VT_RGB); + } + /// The hub warns about an RGB brightness above this (0-65535). Only meaningful with `rgb`. + uint16_t rgb_warn_above() const { + return GetField(VT_RGB_WARN_ABOVE, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_MODE, 1) && + VerifyOffset(verifier, VT_MONO) && + verifier.VerifyTable(mono()) && + VerifyOffset(verifier, VT_RGB) && + verifier.VerifyTable(rgb()) && + VerifyField(verifier, VT_RGB_WARN_ABOVE, 2) && + verifier.EndTable(); + } +}; + +struct LedSettingsBuilder { + typedef LedSettings Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_mode(OpenShock::Serialization::Types::LedMode mode) { + fbb_.AddElement(LedSettings::VT_MODE, static_cast(mode), 0); + } + void add_mono(::flatbuffers::Offset mono) { + fbb_.AddOffset(LedSettings::VT_MONO, mono); + } + void add_rgb(::flatbuffers::Offset rgb) { + fbb_.AddOffset(LedSettings::VT_RGB, rgb); + } + void add_rgb_warn_above(uint16_t rgb_warn_above) { + fbb_.AddElement(LedSettings::VT_RGB_WARN_ABOVE, rgb_warn_above, 0); + } + explicit LedSettingsBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedSettings( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Types::LedMode mode = OpenShock::Serialization::Types::LedMode::All, + ::flatbuffers::Offset mono = 0, + ::flatbuffers::Offset rgb = 0, + uint16_t rgb_warn_above = 0) { + LedSettingsBuilder builder_(_fbb); + builder_.add_rgb(rgb); + builder_.add_mono(mono); + builder_.add_rgb_warn_above(rgb_warn_above); + builder_.add_mode(mode); + return builder_.Finish(); +} + +struct LedSettings::Traits { + using type = LedSettings; + static auto constexpr Create = CreateLedSettings; +}; + +/// An override on one layer, while it lasts. +struct LedOverride FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedOverrideBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedOverride"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_OWNER = 4, + VT_TIMEOUT_MS = 6 + }; + /// The client that holds it (ReadyMessage.client_id) + uint8_t owner() const { + return GetField(VT_OWNER, 0); + } + /// How long it lasts after the owner's last message for it (a pattern preview: from its start) + uint16_t timeout_ms() const { + return GetField(VT_TIMEOUT_MS, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_OWNER, 1) && + VerifyField(verifier, VT_TIMEOUT_MS, 2) && + verifier.EndTable(); + } +}; + +struct LedOverrideBuilder { + typedef LedOverride Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_owner(uint8_t owner) { + fbb_.AddElement(LedOverride::VT_OWNER, owner, 0); + } + void add_timeout_ms(uint16_t timeout_ms) { + fbb_.AddElement(LedOverride::VT_TIMEOUT_MS, timeout_ms, 0); + } + explicit LedOverrideBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedOverride( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t owner = 0, + uint16_t timeout_ms = 0) { + LedOverrideBuilder builder_(_fbb); + builder_.add_timeout_ms(timeout_ms); + builder_.add_owner(owner); + return builder_.Finish(); +} + +struct LedOverride::Traits { + using type = LedOverride; + static auto constexpr Create = CreateLedOverride; +}; + +struct LedStatus FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedStatusBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedStatus"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SHOWING = 4, + VT_PATTERN = 6, + VT_DISPLAY = 8, + VT_DRAFT = 10 + }; + OpenShock::Serialization::Local::LedShowing showing() const { + return static_cast(GetField(VT_SHOWING, 0)); + } + /// The pattern shown, for Live and Pattern + OpenShock::Serialization::Types::LedPattern pattern() const { + return static_cast(GetField(VT_PATTERN, 0)); + } + /// Present while a preview holds the LEDs + const OpenShock::Serialization::Local::LedOverride *display() const { + return GetPointer(VT_DISPLAY); + } + /// Present while unsaved brightness is applied + const OpenShock::Serialization::Local::LedOverride *draft() const { + return GetPointer(VT_DRAFT); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SHOWING, 1) && + VerifyField(verifier, VT_PATTERN, 1) && + VerifyOffset(verifier, VT_DISPLAY) && + verifier.VerifyTable(display()) && + VerifyOffset(verifier, VT_DRAFT) && + verifier.VerifyTable(draft()) && + verifier.EndTable(); + } +}; + +struct LedStatusBuilder { + typedef LedStatus Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_showing(OpenShock::Serialization::Local::LedShowing showing) { + fbb_.AddElement(LedStatus::VT_SHOWING, static_cast(showing), 0); + } + void add_pattern(OpenShock::Serialization::Types::LedPattern pattern) { + fbb_.AddElement(LedStatus::VT_PATTERN, static_cast(pattern), 0); + } + void add_display(::flatbuffers::Offset display) { + fbb_.AddOffset(LedStatus::VT_DISPLAY, display); + } + void add_draft(::flatbuffers::Offset draft) { + fbb_.AddOffset(LedStatus::VT_DRAFT, draft); + } + explicit LedStatusBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedStatus( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Local::LedShowing showing = OpenShock::Serialization::Local::LedShowing::Live, + OpenShock::Serialization::Types::LedPattern pattern = OpenShock::Serialization::Types::LedPattern::WifiDisconnected, + ::flatbuffers::Offset display = 0, + ::flatbuffers::Offset draft = 0) { + LedStatusBuilder builder_(_fbb); + builder_.add_draft(draft); + builder_.add_display(display); + builder_.add_pattern(pattern); + builder_.add_showing(showing); + return builder_.Finish(); +} + +struct LedStatus::Traits { + using type = LedStatus; + static auto constexpr Create = CreateLedStatus; +}; + +struct LedOverrideEnded FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedOverrideEndedBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedOverrideEnded"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_LAYER = 4, + VT_REASON = 6, + VT_OWNER = 8, + VT_SEQ = 10 + }; + OpenShock::Serialization::Types::LedOverrideLayer layer() const { + return static_cast(GetField(VT_LAYER, 0)); + } + OpenShock::Serialization::Local::LedOverrideEnd reason() const { + return static_cast(GetField(VT_REASON, 0)); + } + uint8_t owner() const { + return GetField(VT_OWNER, 0); + } + /// The seq of the owner's last message for it + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_LAYER, 1) && + VerifyField(verifier, VT_REASON, 1) && + VerifyField(verifier, VT_OWNER, 1) && + VerifyField(verifier, VT_SEQ, 2) && + verifier.EndTable(); + } +}; + +struct LedOverrideEndedBuilder { + typedef LedOverrideEnded Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_layer(OpenShock::Serialization::Types::LedOverrideLayer layer) { + fbb_.AddElement(LedOverrideEnded::VT_LAYER, static_cast(layer), 0); + } + void add_reason(OpenShock::Serialization::Local::LedOverrideEnd reason) { + fbb_.AddElement(LedOverrideEnded::VT_REASON, static_cast(reason), 0); + } + void add_owner(uint8_t owner) { + fbb_.AddElement(LedOverrideEnded::VT_OWNER, owner, 0); + } + void add_seq(uint16_t seq) { + fbb_.AddElement(LedOverrideEnded::VT_SEQ, seq, 0); + } + explicit LedOverrideEndedBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedOverrideEnded( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Types::LedOverrideLayer layer = OpenShock::Serialization::Types::LedOverrideLayer::Display, + OpenShock::Serialization::Local::LedOverrideEnd reason = OpenShock::Serialization::Local::LedOverrideEnd::Expired, + uint8_t owner = 0, + uint16_t seq = 0) { + LedOverrideEndedBuilder builder_(_fbb); + builder_.add_seq(seq); + builder_.add_owner(owner); + builder_.add_reason(reason); + builder_.add_layer(layer); + return builder_.Finish(); +} + +struct LedOverrideEnded::Traits { + using type = LedOverrideEnded; + static auto constexpr Create = CreateLedOverrideEnded; +}; + +struct LedRequestFailed FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedRequestFailedBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedRequestFailed"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_OWNER = 4, + VT_SEQ = 6, + VT_ERROR = 8 + }; + uint8_t owner() const { + return GetField(VT_OWNER, 0); + } + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + OpenShock::Serialization::Local::LedRequestError error() const { + return static_cast(GetField(VT_ERROR, 0)); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_OWNER, 1) && + VerifyField(verifier, VT_SEQ, 2) && + VerifyField(verifier, VT_ERROR, 1) && + verifier.EndTable(); + } +}; + +struct LedRequestFailedBuilder { + typedef LedRequestFailed Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_owner(uint8_t owner) { + fbb_.AddElement(LedRequestFailed::VT_OWNER, owner, 0); + } + void add_seq(uint16_t seq) { + fbb_.AddElement(LedRequestFailed::VT_SEQ, seq, 0); + } + void add_error(OpenShock::Serialization::Local::LedRequestError error) { + fbb_.AddElement(LedRequestFailed::VT_ERROR, static_cast(error), 0); + } + explicit LedRequestFailedBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedRequestFailed( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t owner = 0, + uint16_t seq = 0, + OpenShock::Serialization::Local::LedRequestError error = OpenShock::Serialization::Local::LedRequestError::NoLed) { + LedRequestFailedBuilder builder_(_fbb); + builder_.add_seq(seq); + builder_.add_error(error); + builder_.add_owner(owner); + return builder_.Finish(); +} + +struct LedRequestFailed::Traits { + using type = LedRequestFailed; + static auto constexpr Create = CreateLedRequestFailed; +}; + struct ReadyMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef ReadyMessageBuilder Builder; struct Traits; @@ -432,7 +996,10 @@ struct ReadyMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_ESTOP_GPIO_PIN = 20, VT_SAVED_SSIDS = 22, VT_OTA = 24, - VT_ACCESS_POINT = 26 + VT_ACCESS_POINT = 26, + VT_LED = 28, + VT_LED_STATUS = 30, + VT_CLIENT_ID = 32 }; bool poggies() const { return GetField(VT_POGGIES, 0) != 0; @@ -471,6 +1038,16 @@ struct ReadyMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { const OpenShock::Serialization::Local::AccessPointSettings *access_point() const { return GetPointer(VT_ACCESS_POINT); } + const OpenShock::Serialization::Local::LedSettings *led() const { + return GetPointer(VT_LED); + } + const OpenShock::Serialization::Local::LedStatus *led_status() const { + return GetPointer(VT_LED_STATUS); + } + /// This connection's id, as LedOverride.owner and the owner fields of LED events name it + uint8_t client_id() const { + return GetField(VT_CLIENT_ID, 0); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -493,6 +1070,11 @@ struct ReadyMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { verifier.VerifyTable(ota()) && VerifyOffset(verifier, VT_ACCESS_POINT) && verifier.VerifyTable(access_point()) && + VerifyOffset(verifier, VT_LED) && + verifier.VerifyTable(led()) && + VerifyOffset(verifier, VT_LED_STATUS) && + verifier.VerifyTable(led_status()) && + VerifyField(verifier, VT_CLIENT_ID, 1) && verifier.EndTable(); } }; @@ -537,6 +1119,15 @@ struct ReadyMessageBuilder { void add_access_point(::flatbuffers::Offset access_point) { fbb_.AddOffset(ReadyMessage::VT_ACCESS_POINT, access_point); } + void add_led(::flatbuffers::Offset led) { + fbb_.AddOffset(ReadyMessage::VT_LED, led); + } + void add_led_status(::flatbuffers::Offset led_status) { + fbb_.AddOffset(ReadyMessage::VT_LED_STATUS, led_status); + } + void add_client_id(uint8_t client_id) { + fbb_.AddElement(ReadyMessage::VT_CLIENT_ID, client_id, 0); + } explicit ReadyMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -561,14 +1152,20 @@ inline ::flatbuffers::Offset CreateReadyMessage( int8_t estop_gpio_pin = -1, ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset<::flatbuffers::String>>> saved_ssids = 0, ::flatbuffers::Offset ota = 0, - ::flatbuffers::Offset access_point = 0) { + ::flatbuffers::Offset access_point = 0, + ::flatbuffers::Offset led = 0, + ::flatbuffers::Offset led_status = 0, + uint8_t client_id = 0) { ReadyMessageBuilder builder_(_fbb); + builder_.add_led_status(led_status); + builder_.add_led(led); builder_.add_access_point(access_point); builder_.add_ota(ota); builder_.add_saved_ssids(saved_ssids); builder_.add_gpio_valid_outputs(gpio_valid_outputs); builder_.add_gpio_valid_inputs(gpio_valid_inputs); builder_.add_connected_wifi(connected_wifi); + builder_.add_client_id(client_id); builder_.add_estop_gpio_pin(estop_gpio_pin); builder_.add_estop_enabled(estop_enabled); builder_.add_rf_tx_pin(rf_tx_pin); @@ -596,7 +1193,10 @@ inline ::flatbuffers::Offset CreateReadyMessageDirect( int8_t estop_gpio_pin = -1, const std::vector<::flatbuffers::Offset<::flatbuffers::String>> *saved_ssids = nullptr, ::flatbuffers::Offset ota = 0, - ::flatbuffers::Offset access_point = 0) { + ::flatbuffers::Offset access_point = 0, + ::flatbuffers::Offset led = 0, + ::flatbuffers::Offset led_status = 0, + uint8_t client_id = 0) { auto gpio_valid_inputs__ = gpio_valid_inputs ? _fbb.CreateVector(*gpio_valid_inputs) : 0; auto gpio_valid_outputs__ = gpio_valid_outputs ? _fbb.CreateVector(*gpio_valid_outputs) : 0; auto saved_ssids__ = saved_ssids ? _fbb.CreateVector<::flatbuffers::Offset<::flatbuffers::String>>(*saved_ssids) : 0; @@ -613,7 +1213,10 @@ inline ::flatbuffers::Offset CreateReadyMessageDirect( estop_gpio_pin, saved_ssids__, ota, - access_point); + access_point, + led, + led_status, + client_id); } struct ErrorMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { @@ -924,6 +1527,137 @@ inline ::flatbuffers::Offset CreateWifiLostIpEventDirect( ip__); } +/// Sent to every connected client when the LED settings change, from any source (the portal, the serial console) +struct LedSettingsChangedEvent FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedSettingsChangedEventBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedSettingsChangedEvent"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_LED = 4 + }; + const OpenShock::Serialization::Local::LedSettings *led() const { + return GetPointer(VT_LED); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_LED) && + verifier.VerifyTable(led()) && + verifier.EndTable(); + } +}; + +struct LedSettingsChangedEventBuilder { + typedef LedSettingsChangedEvent Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_led(::flatbuffers::Offset led) { + fbb_.AddOffset(LedSettingsChangedEvent::VT_LED, led); + } + explicit LedSettingsChangedEventBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedSettingsChangedEvent( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset led = 0) { + LedSettingsChangedEventBuilder builder_(_fbb); + builder_.add_led(led); + return builder_.Finish(); +} + +struct LedSettingsChangedEvent::Traits { + using type = LedSettingsChangedEvent; + static auto constexpr Create = CreateLedSettingsChangedEvent; +}; + +/// Sent to every connected client when what the LEDs show changes, an override starts or ends, or an LED request +/// fails. Not sent for every frame of a dragged control: a client's repeated messages change nothing here. +struct LedStatusEvent FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedStatusEventBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedStatusEvent"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_STATUS = 4, + VT_ENDED = 6, + VT_FAILED = 8 + }; + const OpenShock::Serialization::Local::LedStatus *status() const { + return GetPointer(VT_STATUS); + } + /// Present when this event reports an override ending + const OpenShock::Serialization::Local::LedOverrideEnded *ended() const { + return GetPointer(VT_ENDED); + } + /// Present when this event reports a request failing + const OpenShock::Serialization::Local::LedRequestFailed *failed() const { + return GetPointer(VT_FAILED); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_STATUS) && + verifier.VerifyTable(status()) && + VerifyOffset(verifier, VT_ENDED) && + verifier.VerifyTable(ended()) && + VerifyOffset(verifier, VT_FAILED) && + verifier.VerifyTable(failed()) && + verifier.EndTable(); + } +}; + +struct LedStatusEventBuilder { + typedef LedStatusEvent Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_status(::flatbuffers::Offset status) { + fbb_.AddOffset(LedStatusEvent::VT_STATUS, status); + } + void add_ended(::flatbuffers::Offset ended) { + fbb_.AddOffset(LedStatusEvent::VT_ENDED, ended); + } + void add_failed(::flatbuffers::Offset failed) { + fbb_.AddOffset(LedStatusEvent::VT_FAILED, failed); + } + explicit LedStatusEventBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedStatusEvent( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset status = 0, + ::flatbuffers::Offset ended = 0, + ::flatbuffers::Offset failed = 0) { + LedStatusEventBuilder builder_(_fbb); + builder_.add_failed(failed); + builder_.add_ended(ended); + builder_.add_status(status); + return builder_.Finish(); +} + +struct LedStatusEvent::Traits { + using type = LedStatusEvent; + static auto constexpr Create = CreateLedStatusEvent; +}; + /// Sent when the auth token status changes (set, cleared, or validated on gateway connect) struct AccountLinkStatusEvent FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef AccountLinkStatusEventBuilder Builder; @@ -1014,6 +1748,12 @@ struct HubToLocalMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table const OpenShock::Serialization::Local::AccountLinkStatusEvent *payload_as_AccountLinkStatusEvent() const { return payload_type() == OpenShock::Serialization::Local::HubToLocalMessagePayload::AccountLinkStatusEvent ? static_cast(payload()) : nullptr; } + const OpenShock::Serialization::Local::LedSettingsChangedEvent *payload_as_LedSettingsChangedEvent() const { + return payload_type() == OpenShock::Serialization::Local::HubToLocalMessagePayload::LedSettingsChangedEvent ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::LedStatusEvent *payload_as_LedStatusEvent() const { + return payload_type() == OpenShock::Serialization::Local::HubToLocalMessagePayload::LedStatusEvent ? static_cast(payload()) : nullptr; + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -1052,6 +1792,14 @@ template<> inline const OpenShock::Serialization::Local::AccountLinkStatusEvent return payload_as_AccountLinkStatusEvent(); } +template<> inline const OpenShock::Serialization::Local::LedSettingsChangedEvent *HubToLocalMessage::payload_as() const { + return payload_as_LedSettingsChangedEvent(); +} + +template<> inline const OpenShock::Serialization::Local::LedStatusEvent *HubToLocalMessage::payload_as() const { + return payload_as_LedStatusEvent(); +} + struct HubToLocalMessageBuilder { typedef HubToLocalMessage Table; ::flatbuffers::FlatBufferBuilder &fbb_; @@ -1122,6 +1870,14 @@ inline bool VerifyHubToLocalMessagePayload(::flatbuffers::VerifierTemplate &v auto ptr = reinterpret_cast(obj); return verifier.VerifyTable(ptr); } + case HubToLocalMessagePayload::LedSettingsChangedEvent: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case HubToLocalMessagePayload::LedStatusEvent: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } default: return true; } } diff --git a/components/serialization/include/serialization/_fbs/LocalToHubMessage_generated.h b/components/serialization/include/serialization/_fbs/LocalToHubMessage_generated.h index 6fa3ad46e..2b14b129f 100644 --- a/components/serialization/include/serialization/_fbs/LocalToHubMessage_generated.h +++ b/components/serialization/include/serialization/_fbs/LocalToHubMessage_generated.h @@ -14,40 +14,140 @@ static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && "Non-compatible flatbuffers version included"); #include "ShockerCommand_generated.h" +#include "StatusLed_generated.h" namespace OpenShock { namespace Serialization { namespace Local { +struct LedPreviewPatternMessage; +struct LedPreviewPatternMessageBuilder; + +struct LedPreviewColorMessage; +struct LedPreviewColorMessageBuilder; + +struct LedDraftMessage; +struct LedDraftMessageBuilder; + +struct LedOverrideStopMessage; +struct LedOverrideStopMessageBuilder; + +struct LedCommitDraftMessage; +struct LedCommitDraftMessageBuilder; + +struct LedSetModeMessage; +struct LedSetModeMessageBuilder; + struct LocalToHubMessage; struct LocalToHubMessageBuilder; +enum class LedTarget : uint8_t { + All = 0, + Mono = 1, + Rgb = 2, + MIN = All, + MAX = Rgb +}; + +inline const LedTarget (&EnumValuesLedTarget())[3] { + static const LedTarget values[] = { + LedTarget::All, + LedTarget::Mono, + LedTarget::Rgb + }; + return values; +} + +inline const char * const *EnumNamesLedTarget() { + static const char * const names[4] = { + "All", + "Mono", + "Rgb", + nullptr + }; + return names; +} + +inline const char *EnumNameLedTarget(LedTarget e) { + if (::flatbuffers::IsOutRange(e, LedTarget::All, LedTarget::Rgb)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedTarget()[index]; +} + +enum class LedSignal : uint8_t { + Normal = 0, + Critical = 1, + MIN = Normal, + MAX = Critical +}; + +inline const LedSignal (&EnumValuesLedSignal())[2] { + static const LedSignal values[] = { + LedSignal::Normal, + LedSignal::Critical + }; + return values; +} + +inline const char * const *EnumNamesLedSignal() { + static const char * const names[3] = { + "Normal", + "Critical", + nullptr + }; + return names; +} + +inline const char *EnumNameLedSignal(LedSignal e) { + if (::flatbuffers::IsOutRange(e, LedSignal::Normal, LedSignal::Critical)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedSignal()[index]; +} + enum class LocalToHubMessagePayload : uint8_t { NONE = 0, Common_ShockerCommandList = 1, + LedPreviewPatternMessage = 2, + LedPreviewColorMessage = 3, + LedDraftMessage = 4, + LedOverrideStopMessage = 5, + LedCommitDraftMessage = 6, + LedSetModeMessage = 7, MIN = NONE, - MAX = Common_ShockerCommandList + MAX = LedSetModeMessage }; -inline const LocalToHubMessagePayload (&EnumValuesLocalToHubMessagePayload())[2] { +inline const LocalToHubMessagePayload (&EnumValuesLocalToHubMessagePayload())[8] { static const LocalToHubMessagePayload values[] = { LocalToHubMessagePayload::NONE, - LocalToHubMessagePayload::Common_ShockerCommandList + LocalToHubMessagePayload::Common_ShockerCommandList, + LocalToHubMessagePayload::LedPreviewPatternMessage, + LocalToHubMessagePayload::LedPreviewColorMessage, + LocalToHubMessagePayload::LedDraftMessage, + LocalToHubMessagePayload::LedOverrideStopMessage, + LocalToHubMessagePayload::LedCommitDraftMessage, + LocalToHubMessagePayload::LedSetModeMessage }; return values; } inline const char * const *EnumNamesLocalToHubMessagePayload() { - static const char * const names[3] = { + static const char * const names[9] = { "NONE", "Common_ShockerCommandList", + "LedPreviewPatternMessage", + "LedPreviewColorMessage", + "LedDraftMessage", + "LedOverrideStopMessage", + "LedCommitDraftMessage", + "LedSetModeMessage", nullptr }; return names; } inline const char *EnumNameLocalToHubMessagePayload(LocalToHubMessagePayload e) { - if (::flatbuffers::IsOutRange(e, LocalToHubMessagePayload::NONE, LocalToHubMessagePayload::Common_ShockerCommandList)) return ""; + if (::flatbuffers::IsOutRange(e, LocalToHubMessagePayload::NONE, LocalToHubMessagePayload::LedSetModeMessage)) return ""; const size_t index = static_cast(e); return EnumNamesLocalToHubMessagePayload()[index]; } @@ -60,11 +160,455 @@ template<> struct LocalToHubMessagePayloadTraits struct LocalToHubMessagePayloadTraits { + static const LocalToHubMessagePayload enum_value = LocalToHubMessagePayload::LedPreviewPatternMessage; +}; + +template<> struct LocalToHubMessagePayloadTraits { + static const LocalToHubMessagePayload enum_value = LocalToHubMessagePayload::LedPreviewColorMessage; +}; + +template<> struct LocalToHubMessagePayloadTraits { + static const LocalToHubMessagePayload enum_value = LocalToHubMessagePayload::LedDraftMessage; +}; + +template<> struct LocalToHubMessagePayloadTraits { + static const LocalToHubMessagePayload enum_value = LocalToHubMessagePayload::LedOverrideStopMessage; +}; + +template<> struct LocalToHubMessagePayloadTraits { + static const LocalToHubMessagePayload enum_value = LocalToHubMessagePayload::LedCommitDraftMessage; +}; + +template<> struct LocalToHubMessagePayloadTraits { + static const LocalToHubMessagePayload enum_value = LocalToHubMessagePayload::LedSetModeMessage; +}; + template bool VerifyLocalToHubMessagePayload(::flatbuffers::VerifierTemplate &verifier, const void *obj, LocalToHubMessagePayload type); template bool VerifyLocalToHubMessagePayloadVector(::flatbuffers::VerifierTemplate &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types); +/// Shows a state's pattern whatever the LED mode, for about two of its cycles. Nothing is saved. +struct LedPreviewPatternMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedPreviewPatternMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedPreviewPatternMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQ = 4, + VT_PATTERN = 6 + }; + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + OpenShock::Serialization::Types::LedPattern pattern() const { + return static_cast(GetField(VT_PATTERN, 0)); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 2) && + VerifyField(verifier, VT_PATTERN, 1) && + verifier.EndTable(); + } +}; + +struct LedPreviewPatternMessageBuilder { + typedef LedPreviewPatternMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_seq(uint16_t seq) { + fbb_.AddElement(LedPreviewPatternMessage::VT_SEQ, seq, 0); + } + void add_pattern(OpenShock::Serialization::Types::LedPattern pattern) { + fbb_.AddElement(LedPreviewPatternMessage::VT_PATTERN, static_cast(pattern), 0); + } + explicit LedPreviewPatternMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedPreviewPatternMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t seq = 0, + OpenShock::Serialization::Types::LedPattern pattern = OpenShock::Serialization::Types::LedPattern::WifiDisconnected) { + LedPreviewPatternMessageBuilder builder_(_fbb); + builder_.add_seq(seq); + builder_.add_pattern(pattern); + return builder_.Finish(); +} + +struct LedPreviewPatternMessage::Traits { + using type = LedPreviewPatternMessage; + static auto constexpr Create = CreateLedPreviewPatternMessage; +}; + +/// Shows a solid color: the RGB LED in the color, the mono LED on unless it is black. Sent at a high rate while the +/// user drags a color or brightness control; the hub ends it two seconds after the last one. Nothing is saved. +struct LedPreviewColorMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedPreviewColorMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedPreviewColorMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQ = 4, + VT_RED = 6, + VT_GREEN = 8, + VT_BLUE = 10, + VT_TARGET = 12, + VT_SIGNAL = 14 + }; + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + /// 16 bits per channel (0-65535); the hub reduces them to what the RGB LED shows. + uint16_t red() const { + return GetField(VT_RED, 0); + } + uint16_t green() const { + return GetField(VT_GREEN, 0); + } + uint16_t blue() const { + return GetField(VT_BLUE, 0); + } + /// The LED to light; the other stays dark. A target the board doesn't have lights nothing. + OpenShock::Serialization::Local::LedTarget target() const { + return static_cast(GetField(VT_TARGET, 0)); + } + /// Which brightness setting it shows at: the client's draft if it holds one, else the saved one. + OpenShock::Serialization::Local::LedSignal signal() const { + return static_cast(GetField(VT_SIGNAL, 0)); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 2) && + VerifyField(verifier, VT_RED, 2) && + VerifyField(verifier, VT_GREEN, 2) && + VerifyField(verifier, VT_BLUE, 2) && + VerifyField(verifier, VT_TARGET, 1) && + VerifyField(verifier, VT_SIGNAL, 1) && + verifier.EndTable(); + } +}; + +struct LedPreviewColorMessageBuilder { + typedef LedPreviewColorMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_seq(uint16_t seq) { + fbb_.AddElement(LedPreviewColorMessage::VT_SEQ, seq, 0); + } + void add_red(uint16_t red) { + fbb_.AddElement(LedPreviewColorMessage::VT_RED, red, 0); + } + void add_green(uint16_t green) { + fbb_.AddElement(LedPreviewColorMessage::VT_GREEN, green, 0); + } + void add_blue(uint16_t blue) { + fbb_.AddElement(LedPreviewColorMessage::VT_BLUE, blue, 0); + } + void add_target(OpenShock::Serialization::Local::LedTarget target) { + fbb_.AddElement(LedPreviewColorMessage::VT_TARGET, static_cast(target), 0); + } + void add_signal(OpenShock::Serialization::Local::LedSignal signal) { + fbb_.AddElement(LedPreviewColorMessage::VT_SIGNAL, static_cast(signal), 0); + } + explicit LedPreviewColorMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedPreviewColorMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t seq = 0, + uint16_t red = 0, + uint16_t green = 0, + uint16_t blue = 0, + OpenShock::Serialization::Local::LedTarget target = OpenShock::Serialization::Local::LedTarget::All, + OpenShock::Serialization::Local::LedSignal signal = OpenShock::Serialization::Local::LedSignal::Normal) { + LedPreviewColorMessageBuilder builder_(_fbb); + builder_.add_blue(blue); + builder_.add_green(green); + builder_.add_red(red); + builder_.add_seq(seq); + builder_.add_signal(signal); + builder_.add_target(target); + return builder_.Finish(); +} + +struct LedPreviewColorMessage::Traits { + using type = LedPreviewColorMessage; + static auto constexpr Create = CreateLedPreviewColorMessage; +}; + +/// Unsaved brightness for the LEDs the board has, shown on whatever the LEDs show. Replaces the client's previous +/// draft. The hub drops it (the LEDs return to the saved brightness) ten seconds after the last one unless it is saved +/// with LedCommitDraftMessage. +struct LedDraftMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedDraftMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedDraftMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQ = 4, + VT_MONO = 6, + VT_RGB = 8 + }; + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + const OpenShock::Serialization::Types::LedBrightness *mono() const { + return GetPointer(VT_MONO); + } + const OpenShock::Serialization::Types::LedBrightness *rgb() const { + return GetPointer(VT_RGB); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 2) && + VerifyOffset(verifier, VT_MONO) && + verifier.VerifyTable(mono()) && + VerifyOffset(verifier, VT_RGB) && + verifier.VerifyTable(rgb()) && + verifier.EndTable(); + } +}; + +struct LedDraftMessageBuilder { + typedef LedDraftMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_seq(uint16_t seq) { + fbb_.AddElement(LedDraftMessage::VT_SEQ, seq, 0); + } + void add_mono(::flatbuffers::Offset mono) { + fbb_.AddOffset(LedDraftMessage::VT_MONO, mono); + } + void add_rgb(::flatbuffers::Offset rgb) { + fbb_.AddOffset(LedDraftMessage::VT_RGB, rgb); + } + explicit LedDraftMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedDraftMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t seq = 0, + ::flatbuffers::Offset mono = 0, + ::flatbuffers::Offset rgb = 0) { + LedDraftMessageBuilder builder_(_fbb); + builder_.add_rgb(rgb); + builder_.add_mono(mono); + builder_.add_seq(seq); + return builder_.Finish(); +} + +struct LedDraftMessage::Traits { + using type = LedDraftMessage; + static auto constexpr Create = CreateLedDraftMessage; +}; + +/// Ends the client's own override on `layer` early. +struct LedOverrideStopMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedOverrideStopMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedOverrideStopMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQ = 4, + VT_LAYER = 6 + }; + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + OpenShock::Serialization::Types::LedOverrideLayer layer() const { + return static_cast(GetField(VT_LAYER, 0)); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 2) && + VerifyField(verifier, VT_LAYER, 1) && + verifier.EndTable(); + } +}; + +struct LedOverrideStopMessageBuilder { + typedef LedOverrideStopMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_seq(uint16_t seq) { + fbb_.AddElement(LedOverrideStopMessage::VT_SEQ, seq, 0); + } + void add_layer(OpenShock::Serialization::Types::LedOverrideLayer layer) { + fbb_.AddElement(LedOverrideStopMessage::VT_LAYER, static_cast(layer), 0); + } + explicit LedOverrideStopMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedOverrideStopMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t seq = 0, + OpenShock::Serialization::Types::LedOverrideLayer layer = OpenShock::Serialization::Types::LedOverrideLayer::Display) { + LedOverrideStopMessageBuilder builder_(_fbb); + builder_.add_seq(seq); + builder_.add_layer(layer); + return builder_.Finish(); +} + +struct LedOverrideStopMessage::Traits { + using type = LedOverrideStopMessage; + static auto constexpr Create = CreateLedOverrideStopMessage; +}; + +/// Saves the draft the hub holds for this client. It ends as Committed, and LedSettingsChangedEvent follows. +struct LedCommitDraftMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedCommitDraftMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedCommitDraftMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQ = 4 + }; + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 2) && + verifier.EndTable(); + } +}; + +struct LedCommitDraftMessageBuilder { + typedef LedCommitDraftMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_seq(uint16_t seq) { + fbb_.AddElement(LedCommitDraftMessage::VT_SEQ, seq, 0); + } + explicit LedCommitDraftMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedCommitDraftMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t seq = 0) { + LedCommitDraftMessageBuilder builder_(_fbb); + builder_.add_seq(seq); + return builder_.Finish(); +} + +struct LedCommitDraftMessage::Traits { + using type = LedCommitDraftMessage; + static auto constexpr Create = CreateLedCommitDraftMessage; +}; + +/// Saves the LED mode. LedSettingsChangedEvent follows. +struct LedSetModeMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedSetModeMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.LedSetModeMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_SEQ = 4, + VT_MODE = 6 + }; + uint16_t seq() const { + return GetField(VT_SEQ, 0); + } + OpenShock::Serialization::Types::LedMode mode() const { + return static_cast(GetField(VT_MODE, 0)); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 2) && + VerifyField(verifier, VT_MODE, 1) && + verifier.EndTable(); + } +}; + +struct LedSetModeMessageBuilder { + typedef LedSetModeMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_seq(uint16_t seq) { + fbb_.AddElement(LedSetModeMessage::VT_SEQ, seq, 0); + } + void add_mode(OpenShock::Serialization::Types::LedMode mode) { + fbb_.AddElement(LedSetModeMessage::VT_MODE, static_cast(mode), 0); + } + explicit LedSetModeMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedSetModeMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t seq = 0, + OpenShock::Serialization::Types::LedMode mode = OpenShock::Serialization::Types::LedMode::All) { + LedSetModeMessageBuilder builder_(_fbb); + builder_.add_seq(seq); + builder_.add_mode(mode); + return builder_.Finish(); +} + +struct LedSetModeMessage::Traits { + using type = LedSetModeMessage; + static auto constexpr Create = CreateLedSetModeMessage; +}; + struct LocalToHubMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef LocalToHubMessageBuilder Builder; struct Traits; @@ -85,6 +629,24 @@ struct LocalToHubMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table const OpenShock::Serialization::Common::ShockerCommandList *payload_as_Common_ShockerCommandList() const { return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::Common_ShockerCommandList ? static_cast(payload()) : nullptr; } + const OpenShock::Serialization::Local::LedPreviewPatternMessage *payload_as_LedPreviewPatternMessage() const { + return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::LedPreviewPatternMessage ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::LedPreviewColorMessage *payload_as_LedPreviewColorMessage() const { + return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::LedPreviewColorMessage ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::LedDraftMessage *payload_as_LedDraftMessage() const { + return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::LedDraftMessage ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::LedOverrideStopMessage *payload_as_LedOverrideStopMessage() const { + return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::LedOverrideStopMessage ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::LedCommitDraftMessage *payload_as_LedCommitDraftMessage() const { + return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::LedCommitDraftMessage ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::LedSetModeMessage *payload_as_LedSetModeMessage() const { + return payload_type() == OpenShock::Serialization::Local::LocalToHubMessagePayload::LedSetModeMessage ? static_cast(payload()) : nullptr; + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -99,6 +661,30 @@ template<> inline const OpenShock::Serialization::Common::ShockerCommandList *Lo return payload_as_Common_ShockerCommandList(); } +template<> inline const OpenShock::Serialization::Local::LedPreviewPatternMessage *LocalToHubMessage::payload_as() const { + return payload_as_LedPreviewPatternMessage(); +} + +template<> inline const OpenShock::Serialization::Local::LedPreviewColorMessage *LocalToHubMessage::payload_as() const { + return payload_as_LedPreviewColorMessage(); +} + +template<> inline const OpenShock::Serialization::Local::LedDraftMessage *LocalToHubMessage::payload_as() const { + return payload_as_LedDraftMessage(); +} + +template<> inline const OpenShock::Serialization::Local::LedOverrideStopMessage *LocalToHubMessage::payload_as() const { + return payload_as_LedOverrideStopMessage(); +} + +template<> inline const OpenShock::Serialization::Local::LedCommitDraftMessage *LocalToHubMessage::payload_as() const { + return payload_as_LedCommitDraftMessage(); +} + +template<> inline const OpenShock::Serialization::Local::LedSetModeMessage *LocalToHubMessage::payload_as() const { + return payload_as_LedSetModeMessage(); +} + struct LocalToHubMessageBuilder { typedef LocalToHubMessage Table; ::flatbuffers::FlatBufferBuilder &fbb_; @@ -145,6 +731,30 @@ inline bool VerifyLocalToHubMessagePayload(::flatbuffers::VerifierTemplate &v auto ptr = reinterpret_cast(obj); return verifier.VerifyTable(ptr); } + case LocalToHubMessagePayload::LedPreviewPatternMessage: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToHubMessagePayload::LedPreviewColorMessage: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToHubMessagePayload::LedDraftMessage: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToHubMessagePayload::LedOverrideStopMessage: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToHubMessagePayload::LedCommitDraftMessage: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToHubMessagePayload::LedSetModeMessage: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } default: return true; } } diff --git a/components/serialization/include/serialization/_fbs/StatusLed_generated.h b/components/serialization/include/serialization/_fbs/StatusLed_generated.h new file mode 100644 index 000000000..a40e08748 --- /dev/null +++ b/components/serialization/include/serialization/_fbs/StatusLed_generated.h @@ -0,0 +1,207 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_STATUSLED_OPENSHOCK_SERIALIZATION_TYPES_H_ +#define FLATBUFFERS_GENERATED_STATUSLED_OPENSHOCK_SERIALIZATION_TYPES_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 25 && + FLATBUFFERS_VERSION_MINOR == 12 && + FLATBUFFERS_VERSION_REVISION == 19, + "Non-compatible flatbuffers version included"); + +namespace OpenShock { +namespace Serialization { +namespace Types { + +struct LedBrightness; +struct LedBrightnessBuilder; + +enum class LedMode : uint8_t { + All = 0, + CriticalOnly = 1, + Off = 2, + MIN = All, + MAX = Off +}; + +inline const LedMode (&EnumValuesLedMode())[3] { + static const LedMode values[] = { + LedMode::All, + LedMode::CriticalOnly, + LedMode::Off + }; + return values; +} + +inline const char * const *EnumNamesLedMode() { + static const char * const names[4] = { + "All", + "CriticalOnly", + "Off", + nullptr + }; + return names; +} + +inline const char *EnumNameLedMode(LedMode e) { + if (::flatbuffers::IsOutRange(e, LedMode::All, LedMode::Off)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedMode()[index]; +} + +/// A hub state's LED pattern. +enum class LedPattern : uint8_t { + WifiDisconnected = 0, + WifiScanning = 1, + NoBackend = 2, + Connected = 3, + EStopped = 4, + EStopClearing = 5, + EStopAwaitingRelease = 6, + CriticalError = 7, + MIN = WifiDisconnected, + MAX = CriticalError +}; + +inline const LedPattern (&EnumValuesLedPattern())[8] { + static const LedPattern values[] = { + LedPattern::WifiDisconnected, + LedPattern::WifiScanning, + LedPattern::NoBackend, + LedPattern::Connected, + LedPattern::EStopped, + LedPattern::EStopClearing, + LedPattern::EStopAwaitingRelease, + LedPattern::CriticalError + }; + return values; +} + +inline const char * const *EnumNamesLedPattern() { + static const char * const names[9] = { + "WifiDisconnected", + "WifiScanning", + "NoBackend", + "Connected", + "EStopped", + "EStopClearing", + "EStopAwaitingRelease", + "CriticalError", + nullptr + }; + return names; +} + +inline const char *EnumNameLedPattern(LedPattern e) { + if (::flatbuffers::IsOutRange(e, LedPattern::WifiDisconnected, LedPattern::CriticalError)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedPattern()[index]; +} + +/// The two things a portal can put on the LEDs without saving them. Each belongs to the client that sent it and ends +/// on its own after a while unless that client keeps sending. +enum class LedOverrideLayer : uint8_t { + /// A previewed pattern or color, shown instead of the hub's state + Display = 0, + /// Unsaved brightness, applied to whatever the LEDs show (the hub's state or a preview) + Draft = 1, + MIN = Display, + MAX = Draft +}; + +inline const LedOverrideLayer (&EnumValuesLedOverrideLayer())[2] { + static const LedOverrideLayer values[] = { + LedOverrideLayer::Display, + LedOverrideLayer::Draft + }; + return values; +} + +inline const char * const *EnumNamesLedOverrideLayer() { + static const char * const names[3] = { + "Display", + "Draft", + nullptr + }; + return names; +} + +inline const char *EnumNameLedOverrideLayer(LedOverrideLayer e) { + if (::flatbuffers::IsOutRange(e, LedOverrideLayer::Display, LedOverrideLayer::Draft)) return ""; + const size_t index = static_cast(e); + return EnumNamesLedOverrideLayer()[index]; +} + +/// Brightness of one status LED, 16-bit: 0-65535 of its full output. +struct LedBrightness FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef LedBrightnessBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Types.LedBrightness"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_CRITICAL = 4, + VT_NORMAL = 6 + }; + /// E-Stop and critical-error patterns + uint16_t critical() const { + return GetField(VT_CRITICAL, 0); + } + /// Every other pattern + uint16_t normal() const { + return GetField(VT_NORMAL, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_CRITICAL, 2) && + VerifyField(verifier, VT_NORMAL, 2) && + verifier.EndTable(); + } +}; + +struct LedBrightnessBuilder { + typedef LedBrightness Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_critical(uint16_t critical) { + fbb_.AddElement(LedBrightness::VT_CRITICAL, critical, 0); + } + void add_normal(uint16_t normal) { + fbb_.AddElement(LedBrightness::VT_NORMAL, normal, 0); + } + explicit LedBrightnessBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateLedBrightness( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t critical = 0, + uint16_t normal = 0) { + LedBrightnessBuilder builder_(_fbb); + builder_.add_normal(normal); + builder_.add_critical(critical); + return builder_.Finish(); +} + +struct LedBrightness::Traits { + using type = LedBrightness; + static auto constexpr Create = CreateLedBrightness; +}; + +} // namespace Types +} // namespace Serialization +} // namespace OpenShock + +#endif // FLATBUFFERS_GENERATED_STATUSLED_OPENSHOCK_SERIALIZATION_TYPES_H_ diff --git a/docs/STORAGE.md b/docs/STORAGE.md index c64739b56..6359b4e35 100644 --- a/docs/STORAGE.md +++ b/docs/STORAGE.md @@ -37,6 +37,7 @@ always kept free for compaction) it holds the whole config with some 32 long WiF | `serialInput` | serial_console (`serial_console/SerialInputConfig.h`) | `serial` | | `otaUpdate` | core (`OtaUpdateConfig.h`) | `ota` | | `estop` | device_control (`estop/EStopConfig.h`) | `estop` | +| `led` | device_control (`visual/LedConfig.h`) | `led` | Namespace and key names are at most 15 characters and are permanent once hubs hold them: renaming one is a migration, not an edit. A missing key diff --git a/frontend/src/lib/MessageHandlers/index.ts b/frontend/src/lib/MessageHandlers/index.ts index 698351a8f..203bd936c 100644 --- a/frontend/src/lib/MessageHandlers/index.ts +++ b/frontend/src/lib/MessageHandlers/index.ts @@ -2,10 +2,14 @@ import { AccountLinkStatusEvent } from '#lib/_fbs/open-shock/serialization/local import { ErrorMessage } from '#lib/_fbs/open-shock/serialization/local/error-message.js'; import { HubToLocalMessagePayload } from '#lib/_fbs/open-shock/serialization/local/hub-to-local-message-payload.js'; import { HubToLocalMessage } from '#lib/_fbs/open-shock/serialization/local/hub-to-local-message.js'; +import { LedSettingsChangedEvent } from '#lib/_fbs/open-shock/serialization/local/led-settings-changed-event.js'; +import { LedStatusEvent } from '#lib/_fbs/open-shock/serialization/local/led-status-event.js'; import { ReadyMessage } from '#lib/_fbs/open-shock/serialization/local/ready-message.js'; import { WifiGotIpEvent } from '#lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.js'; import { WifiScanStatusMessage } from '#lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.js'; -import { mapConfig } from '#lib/mappers/ConfigMapper.js'; +import { dispatchLedNotice } from '#lib/ledControl.js'; +import { mapConfig, mapLedConfig } from '#lib/mappers/ConfigMapper.js'; +import { mapLedNotice, mapLedStatus } from '#lib/mappers/LedStatusMapper.js'; import { hubState } from '#lib/stores/index.js'; import type { WebSocketClient } from '#lib/WebSocketClient.js'; import { ByteBuffer } from 'flatbuffers'; @@ -42,6 +46,11 @@ PayloadHandlers[HubToLocalMessagePayload.ReadyMessage] = (cli, msg) => { ); hubState.accountLinked = payload.accountLinked(); hubState.config = mapConfig(payload); + // A new connection holds no LED override: the hub ended the old one's when it went away + hubState.clientId = payload.clientId(); + hubState.connectionEpoch++; + const ledStatus = payload.ledStatus(); + hubState.ledStatus = ledStatus ? mapLedStatus(ledStatus) : null; const gpioValidInputs = payload.gpioValidInputsArray(); if (gpioValidInputs) { @@ -93,6 +102,27 @@ PayloadHandlers[HubToLocalMessagePayload.AccountLinkStatusEvent] = (cli, msg) => hubState.accountLinked = payload.linked(); }; +// A change from anywhere, this client included: the hub's settings as saved +PayloadHandlers[HubToLocalMessagePayload.LedSettingsChangedEvent] = (cli, msg) => { + const payload = new LedSettingsChangedEvent(); + msg.payload(payload); + + const led = payload.led(); + if (led && hubState.config) hubState.config.led = mapLedConfig(led); +}; + +// What the LEDs show changed, an override ended, or an LED request failed; from any client +PayloadHandlers[HubToLocalMessagePayload.LedStatusEvent] = (cli, msg) => { + const payload = new LedStatusEvent(); + msg.payload(payload); + + const status = payload.status(); + if (status) hubState.ledStatus = mapLedStatus(status); + + const notice = mapLedNotice(payload); + if (notice) dispatchLedNotice(notice); +}; + export function WebSocketMessageBinaryHandler(cli: WebSocketClient, data: ArrayBuffer) { const msg = HubToLocalMessage.getRootAsHubToLocalMessage(new ByteBuffer(new Uint8Array(data))); diff --git a/frontend/src/lib/WebSocketClient.ts b/frontend/src/lib/WebSocketClient.ts index 681a64716..2e8aa3730 100644 --- a/frontend/src/lib/WebSocketClient.ts +++ b/frontend/src/lib/WebSocketClient.ts @@ -127,6 +127,11 @@ export class WebSocketClient { }, delay); } + /** Bytes queued but not yet sent; 0 when there is no socket. */ + public get BufferedAmount(): number { + return this.#socket?.bufferedAmount ?? 0; + } + public Send(data: string | Blob | BufferSource): boolean { if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) { return false; diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local.ts b/frontend/src/lib/_fbs/open-shock/serialization/local.ts index 783a2b49c..3210bd941 100644 --- a/frontend/src/lib/_fbs/open-shock/serialization/local.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local.ts @@ -2,5 +2,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ +export { LedCommitDraftMessage } from './local/led-commit-draft-message'; +export { LedDraftMessage } from './local/led-draft-message'; +export { LedOverrideStopMessage } from './local/led-override-stop-message'; +export { LedPreviewColorMessage } from './local/led-preview-color-message'; +export { LedPreviewPatternMessage } from './local/led-preview-pattern-message'; +export { LedSetModeMessage } from './local/led-set-mode-message'; +export { LedSignal } from './local/led-signal'; +export { LedTarget } from './local/led-target'; export { LocalToHubMessage } from './local/local-to-hub-message'; export { LocalToHubMessagePayload } from './local/local-to-hub-message-payload'; diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/hub-to-local-message-payload.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/hub-to-local-message-payload.ts index 29b4069e9..c779aa448 100644 --- a/frontend/src/lib/_fbs/open-shock/serialization/local/hub-to-local-message-payload.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/hub-to-local-message-payload.ts @@ -4,6 +4,8 @@ import { AccountLinkStatusEvent } from '../../../open-shock/serialization/local/account-link-status-event'; import { ErrorMessage } from '../../../open-shock/serialization/local/error-message'; +import { LedSettingsChangedEvent } from '../../../open-shock/serialization/local/led-settings-changed-event'; +import { LedStatusEvent } from '../../../open-shock/serialization/local/led-status-event'; import { ReadyMessage } from '../../../open-shock/serialization/local/ready-message'; import { WifiGotIpEvent } from '../../../open-shock/serialization/local/wifi-got-ip-event'; import { WifiLostIpEvent } from '../../../open-shock/serialization/local/wifi-lost-ip-event'; @@ -19,13 +21,15 @@ export enum HubToLocalMessagePayload { WifiNetworkEvent = 4, WifiGotIpEvent = 5, WifiLostIpEvent = 6, - AccountLinkStatusEvent = 7 + AccountLinkStatusEvent = 7, + LedSettingsChangedEvent = 8, + LedStatusEvent = 9 } export function unionToHubToLocalMessagePayload( type: HubToLocalMessagePayload, - accessor: (obj:AccountLinkStatusEvent|ErrorMessage|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage) => AccountLinkStatusEvent|ErrorMessage|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null -): AccountLinkStatusEvent|ErrorMessage|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null { + accessor: (obj:AccountLinkStatusEvent|ErrorMessage|LedSettingsChangedEvent|LedStatusEvent|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage) => AccountLinkStatusEvent|ErrorMessage|LedSettingsChangedEvent|LedStatusEvent|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null +): AccountLinkStatusEvent|ErrorMessage|LedSettingsChangedEvent|LedStatusEvent|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null { switch(HubToLocalMessagePayload[type]) { case 'NONE': return null; case 'ReadyMessage': return accessor(new ReadyMessage())! as ReadyMessage; @@ -35,15 +39,17 @@ export function unionToHubToLocalMessagePayload( case 'WifiGotIpEvent': return accessor(new WifiGotIpEvent())! as WifiGotIpEvent; case 'WifiLostIpEvent': return accessor(new WifiLostIpEvent())! as WifiLostIpEvent; case 'AccountLinkStatusEvent': return accessor(new AccountLinkStatusEvent())! as AccountLinkStatusEvent; + case 'LedSettingsChangedEvent': return accessor(new LedSettingsChangedEvent())! as LedSettingsChangedEvent; + case 'LedStatusEvent': return accessor(new LedStatusEvent())! as LedStatusEvent; default: return null; } } export function unionListToHubToLocalMessagePayload( type: HubToLocalMessagePayload, - accessor: (index: number, obj:AccountLinkStatusEvent|ErrorMessage|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage) => AccountLinkStatusEvent|ErrorMessage|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null, + accessor: (index: number, obj:AccountLinkStatusEvent|ErrorMessage|LedSettingsChangedEvent|LedStatusEvent|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage) => AccountLinkStatusEvent|ErrorMessage|LedSettingsChangedEvent|LedStatusEvent|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null, index: number -): AccountLinkStatusEvent|ErrorMessage|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null { +): AccountLinkStatusEvent|ErrorMessage|LedSettingsChangedEvent|LedStatusEvent|ReadyMessage|WifiGotIpEvent|WifiLostIpEvent|WifiNetworkEvent|WifiScanStatusMessage|null { switch(HubToLocalMessagePayload[type]) { case 'NONE': return null; case 'ReadyMessage': return accessor(index, new ReadyMessage())! as ReadyMessage; @@ -53,6 +59,8 @@ export function unionListToHubToLocalMessagePayload( case 'WifiGotIpEvent': return accessor(index, new WifiGotIpEvent())! as WifiGotIpEvent; case 'WifiLostIpEvent': return accessor(index, new WifiLostIpEvent())! as WifiLostIpEvent; case 'AccountLinkStatusEvent': return accessor(index, new AccountLinkStatusEvent())! as AccountLinkStatusEvent; + case 'LedSettingsChangedEvent': return accessor(index, new LedSettingsChangedEvent())! as LedSettingsChangedEvent; + case 'LedStatusEvent': return accessor(index, new LedStatusEvent())! as LedStatusEvent; default: return null; } } diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-commit-draft-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-commit-draft-message.ts new file mode 100644 index 000000000..44c2802c6 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-commit-draft-message.ts @@ -0,0 +1,51 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +/** + * Saves the draft the hub holds for this client. It ends as Committed, and LedSettingsChangedEvent follows. + */ +export class LedCommitDraftMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedCommitDraftMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedCommitDraftMessage(bb:flatbuffers.ByteBuffer, obj?:LedCommitDraftMessage):LedCommitDraftMessage { + return (obj || new LedCommitDraftMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedCommitDraftMessage(bb:flatbuffers.ByteBuffer, obj?:LedCommitDraftMessage):LedCommitDraftMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedCommitDraftMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +static startLedCommitDraftMessage(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(0, seq, 0); +} + +static endLedCommitDraftMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedCommitDraftMessage(builder:flatbuffers.Builder, seq:number):flatbuffers.Offset { + LedCommitDraftMessage.startLedCommitDraftMessage(builder); + LedCommitDraftMessage.addSeq(builder, seq); + return LedCommitDraftMessage.endLedCommitDraftMessage(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-draft-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-draft-message.ts new file mode 100644 index 000000000..abcb24ac3 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-draft-message.ts @@ -0,0 +1,69 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedBrightness } from '../../../open-shock/serialization/types/led-brightness'; + + +/** + * Unsaved brightness for the LEDs the board has, shown on whatever the LEDs show. Replaces the client's previous + * draft. The hub drops it (the LEDs return to the saved brightness) ten seconds after the last one unless it is saved + * with LedCommitDraftMessage. + */ +export class LedDraftMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedDraftMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedDraftMessage(bb:flatbuffers.ByteBuffer, obj?:LedDraftMessage):LedDraftMessage { + return (obj || new LedDraftMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedDraftMessage(bb:flatbuffers.ByteBuffer, obj?:LedDraftMessage):LedDraftMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedDraftMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +mono(obj?:LedBrightness):LedBrightness|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new LedBrightness()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +rgb(obj?:LedBrightness):LedBrightness|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new LedBrightness()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startLedDraftMessage(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(0, seq, 0); +} + +static addMono(builder:flatbuffers.Builder, monoOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, monoOffset, 0); +} + +static addRgb(builder:flatbuffers.Builder, rgbOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, rgbOffset, 0); +} + +static endLedDraftMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-end.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-end.ts new file mode 100644 index 000000000..1c2574f69 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-end.ts @@ -0,0 +1,35 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum LedOverrideEnd { + /** + * Its owner sent nothing for it within its timeout + */ + Expired = 0, + + /** + * Its owner ended it (LedOverrideStopMessage) + */ + Stopped = 1, + + /** + * Another client took the layer over + */ + Replaced = 2, + + /** + * Its owner disconnected + */ + OwnerLeft = 3, + + /** + * Saved (a draft only) + */ + Committed = 4, + + /** + * The LED test took over the LEDs + */ + LedTest = 5 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-ended.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-ended.ts new file mode 100644 index 000000000..2a186c02e --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-ended.ts @@ -0,0 +1,85 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedOverrideEnd } from '../../../open-shock/serialization/local/led-override-end'; +import { LedOverrideLayer } from '../../../open-shock/serialization/types/led-override-layer'; + + +export class LedOverrideEnded { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedOverrideEnded { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedOverrideEnded(bb:flatbuffers.ByteBuffer, obj?:LedOverrideEnded):LedOverrideEnded { + return (obj || new LedOverrideEnded()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedOverrideEnded(bb:flatbuffers.ByteBuffer, obj?:LedOverrideEnded):LedOverrideEnded { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedOverrideEnded()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +layer():LedOverrideLayer { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedOverrideLayer.Display; +} + +reason():LedOverrideEnd { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedOverrideEnd.Expired; +} + +owner():number { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; +} + +/** + * The seq of the owner's last message for it + */ +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +static startLedOverrideEnded(builder:flatbuffers.Builder) { + builder.startObject(4); +} + +static addLayer(builder:flatbuffers.Builder, layer:LedOverrideLayer) { + builder.addFieldInt8(0, layer, LedOverrideLayer.Display); +} + +static addReason(builder:flatbuffers.Builder, reason:LedOverrideEnd) { + builder.addFieldInt8(1, reason, LedOverrideEnd.Expired); +} + +static addOwner(builder:flatbuffers.Builder, owner:number) { + builder.addFieldInt8(2, owner, 0); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(3, seq, 0); +} + +static endLedOverrideEnded(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedOverrideEnded(builder:flatbuffers.Builder, layer:LedOverrideLayer, reason:LedOverrideEnd, owner:number, seq:number):flatbuffers.Offset { + LedOverrideEnded.startLedOverrideEnded(builder); + LedOverrideEnded.addLayer(builder, layer); + LedOverrideEnded.addReason(builder, reason); + LedOverrideEnded.addOwner(builder, owner); + LedOverrideEnded.addSeq(builder, seq); + return LedOverrideEnded.endLedOverrideEnded(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-stop-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-stop-message.ts new file mode 100644 index 000000000..b94602c20 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override-stop-message.ts @@ -0,0 +1,64 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedOverrideLayer } from '../../../open-shock/serialization/types/led-override-layer'; + + +/** + * Ends the client's own override on `layer` early. + */ +export class LedOverrideStopMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedOverrideStopMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedOverrideStopMessage(bb:flatbuffers.ByteBuffer, obj?:LedOverrideStopMessage):LedOverrideStopMessage { + return (obj || new LedOverrideStopMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedOverrideStopMessage(bb:flatbuffers.ByteBuffer, obj?:LedOverrideStopMessage):LedOverrideStopMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedOverrideStopMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +layer():LedOverrideLayer { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedOverrideLayer.Display; +} + +static startLedOverrideStopMessage(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(0, seq, 0); +} + +static addLayer(builder:flatbuffers.Builder, layer:LedOverrideLayer) { + builder.addFieldInt8(1, layer, LedOverrideLayer.Display); +} + +static endLedOverrideStopMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedOverrideStopMessage(builder:flatbuffers.Builder, seq:number, layer:LedOverrideLayer):flatbuffers.Offset { + LedOverrideStopMessage.startLedOverrideStopMessage(builder); + LedOverrideStopMessage.addSeq(builder, seq); + LedOverrideStopMessage.addLayer(builder, layer); + return LedOverrideStopMessage.endLedOverrideStopMessage(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-override.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override.ts new file mode 100644 index 000000000..802b17246 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-override.ts @@ -0,0 +1,67 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +/** + * An override on one layer, while it lasts. + */ +export class LedOverride { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedOverride { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedOverride(bb:flatbuffers.ByteBuffer, obj?:LedOverride):LedOverride { + return (obj || new LedOverride()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedOverride(bb:flatbuffers.ByteBuffer, obj?:LedOverride):LedOverride { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedOverride()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +/** + * The client that holds it (ReadyMessage.client_id) + */ +owner():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; +} + +/** + * How long it lasts after the owner's last message for it (a pattern preview: from its start) + */ +timeoutMs():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +static startLedOverride(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addOwner(builder:flatbuffers.Builder, owner:number) { + builder.addFieldInt8(0, owner, 0); +} + +static addTimeoutMs(builder:flatbuffers.Builder, timeoutMs:number) { + builder.addFieldInt16(1, timeoutMs, 0); +} + +static endLedOverride(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedOverride(builder:flatbuffers.Builder, owner:number, timeoutMs:number):flatbuffers.Offset { + LedOverride.startLedOverride(builder); + LedOverride.addOwner(builder, owner); + LedOverride.addTimeoutMs(builder, timeoutMs); + return LedOverride.endLedOverride(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-preview-color-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-preview-color-message.ts new file mode 100644 index 000000000..49282ffd3 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-preview-color-message.ts @@ -0,0 +1,115 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedSignal } from '../../../open-shock/serialization/local/led-signal'; +import { LedTarget } from '../../../open-shock/serialization/local/led-target'; + + +/** + * Shows a solid color: the RGB LED in the color, the mono LED on unless it is black. Sent at a high rate while the + * user drags a color or brightness control; the hub ends it two seconds after the last one. Nothing is saved. + */ +export class LedPreviewColorMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedPreviewColorMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedPreviewColorMessage(bb:flatbuffers.ByteBuffer, obj?:LedPreviewColorMessage):LedPreviewColorMessage { + return (obj || new LedPreviewColorMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedPreviewColorMessage(bb:flatbuffers.ByteBuffer, obj?:LedPreviewColorMessage):LedPreviewColorMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedPreviewColorMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +/** + * 16 bits per channel (0-65535); the hub reduces them to what the RGB LED shows. + */ +red():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +green():number { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +blue():number { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +/** + * The LED to light; the other stays dark. A target the board doesn't have lights nothing. + */ +target():LedTarget { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedTarget.All; +} + +/** + * Which brightness setting it shows at: the client's draft if it holds one, else the saved one. + */ +signal():LedSignal { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedSignal.Normal; +} + +static startLedPreviewColorMessage(builder:flatbuffers.Builder) { + builder.startObject(6); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(0, seq, 0); +} + +static addRed(builder:flatbuffers.Builder, red:number) { + builder.addFieldInt16(1, red, 0); +} + +static addGreen(builder:flatbuffers.Builder, green:number) { + builder.addFieldInt16(2, green, 0); +} + +static addBlue(builder:flatbuffers.Builder, blue:number) { + builder.addFieldInt16(3, blue, 0); +} + +static addTarget(builder:flatbuffers.Builder, target:LedTarget) { + builder.addFieldInt8(4, target, LedTarget.All); +} + +static addSignal(builder:flatbuffers.Builder, signal:LedSignal) { + builder.addFieldInt8(5, signal, LedSignal.Normal); +} + +static endLedPreviewColorMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedPreviewColorMessage(builder:flatbuffers.Builder, seq:number, red:number, green:number, blue:number, target:LedTarget, signal:LedSignal):flatbuffers.Offset { + LedPreviewColorMessage.startLedPreviewColorMessage(builder); + LedPreviewColorMessage.addSeq(builder, seq); + LedPreviewColorMessage.addRed(builder, red); + LedPreviewColorMessage.addGreen(builder, green); + LedPreviewColorMessage.addBlue(builder, blue); + LedPreviewColorMessage.addTarget(builder, target); + LedPreviewColorMessage.addSignal(builder, signal); + return LedPreviewColorMessage.endLedPreviewColorMessage(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-preview-pattern-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-preview-pattern-message.ts new file mode 100644 index 000000000..5c0ed8335 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-preview-pattern-message.ts @@ -0,0 +1,64 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedPattern } from '../../../open-shock/serialization/types/led-pattern'; + + +/** + * Shows a state's pattern whatever the LED mode, for about two of its cycles. Nothing is saved. + */ +export class LedPreviewPatternMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedPreviewPatternMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedPreviewPatternMessage(bb:flatbuffers.ByteBuffer, obj?:LedPreviewPatternMessage):LedPreviewPatternMessage { + return (obj || new LedPreviewPatternMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedPreviewPatternMessage(bb:flatbuffers.ByteBuffer, obj?:LedPreviewPatternMessage):LedPreviewPatternMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedPreviewPatternMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +pattern():LedPattern { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedPattern.WifiDisconnected; +} + +static startLedPreviewPatternMessage(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(0, seq, 0); +} + +static addPattern(builder:flatbuffers.Builder, pattern:LedPattern) { + builder.addFieldInt8(1, pattern, LedPattern.WifiDisconnected); +} + +static endLedPreviewPatternMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedPreviewPatternMessage(builder:flatbuffers.Builder, seq:number, pattern:LedPattern):flatbuffers.Offset { + LedPreviewPatternMessage.startLedPreviewPatternMessage(builder); + LedPreviewPatternMessage.addSeq(builder, seq); + LedPreviewPatternMessage.addPattern(builder, pattern); + return LedPreviewPatternMessage.endLedPreviewPatternMessage(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-request-error.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-request-error.ts new file mode 100644 index 000000000..b25e439a2 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-request-error.ts @@ -0,0 +1,30 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum LedRequestError { + /** + * The board has none of the LEDs the request is for + */ + NoLed = 0, + + /** + * The LED test holds the LEDs + */ + LedTestRunning = 1, + + /** + * Nothing to commit or stop: the client holds no override on that layer + */ + NotHeld = 2, + + /** + * The settings could not be saved + */ + SaveFailed = 3, + + /** + * A value the hub doesn't know + */ + InvalidValue = 4 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-request-failed.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-request-failed.ts new file mode 100644 index 000000000..64dd368ae --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-request-failed.ts @@ -0,0 +1,71 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedRequestError } from '../../../open-shock/serialization/local/led-request-error'; + + +export class LedRequestFailed { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedRequestFailed { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedRequestFailed(bb:flatbuffers.ByteBuffer, obj?:LedRequestFailed):LedRequestFailed { + return (obj || new LedRequestFailed()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedRequestFailed(bb:flatbuffers.ByteBuffer, obj?:LedRequestFailed):LedRequestFailed { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedRequestFailed()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +owner():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +error():LedRequestError { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedRequestError.NoLed; +} + +static startLedRequestFailed(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addOwner(builder:flatbuffers.Builder, owner:number) { + builder.addFieldInt8(0, owner, 0); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(1, seq, 0); +} + +static addError(builder:flatbuffers.Builder, error:LedRequestError) { + builder.addFieldInt8(2, error, LedRequestError.NoLed); +} + +static endLedRequestFailed(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedRequestFailed(builder:flatbuffers.Builder, owner:number, seq:number, error:LedRequestError):flatbuffers.Offset { + LedRequestFailed.startLedRequestFailed(builder); + LedRequestFailed.addOwner(builder, owner); + LedRequestFailed.addSeq(builder, seq); + LedRequestFailed.addError(builder, error); + return LedRequestFailed.endLedRequestFailed(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-set-mode-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-set-mode-message.ts new file mode 100644 index 000000000..28d62b86e --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-set-mode-message.ts @@ -0,0 +1,64 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedMode } from '../../../open-shock/serialization/types/led-mode'; + + +/** + * Saves the LED mode. LedSettingsChangedEvent follows. + */ +export class LedSetModeMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedSetModeMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedSetModeMessage(bb:flatbuffers.ByteBuffer, obj?:LedSetModeMessage):LedSetModeMessage { + return (obj || new LedSetModeMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedSetModeMessage(bb:flatbuffers.ByteBuffer, obj?:LedSetModeMessage):LedSetModeMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedSetModeMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +seq():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +mode():LedMode { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedMode.All; +} + +static startLedSetModeMessage(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addSeq(builder:flatbuffers.Builder, seq:number) { + builder.addFieldInt16(0, seq, 0); +} + +static addMode(builder:flatbuffers.Builder, mode:LedMode) { + builder.addFieldInt8(1, mode, LedMode.All); +} + +static endLedSetModeMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedSetModeMessage(builder:flatbuffers.Builder, seq:number, mode:LedMode):flatbuffers.Offset { + LedSetModeMessage.startLedSetModeMessage(builder); + LedSetModeMessage.addSeq(builder, seq); + LedSetModeMessage.addMode(builder, mode); + return LedSetModeMessage.endLedSetModeMessage(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-settings-changed-event.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-settings-changed-event.ts new file mode 100644 index 000000000..77e5a3367 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-settings-changed-event.ts @@ -0,0 +1,54 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedSettings } from '../../../open-shock/serialization/local/led-settings'; + + +/** + * Sent to every connected client when the LED settings change, from any source (the portal, the serial console) + */ +export class LedSettingsChangedEvent { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedSettingsChangedEvent { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedSettingsChangedEvent(bb:flatbuffers.ByteBuffer, obj?:LedSettingsChangedEvent):LedSettingsChangedEvent { + return (obj || new LedSettingsChangedEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedSettingsChangedEvent(bb:flatbuffers.ByteBuffer, obj?:LedSettingsChangedEvent):LedSettingsChangedEvent { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedSettingsChangedEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +led(obj?:LedSettings):LedSettings|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new LedSettings()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startLedSettingsChangedEvent(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addLed(builder:flatbuffers.Builder, ledOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, ledOffset, 0); +} + +static endLedSettingsChangedEvent(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedSettingsChangedEvent(builder:flatbuffers.Builder, ledOffset:flatbuffers.Offset):flatbuffers.Offset { + LedSettingsChangedEvent.startLedSettingsChangedEvent(builder); + LedSettingsChangedEvent.addLed(builder, ledOffset); + return LedSettingsChangedEvent.endLedSettingsChangedEvent(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-settings.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-settings.ts new file mode 100644 index 000000000..1b64333be --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-settings.ts @@ -0,0 +1,87 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedBrightness } from '../../../open-shock/serialization/types/led-brightness'; +import { LedMode } from '../../../open-shock/serialization/types/led-mode'; + + +/** + * The status LED settings the portal shows and edits. Only the LEDs the board has are present, so the portal shows + * no option for an LED that isn't there. + */ +export class LedSettings { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedSettings { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedSettings(bb:flatbuffers.ByteBuffer, obj?:LedSettings):LedSettings { + return (obj || new LedSettings()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedSettings(bb:flatbuffers.ByteBuffer, obj?:LedSettings):LedSettings { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedSettings()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +mode():LedMode { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedMode.All; +} + +/** + * The single-color built-in LED; absent when the board has none. + */ +mono(obj?:LedBrightness):LedBrightness|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new LedBrightness()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * The WS2812B RGB LED; absent when the board has none. + */ +rgb(obj?:LedBrightness):LedBrightness|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new LedBrightness()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * The hub warns about an RGB brightness above this (0-65535). Only meaningful with `rgb`. + */ +rgbWarnAbove():number { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +static startLedSettings(builder:flatbuffers.Builder) { + builder.startObject(4); +} + +static addMode(builder:flatbuffers.Builder, mode:LedMode) { + builder.addFieldInt8(0, mode, LedMode.All); +} + +static addMono(builder:flatbuffers.Builder, monoOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, monoOffset, 0); +} + +static addRgb(builder:flatbuffers.Builder, rgbOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, rgbOffset, 0); +} + +static addRgbWarnAbove(builder:flatbuffers.Builder, rgbWarnAbove:number) { + builder.addFieldInt16(3, rgbWarnAbove, 0); +} + +static endLedSettings(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-showing.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-showing.ts new file mode 100644 index 000000000..d892911ea --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-showing.ts @@ -0,0 +1,33 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +/** + * What the status LEDs show right now. + */ +export enum LedShowing { + /** + * The hub's state, in LedStatus.pattern + */ + Live = 0, + + /** + * The hub's state, but the LED mode hides it, so the LEDs are dark + */ + Hidden = 1, + + /** + * A previewed pattern, in LedStatus.pattern + */ + Pattern = 2, + + /** + * A previewed color + */ + Color = 3, + + /** + * The LED test (serial console); overrides wait until it is done + */ + Test = 4 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-signal.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-signal.ts new file mode 100644 index 000000000..29af229e5 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-signal.ts @@ -0,0 +1,8 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum LedSignal { + Normal = 0, + Critical = 1 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-status-event.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-status-event.ts new file mode 100644 index 000000000..2ecb7ee88 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-status-event.ts @@ -0,0 +1,76 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedOverrideEnded } from '../../../open-shock/serialization/local/led-override-ended'; +import { LedRequestFailed } from '../../../open-shock/serialization/local/led-request-failed'; +import { LedStatus } from '../../../open-shock/serialization/local/led-status'; + + +/** + * Sent to every connected client when what the LEDs show changes, an override starts or ends, or an LED request + * fails. Not sent for every frame of a dragged control: a client's repeated messages change nothing here. + */ +export class LedStatusEvent { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedStatusEvent { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedStatusEvent(bb:flatbuffers.ByteBuffer, obj?:LedStatusEvent):LedStatusEvent { + return (obj || new LedStatusEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedStatusEvent(bb:flatbuffers.ByteBuffer, obj?:LedStatusEvent):LedStatusEvent { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedStatusEvent()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +status(obj?:LedStatus):LedStatus|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new LedStatus()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * Present when this event reports an override ending + */ +ended(obj?:LedOverrideEnded):LedOverrideEnded|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new LedOverrideEnded()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * Present when this event reports a request failing + */ +failed(obj?:LedRequestFailed):LedRequestFailed|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new LedRequestFailed()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startLedStatusEvent(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addStatus(builder:flatbuffers.Builder, statusOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, statusOffset, 0); +} + +static addEnded(builder:flatbuffers.Builder, endedOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, endedOffset, 0); +} + +static addFailed(builder:flatbuffers.Builder, failedOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, failedOffset, 0); +} + +static endLedStatusEvent(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-status.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-status.ts new file mode 100644 index 000000000..c4f4bc8c4 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-status.ts @@ -0,0 +1,84 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { LedOverride } from '../../../open-shock/serialization/local/led-override'; +import { LedShowing } from '../../../open-shock/serialization/local/led-showing'; +import { LedPattern } from '../../../open-shock/serialization/types/led-pattern'; + + +export class LedStatus { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedStatus { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedStatus(bb:flatbuffers.ByteBuffer, obj?:LedStatus):LedStatus { + return (obj || new LedStatus()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedStatus(bb:flatbuffers.ByteBuffer, obj?:LedStatus):LedStatus { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedStatus()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +showing():LedShowing { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedShowing.Live; +} + +/** + * The pattern shown, for Live and Pattern + */ +pattern():LedPattern { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : LedPattern.WifiDisconnected; +} + +/** + * Present while a preview holds the LEDs + */ +display(obj?:LedOverride):LedOverride|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new LedOverride()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * Present while unsaved brightness is applied + */ +draft(obj?:LedOverride):LedOverride|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? (obj || new LedOverride()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startLedStatus(builder:flatbuffers.Builder) { + builder.startObject(4); +} + +static addShowing(builder:flatbuffers.Builder, showing:LedShowing) { + builder.addFieldInt8(0, showing, LedShowing.Live); +} + +static addPattern(builder:flatbuffers.Builder, pattern:LedPattern) { + builder.addFieldInt8(1, pattern, LedPattern.WifiDisconnected); +} + +static addDisplay(builder:flatbuffers.Builder, displayOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, displayOffset, 0); +} + +static addDraft(builder:flatbuffers.Builder, draftOffset:flatbuffers.Offset) { + builder.addFieldOffset(3, draftOffset, 0); +} + +static endLedStatus(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/led-target.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/led-target.ts new file mode 100644 index 000000000..68050a6f3 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/led-target.ts @@ -0,0 +1,9 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum LedTarget { + All = 0, + Mono = 1, + Rgb = 2 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-hub-message-payload.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-hub-message-payload.ts index d3dff488b..488527cab 100644 --- a/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-hub-message-payload.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-hub-message-payload.ts @@ -3,32 +3,56 @@ /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ import { ShockerCommandList } from '../../../open-shock/serialization/common/shocker-command-list'; +import { LedCommitDraftMessage } from '../../../open-shock/serialization/local/led-commit-draft-message'; +import { LedDraftMessage } from '../../../open-shock/serialization/local/led-draft-message'; +import { LedOverrideStopMessage } from '../../../open-shock/serialization/local/led-override-stop-message'; +import { LedPreviewColorMessage } from '../../../open-shock/serialization/local/led-preview-color-message'; +import { LedPreviewPatternMessage } from '../../../open-shock/serialization/local/led-preview-pattern-message'; +import { LedSetModeMessage } from '../../../open-shock/serialization/local/led-set-mode-message'; export enum LocalToHubMessagePayload { NONE = 0, - Common_ShockerCommandList = 1 + Common_ShockerCommandList = 1, + LedPreviewPatternMessage = 2, + LedPreviewColorMessage = 3, + LedDraftMessage = 4, + LedOverrideStopMessage = 5, + LedCommitDraftMessage = 6, + LedSetModeMessage = 7 } export function unionToLocalToHubMessagePayload( type: LocalToHubMessagePayload, - accessor: (obj:ShockerCommandList) => ShockerCommandList|null -): ShockerCommandList|null { + accessor: (obj:LedCommitDraftMessage|LedDraftMessage|LedOverrideStopMessage|LedPreviewColorMessage|LedPreviewPatternMessage|LedSetModeMessage|ShockerCommandList) => LedCommitDraftMessage|LedDraftMessage|LedOverrideStopMessage|LedPreviewColorMessage|LedPreviewPatternMessage|LedSetModeMessage|ShockerCommandList|null +): LedCommitDraftMessage|LedDraftMessage|LedOverrideStopMessage|LedPreviewColorMessage|LedPreviewPatternMessage|LedSetModeMessage|ShockerCommandList|null { switch(LocalToHubMessagePayload[type]) { case 'NONE': return null; case 'Common_ShockerCommandList': return accessor(new ShockerCommandList())! as ShockerCommandList; + case 'LedPreviewPatternMessage': return accessor(new LedPreviewPatternMessage())! as LedPreviewPatternMessage; + case 'LedPreviewColorMessage': return accessor(new LedPreviewColorMessage())! as LedPreviewColorMessage; + case 'LedDraftMessage': return accessor(new LedDraftMessage())! as LedDraftMessage; + case 'LedOverrideStopMessage': return accessor(new LedOverrideStopMessage())! as LedOverrideStopMessage; + case 'LedCommitDraftMessage': return accessor(new LedCommitDraftMessage())! as LedCommitDraftMessage; + case 'LedSetModeMessage': return accessor(new LedSetModeMessage())! as LedSetModeMessage; default: return null; } } export function unionListToLocalToHubMessagePayload( type: LocalToHubMessagePayload, - accessor: (index: number, obj:ShockerCommandList) => ShockerCommandList|null, + accessor: (index: number, obj:LedCommitDraftMessage|LedDraftMessage|LedOverrideStopMessage|LedPreviewColorMessage|LedPreviewPatternMessage|LedSetModeMessage|ShockerCommandList) => LedCommitDraftMessage|LedDraftMessage|LedOverrideStopMessage|LedPreviewColorMessage|LedPreviewPatternMessage|LedSetModeMessage|ShockerCommandList|null, index: number -): ShockerCommandList|null { +): LedCommitDraftMessage|LedDraftMessage|LedOverrideStopMessage|LedPreviewColorMessage|LedPreviewPatternMessage|LedSetModeMessage|ShockerCommandList|null { switch(LocalToHubMessagePayload[type]) { case 'NONE': return null; case 'Common_ShockerCommandList': return accessor(index, new ShockerCommandList())! as ShockerCommandList; + case 'LedPreviewPatternMessage': return accessor(index, new LedPreviewPatternMessage())! as LedPreviewPatternMessage; + case 'LedPreviewColorMessage': return accessor(index, new LedPreviewColorMessage())! as LedPreviewColorMessage; + case 'LedDraftMessage': return accessor(index, new LedDraftMessage())! as LedDraftMessage; + case 'LedOverrideStopMessage': return accessor(index, new LedOverrideStopMessage())! as LedOverrideStopMessage; + case 'LedCommitDraftMessage': return accessor(index, new LedCommitDraftMessage())! as LedCommitDraftMessage; + case 'LedSetModeMessage': return accessor(index, new LedSetModeMessage())! as LedSetModeMessage; default: return null; } } diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts index d15a2d73c..2231489bf 100644 --- a/frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts @@ -5,6 +5,8 @@ import * as flatbuffers from 'flatbuffers'; import { AccessPointSettings } from '../../../open-shock/serialization/local/access-point-settings'; +import { LedSettings } from '../../../open-shock/serialization/local/led-settings'; +import { LedStatus } from '../../../open-shock/serialization/local/led-status'; import { OtaSettings } from '../../../open-shock/serialization/local/ota-settings'; import { WifiNetwork } from '../../../open-shock/serialization/types/wifi-network'; @@ -117,8 +119,26 @@ accessPoint(obj?:AccessPointSettings):AccessPointSettings|null { return offset ? (obj || new AccessPointSettings()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } +led(obj?:LedSettings):LedSettings|null { + const offset = this.bb!.__offset(this.bb_pos, 28); + return offset ? (obj || new LedSettings()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +ledStatus(obj?:LedStatus):LedStatus|null { + const offset = this.bb!.__offset(this.bb_pos, 30); + return offset ? (obj || new LedStatus()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * This connection's id, as LedOverride.owner and the owner fields of LED events name it + */ +clientId():number { + const offset = this.bb!.__offset(this.bb_pos, 32); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; +} + static startReadyMessage(builder:flatbuffers.Builder) { - builder.startObject(12); + builder.startObject(15); } static addPoggies(builder:flatbuffers.Builder, poggies:boolean) { @@ -215,6 +235,18 @@ static addAccessPoint(builder:flatbuffers.Builder, accessPointOffset:flatbuffers builder.addFieldOffset(11, accessPointOffset, 0); } +static addLed(builder:flatbuffers.Builder, ledOffset:flatbuffers.Offset) { + builder.addFieldOffset(12, ledOffset, 0); +} + +static addLedStatus(builder:flatbuffers.Builder, ledStatusOffset:flatbuffers.Offset) { + builder.addFieldOffset(13, ledStatusOffset, 0); +} + +static addClientId(builder:flatbuffers.Builder, clientId:number) { + builder.addFieldInt8(14, clientId, 0); +} + static endReadyMessage(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/led-brightness.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/led-brightness.ts new file mode 100644 index 000000000..6f7af3376 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/led-brightness.ts @@ -0,0 +1,67 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +/** + * Brightness of one status LED, 16-bit: 0-65535 of its full output. + */ +export class LedBrightness { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):LedBrightness { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsLedBrightness(bb:flatbuffers.ByteBuffer, obj?:LedBrightness):LedBrightness { + return (obj || new LedBrightness()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsLedBrightness(bb:flatbuffers.ByteBuffer, obj?:LedBrightness):LedBrightness { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new LedBrightness()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +/** + * E-Stop and critical-error patterns + */ +critical():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +/** + * Every other pattern + */ +normal():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +static startLedBrightness(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addCritical(builder:flatbuffers.Builder, critical:number) { + builder.addFieldInt16(0, critical, 0); +} + +static addNormal(builder:flatbuffers.Builder, normal:number) { + builder.addFieldInt16(1, normal, 0); +} + +static endLedBrightness(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createLedBrightness(builder:flatbuffers.Builder, critical:number, normal:number):flatbuffers.Offset { + LedBrightness.startLedBrightness(builder); + LedBrightness.addCritical(builder, critical); + LedBrightness.addNormal(builder, normal); + return LedBrightness.endLedBrightness(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/led-mode.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/led-mode.ts new file mode 100644 index 000000000..a8fc78abe --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/led-mode.ts @@ -0,0 +1,9 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum LedMode { + All = 0, + CriticalOnly = 1, + Off = 2 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/led-override-layer.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/led-override-layer.ts new file mode 100644 index 000000000..c7ac6ebbf --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/led-override-layer.ts @@ -0,0 +1,19 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +/** + * The two things a portal can put on the LEDs without saving them. Each belongs to the client that sent it and ends + * on its own after a while unless that client keeps sending. + */ +export enum LedOverrideLayer { + /** + * A previewed pattern or color, shown instead of the hub's state + */ + Display = 0, + + /** + * Unsaved brightness, applied to whatever the LEDs show (the hub's state or a preview) + */ + Draft = 1 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/led-pattern.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/led-pattern.ts new file mode 100644 index 000000000..f3a259336 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/led-pattern.ts @@ -0,0 +1,17 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +/** + * A hub state's LED pattern. + */ +export enum LedPattern { + WifiDisconnected = 0, + WifiScanning = 1, + NoBackend = 2, + Connected = 3, + EStopped = 4, + EStopClearing = 5, + EStopAwaitingRelease = 6, + CriticalError = 7 +} diff --git a/frontend/src/lib/components/ColorWheel.svelte b/frontend/src/lib/components/ColorWheel.svelte new file mode 100644 index 000000000..4f08ee55f --- /dev/null +++ b/frontend/src/lib/components/ColorWheel.svelte @@ -0,0 +1,113 @@ + + +
+ +
diff --git a/frontend/src/lib/components/LabeledSlider.svelte b/frontend/src/lib/components/LabeledSlider.svelte new file mode 100644 index 000000000..b5db00016 --- /dev/null +++ b/frontend/src/lib/components/LabeledSlider.svelte @@ -0,0 +1,72 @@ + + + + {#snippet children({ thumbItems })} + + + + {#each thumbItems as thumb (thumb.index)} + + {/each} + {/snippet} + diff --git a/frontend/src/lib/components/sections/LedSection.svelte b/frontend/src/lib/components/sections/LedSection.svelte new file mode 100644 index 000000000..cef63c4b8 --- /dev/null +++ b/frontend/src/lib/components/sections/LedSection.svelte @@ -0,0 +1,565 @@ + + +
+ + + {#if !led} +

Loading...

+ {:else if presentTypes.length === 0} +

This board has no status LED.

+ {:else} + {#if showingText} +
+

+ LEDs now show: + {showingText} +

+ {#if myDisplay && displaySeconds !== null} +

+ Your preview ends in {displaySeconds}s unless you keep interacting. +

+ {:else if otherDisplay} +

Another portal is previewing on the LEDs.

+ {/if} + {#if otherDraft} +

Another portal is adjusting the brightness.

+ {/if} +
+ {/if} + + {#if note} +

+ {#if note.tone === 'warning'} + + {:else} + + {/if} + {note.text} +

+ {/if} + +
+ +
+ {#each modes as mode (mode.value)} + {@const active = led.mode === mode.value} + {@const pending = pendingMode?.mode === mode.value} + + {/each} +
+

+ {modes.find((m) => m.value === led.mode)?.description} +

+
+ + {#each presentTypes as { type, label } (type)} +
+

{label} Brightness

+ {#each signals as s (s.signal)} + {@const value = brightness(type, s.signal)} + {@const edited = edits[settingKey(type, s.signal)] !== undefined} + {@const labelId = `led-${type}-${s.signal}-label`} +
+
+ + {s.label} + ({s.description}) + + +
+ onBrightnessChange(type, s.signal, v)} + /> + {#if type === 'rgb' && value > led.rgbWarnAbove} +

+ + Above {percentText(led.rgbWarnAbove)} the RGB LED is glaring and runs warm. +

+ {/if} +
+ {/each} +
+ {/each} + +
+
+ + +
+ {#if dirty && myDraft && draftSeconds !== null} +
+

+ Unsaved: the LEDs show it, and go back to the saved brightness in {draftSeconds}s unless + you save or keep adjusting. +

+
+
+
+
+ {:else if dirty && !saving} +

Unsaved changes.

+ {/if} +
+ +
+ +
+ {#each patterns as pattern (pattern.value)} + {@const active = + myDisplay && status?.showing === 'pattern' && status.pattern === pattern.value} + + {/each} +
+
+ + {#if led.rgb.present} +
+ +
+ +
+ + #{color} + {#if myDisplay && status?.showing === 'color'} + On the LED + {/if} +
+
+
+ {/if} + +
+

+ Previews show a state's pattern whatever the mode{led.rgb.present + ? ', or light the RGB LED in the picked color' + : ''}, at the brightness on the sliders. A color preview ends shortly after you stop + moving it, a pattern after a couple of its cycles. Nothing is saved until you press Save. +

+
+ + + {/if} +
diff --git a/frontend/src/lib/ledControl.ts b/frontend/src/lib/ledControl.ts new file mode 100644 index 000000000..e1e462afe --- /dev/null +++ b/frontend/src/lib/ledControl.ts @@ -0,0 +1,208 @@ +import { LedCommitDraftMessage } from '#lib/_fbs/open-shock/serialization/local/led-commit-draft-message.js'; +import { LedDraftMessage } from '#lib/_fbs/open-shock/serialization/local/led-draft-message.js'; +import { LedOverrideStopMessage } from '#lib/_fbs/open-shock/serialization/local/led-override-stop-message.js'; +import { LedPreviewColorMessage } from '#lib/_fbs/open-shock/serialization/local/led-preview-color-message.js'; +import { LedPreviewPatternMessage } from '#lib/_fbs/open-shock/serialization/local/led-preview-pattern-message.js'; +import { LedSetModeMessage } from '#lib/_fbs/open-shock/serialization/local/led-set-mode-message.js'; +import { LedSignal } from '#lib/_fbs/open-shock/serialization/local/led-signal.js'; +import { LedTarget } from '#lib/_fbs/open-shock/serialization/local/led-target.js'; +import { LocalToHubMessagePayload } from '#lib/_fbs/open-shock/serialization/local/local-to-hub-message-payload.js'; +import { LocalToHubMessage } from '#lib/_fbs/open-shock/serialization/local/local-to-hub-message.js'; +import { LedBrightness } from '#lib/_fbs/open-shock/serialization/types/led-brightness.js'; +import { LedMode as FbsLedMode } from '#lib/_fbs/open-shock/serialization/types/led-mode.js'; +import { LedOverrideLayer } from '#lib/_fbs/open-shock/serialization/types/led-override-layer.js'; +import { LedPattern } from '#lib/_fbs/open-shock/serialization/types/led-pattern.js'; +import type { LedMode } from '#lib/mappers/ConfigMapper.js'; +import type { LedNotice } from '#lib/mappers/LedStatusMapper.js'; +import type { Rgb } from '#lib/utils/color.js'; +import { ConnectionState, WebSocketClient } from '#lib/WebSocketClient.js'; +import { Builder as FlatbufferBuilder } from 'flatbuffers'; + +// Everything the portal does to the status LEDs goes over the WebSocket: previews and unsaved brightness (a dragged +// control sends them many times a second), and saving. The hub times every override and reports each outcome in a +// LedStatusEvent, which the message handler turns into hubState.ledStatus and the notices below. Each send returns +// the request's seq, which the hub echoes in a notice about it, or null when not connected. + +export { LedOverrideLayer, LedPattern, LedSignal, LedTarget }; + +// At most this often while dragging; the LED can't show changes faster than a person can see them anyway +const MIN_INTERVAL_MS = 40; + +let nextSeq = 1; +function takeSeq(): number { + const seq = nextSeq; + nextSeq = nextSeq >= 0xffff ? 1 : nextSeq + 1; + return seq; +} + +function connected(): boolean { + return WebSocketClient.Instance.ConnectionState === ConnectionState.CONNECTED; +} + +function send( + payloadType: LocalToHubMessagePayload, + build: (fbb: FlatbufferBuilder, seq: number) => number +): number | null { + const seq = takeSeq(); + const fbb = new FlatbufferBuilder(64); + const payload = build(fbb, seq); + const msg = LocalToHubMessage.createLocalToHubMessage(fbb, payloadType, payload); + fbb.finish(msg); + return WebSocketClient.Instance.Send(new Uint8Array(fbb.asUint8Array())) ? seq : null; +} + +// --- Notices from the hub --- + +type NoticeListener = (notice: LedNotice) => void; +const listeners = new Set(); + +/** Calls `listener` with every LED notice from the hub, every client's. Returns the unsubscribe function. */ +export function onLedNotice(listener: NoticeListener): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +/** For the message handler. */ +export function dispatchLedNotice(notice: LedNotice) { + for (const listener of listeners) listener(notice); +} + +// --- Rate-limited streams: the latest color and draft, sent together at most every MIN_INTERVAL_MS --- + +export interface LedColorPreview { + color: Rgb; + target?: LedTarget; // Default: every LED + signal?: LedSignal; // The brightness setting it shows at; default: normal +} + +export interface LedBrightnessPair { + critical: number; // 0-65535 + normal: number; +} + +export interface LedDraft { + mono?: LedBrightnessPair; + rgb?: LedBrightnessPair; +} + +// A 0-1 channel as the 16 bits the hub takes +const toUint16 = (v: number) => Math.min(65535, Math.max(0, Math.round(v * 65535))); + +function sendColor({ color, target = LedTarget.All, signal = LedSignal.Normal }: LedColorPreview) { + return send(LocalToHubMessagePayload.LedPreviewColorMessage, (fbb, seq) => + LedPreviewColorMessage.createLedPreviewColorMessage( + fbb, + seq, + toUint16(color.r), + toUint16(color.g), + toUint16(color.b), + target, + signal + ) + ); +} + +function sendDraft(draft: LedDraft) { + return send(LocalToHubMessagePayload.LedDraftMessage, (fbb, seq) => { + const pair = (p?: LedBrightnessPair) => + p ? LedBrightness.createLedBrightness(fbb, p.critical, p.normal) : 0; + const mono = pair(draft.mono); + const rgb = pair(draft.rgb); + LedDraftMessage.startLedDraftMessage(fbb); + LedDraftMessage.addSeq(fbb, seq); + if (mono) LedDraftMessage.addMono(fbb, mono); + if (rgb) LedDraftMessage.addRgb(fbb, rgb); + return LedDraftMessage.endLedDraftMessage(fbb); + }); +} + +let pendingColor: LedColorPreview | null = null; +let pendingDraft: LedDraft | null = null; +let timer: ReturnType | null = null; +let lastSent = 0; + +function schedule() { + if (timer !== null) return; + const wait = Math.max(0, lastSent + MIN_INTERVAL_MS - Date.now()); + timer = setTimeout(flush, wait); +} + +function flush() { + timer = null; + if (!pendingColor && !pendingDraft) return; + // Still sending the previous frame: wait rather than queue up stale values + if (WebSocketClient.Instance.BufferedAmount > 0) { + timer = setTimeout(flush, MIN_INTERVAL_MS); + return; + } + flushNow(); +} + +// The draft first, so the color it comes with already shows at the new brightness +function flushNow() { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + const draft = pendingDraft; + const color = pendingColor; + pendingDraft = null; + pendingColor = null; + lastSent = Date.now(); + if (draft) sendDraft(draft); + if (color) sendColor(color); +} + +/** Previews a color, rate limited: calls faster than every 40 ms collapse into the latest one. */ +export function previewLedColor(preview: LedColorPreview): boolean { + if (!connected()) return false; + pendingColor = preview; + schedule(); + return true; +} + +/** Shows unsaved brightness on the LEDs, rate limited like previewLedColor. */ +export function setLedDraft(draft: LedDraft): boolean { + if (!connected()) return false; + pendingDraft = draft; + schedule(); + return true; +} + +// --- One-off requests --- + +export function previewLedPattern(pattern: LedPattern): number | null { + pendingColor = null; // A queued color would flush after the pattern and replace it + return send(LocalToHubMessagePayload.LedPreviewPatternMessage, (fbb, seq) => + LedPreviewPatternMessage.createLedPreviewPatternMessage(fbb, seq, pattern) + ); +} + +export function stopLedOverride(layer: LedOverrideLayer): number | null { + if (layer === LedOverrideLayer.Display) pendingColor = null; + else pendingDraft = null; + return send(LocalToHubMessagePayload.LedOverrideStopMessage, (fbb, seq) => + LedOverrideStopMessage.createLedOverrideStopMessage(fbb, seq, layer) + ); +} + +/** Saves the draft the hub holds; a draft still waiting on the rate limit goes out first. */ +export function commitLedDraft(): number | null { + if (!connected()) return null; + flushNow(); + return send(LocalToHubMessagePayload.LedCommitDraftMessage, (fbb, seq) => + LedCommitDraftMessage.createLedCommitDraftMessage(fbb, seq) + ); +} + +const _modes: Record = { + all: FbsLedMode.All, + critical: FbsLedMode.CriticalOnly, + off: FbsLedMode.Off, +}; + +export function setLedMode(mode: LedMode): number | null { + return send(LocalToHubMessagePayload.LedSetModeMessage, (fbb, seq) => + LedSetModeMessage.createLedSetModeMessage(fbb, seq, _modes[mode]) + ); +} diff --git a/frontend/src/lib/mappers/ConfigMapper.test.ts b/frontend/src/lib/mappers/ConfigMapper.test.ts index b921df4b6..5742b73c4 100644 --- a/frontend/src/lib/mappers/ConfigMapper.test.ts +++ b/frontend/src/lib/mappers/ConfigMapper.test.ts @@ -1,8 +1,11 @@ import { AccessPointProtection } from '#lib/_fbs/open-shock/serialization/local/access-point-protection.js'; import { AccessPointSettings } from '#lib/_fbs/open-shock/serialization/local/access-point-settings.js'; +import { LedSettings } from '#lib/_fbs/open-shock/serialization/local/led-settings.js'; import { OtaChannel } from '#lib/_fbs/open-shock/serialization/local/ota-channel.js'; import { OtaSettings } from '#lib/_fbs/open-shock/serialization/local/ota-settings.js'; import { ReadyMessage } from '#lib/_fbs/open-shock/serialization/local/ready-message.js'; +import { LedBrightness } from '#lib/_fbs/open-shock/serialization/types/led-brightness.js'; +import { LedMode } from '#lib/_fbs/open-shock/serialization/types/led-mode.js'; import { Builder, ByteBuffer } from 'flatbuffers'; import { describe, expect, it } from 'vitest'; import { mapConfig } from './ConfigMapper'; @@ -12,6 +15,16 @@ function readBack(b: Builder, root: number): ReadyMessage { return ReadyMessage.getRootAsReadyMessage(new ByteBuffer(b.asUint8Array())); } +// The LED settings of a board with only an RGB LED, the way the hub serializes them +function buildLed(b: Builder): number { + const rgb = LedBrightness.createLedBrightness(b, 40000, 5140); + LedSettings.startLedSettings(b); + LedSettings.addMode(b, LedMode.CriticalOnly); + LedSettings.addRgb(b, rgb); + LedSettings.addRgbWarnAbove(b, 19661); + return LedSettings.endLedSettings(b); +} + // Builds a Ready message the way the hub serializes it. function buildReady(cdnDomain: string, ssids: string[]): ReadyMessage { const b = new Builder(256); @@ -29,6 +42,8 @@ function buildReady(cdnDomain: string, ssids: string[]): ReadyMessage { AccessPointProtection.DefaultPassword ); + const led = buildLed(b); + ReadyMessage.startReadyMessage(b); ReadyMessage.addRfTxPin(b, 15); ReadyMessage.addEstopEnabled(b, true); @@ -36,6 +51,7 @@ function buildReady(cdnDomain: string, ssids: string[]): ReadyMessage { ReadyMessage.addSavedSsids(b, saved); ReadyMessage.addOta(b, ota); ReadyMessage.addAccessPoint(b, ap); + ReadyMessage.addLed(b, led); return readBack(b, ReadyMessage.endReadyMessage(b)); } @@ -58,6 +74,12 @@ describe('mapConfig', () => { protection: 'default', }, savedSSIDs: ['HomeNet', 'Cafe'], + led: { + mode: 'critical', + mono: { present: false, critical: 0, normal: 0 }, + rgb: { present: true, critical: 40000, normal: 5140 }, + rgbWarnAbove: 19661, + }, }); }); @@ -82,8 +104,10 @@ describe('mapConfig', () => { const b = new Builder(64); const domain = b.createString('fw.example.org'); const ota = OtaSettings.createOtaSettings(b, true, domain, OtaChannel.Stable, 30, true, false); + const led = buildLed(b); ReadyMessage.startReadyMessage(b); ReadyMessage.addOta(b, ota); + ReadyMessage.addLed(b, led); const config = mapConfig(readBack(b, ReadyMessage.endReadyMessage(b))); @@ -91,6 +115,38 @@ describe('mapConfig', () => { expect(config.estop.gpioPin).toBe(-1); }); + it('maps a mono LED table as present', () => { + const b = new Builder(64); + const domain = b.createString('fw.example.org'); + const ota = OtaSettings.createOtaSettings(b, true, domain, OtaChannel.Stable, 30, true, false); + const mono = LedBrightness.createLedBrightness(b, 30000, 2000); + LedSettings.startLedSettings(b); + LedSettings.addMode(b, LedMode.All); + LedSettings.addMono(b, mono); + const led = LedSettings.endLedSettings(b); + ReadyMessage.startReadyMessage(b); + ReadyMessage.addOta(b, ota); + ReadyMessage.addLed(b, led); + + const config = mapConfig(readBack(b, ReadyMessage.endReadyMessage(b))); + + expect(config.led?.mono).toEqual({ present: true, critical: 30000, normal: 2000 }); + expect(config.led?.rgb.present).toBe(false); + }); + + it('loads the other settings from firmware without LED settings', () => { + const b = new Builder(64); + const domain = b.createString('fw.example.org'); + const ota = OtaSettings.createOtaSettings(b, true, domain, OtaChannel.Stable, 30, true, false); + ReadyMessage.startReadyMessage(b); + ReadyMessage.addOta(b, ota); + + const config = mapConfig(readBack(b, ReadyMessage.endReadyMessage(b))); + + expect(config.led).toBeNull(); + expect(config.otaUpdate.cdnDomain).toBe('fw.example.org'); + }); + it('rejects a message without the OTA settings', () => { const b = new Builder(64); ReadyMessage.startReadyMessage(b); diff --git a/frontend/src/lib/mappers/ConfigMapper.ts b/frontend/src/lib/mappers/ConfigMapper.ts index 7a7cc80ed..cf59b0a87 100644 --- a/frontend/src/lib/mappers/ConfigMapper.ts +++ b/frontend/src/lib/mappers/ConfigMapper.ts @@ -1,6 +1,9 @@ import { AccessPointProtection } from '#lib/_fbs/open-shock/serialization/local/access-point-protection.js'; +import type { LedSettings } from '#lib/_fbs/open-shock/serialization/local/led-settings.js'; import type { OtaChannel } from '#lib/_fbs/open-shock/serialization/local/ota-channel.js'; import type { ReadyMessage } from '#lib/_fbs/open-shock/serialization/local/ready-message.js'; +import type { LedBrightness as LedBrightnessTable } from '#lib/_fbs/open-shock/serialization/types/led-brightness.js'; +import { LedMode as FbsLedMode } from '#lib/_fbs/open-shock/serialization/types/led-mode.js'; export interface OtaUpdateConfig { isEnabled: boolean; @@ -20,12 +23,28 @@ export interface AccessPointConfig { protection: AccessPointProtectionKind; } +export type LedMode = 'all' | 'critical' | 'off'; + +export interface LedBrightness { + present: boolean; // Whether the board has this LED + critical: number; // 16-bit, 0-65535 of the LED's full output + normal: number; +} + +export interface LedConfig { + mode: LedMode; + mono: LedBrightness; + rgb: LedBrightness; + rgbWarnAbove: number; // RGB brightness (0-65535) above which the hub warns +} + // The settings the portal shows, as the hub reports them in its Ready message. export interface Config { rf: { txPin: number }; estop: { enabled: boolean; gpioPin: number }; otaUpdate: OtaUpdateConfig; accessPoint: AccessPointConfig | null; // Null from firmware older than this frontend + led: LedConfig | null; // Null from firmware older than this frontend (e.g. after an interrupted update) savedSSIDs: string[]; } @@ -46,6 +65,32 @@ function mapAccessPointConfig(ready: ReadyMessage): AccessPointConfig | null { }; } +const _ledModes: Record = { + [FbsLedMode.All]: 'all', + [FbsLedMode.CriticalOnly]: 'critical', + [FbsLedMode.Off]: 'off', +}; + +// From the Ready message, or a LedSettingsChangedEvent +export function mapLedConfig(led: LedSettings): LedConfig { + // The hub only sends the LEDs the board has + const mapBrightness = (b: LedBrightnessTable | null): LedBrightness => ({ + present: b !== null, + critical: b?.critical() ?? 0, + normal: b?.normal() ?? 0, + }); + return { + mode: _ledModes[led.mode()] ?? 'all', + mono: mapBrightness(led.mono()), + rgb: mapBrightness(led.rgb()), + rgbWarnAbove: led.rgbWarnAbove(), + }; +} + +function mapOptionalLed(led: LedSettings | null): LedConfig | null { + return led ? mapLedConfig(led) : null; +} + function mapOtaUpdateConfig(ready: ReadyMessage): OtaUpdateConfig { const ota = ready.ota(); if (!ota) throw new Error('ready.ota is null'); @@ -74,6 +119,8 @@ export function mapConfig(ready: ReadyMessage): Config { estop: { enabled: ready.estopEnabled(), gpioPin: ready.estopGpioPin() }, otaUpdate: mapOtaUpdateConfig(ready), accessPoint: mapAccessPointConfig(ready), + // Optional, so the other settings still load from firmware that has no LED settings + led: mapOptionalLed(ready.led()), savedSSIDs, }; } diff --git a/frontend/src/lib/mappers/LedStatusMapper.test.ts b/frontend/src/lib/mappers/LedStatusMapper.test.ts new file mode 100644 index 000000000..72d83a0ad --- /dev/null +++ b/frontend/src/lib/mappers/LedStatusMapper.test.ts @@ -0,0 +1,81 @@ +import { LedOverrideEnd } from '#lib/_fbs/open-shock/serialization/local/led-override-end.js'; +import { LedOverrideEnded } from '#lib/_fbs/open-shock/serialization/local/led-override-ended.js'; +import { LedOverride } from '#lib/_fbs/open-shock/serialization/local/led-override.js'; +import { LedRequestError } from '#lib/_fbs/open-shock/serialization/local/led-request-error.js'; +import { LedRequestFailed } from '#lib/_fbs/open-shock/serialization/local/led-request-failed.js'; +import { LedShowing } from '#lib/_fbs/open-shock/serialization/local/led-showing.js'; +import { LedStatusEvent } from '#lib/_fbs/open-shock/serialization/local/led-status-event.js'; +import { LedStatus } from '#lib/_fbs/open-shock/serialization/local/led-status.js'; +import { LedOverrideLayer } from '#lib/_fbs/open-shock/serialization/types/led-override-layer.js'; +import { LedPattern } from '#lib/_fbs/open-shock/serialization/types/led-pattern.js'; +import { Builder, ByteBuffer } from 'flatbuffers'; +import { describe, expect, it } from 'vitest'; +import { mapLedNotice, mapLedStatus } from './LedStatusMapper'; + +// A status event the way the hub serializes it: a color preview by client 2, a draft by client 1 +function buildEvent(extra: (b: Builder) => { ended?: number; failed?: number }): LedStatusEvent { + const b = new Builder(128); + const display = LedOverride.createLedOverride(b, 2, 2000); + const draft = LedOverride.createLedOverride(b, 1, 10000); + LedStatus.startLedStatus(b); + LedStatus.addShowing(b, LedShowing.Color); + LedStatus.addPattern(b, LedPattern.Connected); + LedStatus.addDisplay(b, display); + LedStatus.addDraft(b, draft); + const status = LedStatus.endLedStatus(b); + const { ended = 0, failed = 0 } = extra(b); + LedStatusEvent.startLedStatusEvent(b); + LedStatusEvent.addStatus(b, status); + if (ended) LedStatusEvent.addEnded(b, ended); + if (failed) LedStatusEvent.addFailed(b, failed); + b.finish(LedStatusEvent.endLedStatusEvent(b)); + return LedStatusEvent.getRootAsLedStatusEvent(new ByteBuffer(b.asUint8Array())); +} + +describe('mapLedStatus', () => { + it('maps what shows and who holds each override', () => { + expect(mapLedStatus(buildEvent(() => ({})).status()!)).toEqual({ + showing: 'color', + pattern: LedPattern.Connected, + display: { owner: 2, timeoutMs: 2000 }, + draft: { owner: 1, timeoutMs: 10000 }, + }); + }); +}); + +describe('mapLedNotice', () => { + it('is null for a status change alone', () => { + expect(mapLedNotice(buildEvent(() => ({})))).toBeNull(); + }); + + it('maps an override ending', () => { + const event = buildEvent((b) => ({ + ended: LedOverrideEnded.createLedOverrideEnded( + b, + LedOverrideLayer.Draft, + LedOverrideEnd.Expired, + 1, + 42 + ), + })); + expect(mapLedNotice(event)).toEqual({ + kind: 'ended', + layer: LedOverrideLayer.Draft, + reason: LedOverrideEnd.Expired, + owner: 1, + seq: 42, + }); + }); + + it('maps a failed request', () => { + const event = buildEvent((b) => ({ + failed: LedRequestFailed.createLedRequestFailed(b, 3, 7, LedRequestError.SaveFailed), + })); + expect(mapLedNotice(event)).toEqual({ + kind: 'failed', + error: LedRequestError.SaveFailed, + owner: 3, + seq: 7, + }); + }); +}); diff --git a/frontend/src/lib/mappers/LedStatusMapper.ts b/frontend/src/lib/mappers/LedStatusMapper.ts new file mode 100644 index 000000000..148adf2cf --- /dev/null +++ b/frontend/src/lib/mappers/LedStatusMapper.ts @@ -0,0 +1,67 @@ +import type { LedOverrideEnd } from '#lib/_fbs/open-shock/serialization/local/led-override-end.js'; +import type { LedOverride } from '#lib/_fbs/open-shock/serialization/local/led-override.js'; +import type { LedRequestError } from '#lib/_fbs/open-shock/serialization/local/led-request-error.js'; +import { LedShowing as FbsLedShowing } from '#lib/_fbs/open-shock/serialization/local/led-showing.js'; +import type { LedStatusEvent } from '#lib/_fbs/open-shock/serialization/local/led-status-event.js'; +import type { LedStatus as FbsLedStatus } from '#lib/_fbs/open-shock/serialization/local/led-status.js'; +import type { LedOverrideLayer } from '#lib/_fbs/open-shock/serialization/types/led-override-layer.js'; +import type { LedPattern } from '#lib/_fbs/open-shock/serialization/types/led-pattern.js'; + +// What the hub's status LEDs show, as its LedStatus reports it + +export type LedShowing = 'live' | 'hidden' | 'pattern' | 'color' | 'test'; + +export interface LedOverrideInfo { + owner: number; // The client holding it (hubState.clientId for this one) + timeoutMs: number; // How long it lasts after the owner's last message for it +} + +export interface LedStatus { + showing: LedShowing; + pattern: LedPattern; // For 'live', 'hidden' (the hidden state) and 'pattern' + display: LedOverrideInfo | null; + draft: LedOverrideInfo | null; +} + +export type LedNotice = + | { kind: 'ended'; layer: LedOverrideLayer; reason: LedOverrideEnd; owner: number; seq: number } + | { kind: 'failed'; error: LedRequestError; owner: number; seq: number }; + +const _showing: Record = { + [FbsLedShowing.Live]: 'live', + [FbsLedShowing.Hidden]: 'hidden', + [FbsLedShowing.Pattern]: 'pattern', + [FbsLedShowing.Color]: 'color', + [FbsLedShowing.Test]: 'test', +}; + +function mapOverride(o: LedOverride | null): LedOverrideInfo | null { + return o ? { owner: o.owner(), timeoutMs: o.timeoutMs() } : null; +} + +export function mapLedStatus(status: FbsLedStatus): LedStatus { + return { + showing: _showing[status.showing()] ?? 'live', + pattern: status.pattern(), + display: mapOverride(status.display()), + draft: mapOverride(status.draft()), + }; +} + +export function mapLedNotice(event: LedStatusEvent): LedNotice | null { + const ended = event.ended(); + if (ended) { + return { + kind: 'ended', + layer: ended.layer(), + reason: ended.reason(), + owner: ended.owner(), + seq: ended.seq(), + }; + } + const failed = event.failed(); + if (failed) { + return { kind: 'failed', error: failed.error(), owner: failed.owner(), seq: failed.seq() }; + } + return null; +} diff --git a/frontend/src/lib/stores/HubStateStore.svelte.ts b/frontend/src/lib/stores/HubStateStore.svelte.ts index 3aa07c18f..ba6429a28 100644 --- a/frontend/src/lib/stores/HubStateStore.svelte.ts +++ b/frontend/src/lib/stores/HubStateStore.svelte.ts @@ -1,6 +1,7 @@ import { WifiAuthMode } from '#lib/_fbs/open-shock/serialization/types/wifi-auth-mode.js'; import type { WifiScanStatus } from '#lib/_fbs/open-shock/serialization/types/wifi-scan-status.js'; import type { Config } from '#lib/mappers/ConfigMapper.js'; +import type { LedStatus } from '#lib/mappers/LedStatusMapper.js'; import type { WiFiNetwork, WiFiNetworkGroup } from '#lib/types/index.js'; import { SvelteMap } from 'svelte/reactivity'; @@ -62,6 +63,13 @@ export class HubStateStore { accountLinked = $state(false); hasPredefinedPins = $state(false); config = $state(null); + // What the status LEDs show; null from firmware that doesn't report it + ledStatus = $state(null); + // This connection's id on the hub, which LED overrides name as their owner. + // It is the hub's socket slot, so a reconnect often gets the same id back. + clientId = $state(null); + // Counts Ready messages, so a reconnect is noticed even when it gets the same clientId. + connectionEpoch = $state(0); gpioValidInputs = $state(new Int8Array()); gpioValidOutputs = $state(new Int8Array()); diff --git a/frontend/src/lib/utils/color.test.ts b/frontend/src/lib/utils/color.test.ts new file mode 100644 index 000000000..b0482fd33 --- /dev/null +++ b/frontend/src/lib/utils/color.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest'; +import { hexToRgb } from './color'; + +describe('hexToRgb', () => { + it('maps ff to 1 and 00 to 0', () => { + expect(hexToRgb('ff0080')).toEqual({ r: 1, g: 0, b: 128 / 255 }); + }); +}); diff --git a/frontend/src/lib/utils/color.ts b/frontend/src/lib/utils/color.ts new file mode 100644 index 000000000..5b0af25fb --- /dev/null +++ b/frontend/src/lib/utils/color.ts @@ -0,0 +1,16 @@ +/** A color with each channel 0-1. */ +export interface Rgb { + r: number; + g: number; + b: number; +} + +/** An RRGGBB hex color. */ +export function hexToRgb(hex: string): Rgb { + const value = parseInt(hex, 16); + return { + r: ((value >> 16) & 0xff) / 255, + g: ((value >> 8) & 0xff) / 255, + b: (value & 0xff) / 255, + }; +} diff --git a/frontend/src/lib/views/Advanced.svelte b/frontend/src/lib/views/Advanced.svelte index 5192f723d..7fe592bb0 100644 --- a/frontend/src/lib/views/Advanced.svelte +++ b/frontend/src/lib/views/Advanced.svelte @@ -1,9 +1,9 @@ @@ -35,8 +36,9 @@ import WiFiStep from '#lib/components/steps/WiFiStep.svelte'; import OtaSection from '#lib/components/sections/OtaSection.svelte'; import AccessPointSection from '#lib/components/sections/AccessPointSection.svelte'; + import LedSection from '#lib/components/sections/LedSection.svelte'; import SectionHeader from '#lib/components/SectionHeader.svelte'; - import { ViewModeStore } from '#lib/stores/index.js'; + import { hubState, ViewModeStore } from '#lib/stores/index.js'; import { Button } from '@openshock/svelte-core/components/ui/button'; import { ChevronRight, ArrowLeft } from '@lucide/svelte'; @@ -46,8 +48,21 @@ let { activeSection = $bindable('menu') }: Props = $props(); + // No LEDs section on a board without a status LED, or on firmware without LED settings. Shown until the hub's config + // arrives, which says. + let hasLeds = $derived( + !hubState.config || + (hubState.config.led !== null && + (hubState.config.led.mono.present || hubState.config.led.rgb.present)) + ); + let sections = $derived(advancedSections.filter((s) => s.id !== 'leds' || hasLeds)); + // On a phone 'menu' hides the content panel, so this only shows from md up. - let shownSection = $derived(activeSection === 'menu' ? advancedSections[0].id : activeSection); + let shownSection = $derived( + activeSection === 'menu' || !sections.some((s) => s.id === activeSection) + ? sections[0].id + : activeSection + );