diff --git a/chips/ESP32-S3/N8/merge-image.py b/chips/ESP32-S3/N8/merge-image.py new file mode 100644 index 00000000..190b5fb0 --- /dev/null +++ b/chips/ESP32-S3/N8/merge-image.py @@ -0,0 +1,16 @@ +#!/bin/python3 + +import esptool + +# fmt: off +# Note: Bootloader for esp32-s3 starts at 0x0000, unlike several other ESP32 variants that start at 0x1000. +esptool.main([ + '--chip', 'esp32s3', + 'merge_bin', '-o', 'merged.bin', + '--flash_size', '8MB', + '0x0', './bootloader.bin', + '0x8000', './partitions.bin', + '0x10000', './firmware.bin', + '0x310000', './filesystem.bin' +]) +# fmt: on diff --git a/chips/ESP32-S3/N8/partitions.csv b/chips/ESP32-S3/N8/partitions.csv new file mode 100644 index 00000000..61254fca --- /dev/null +++ b/chips/ESP32-S3/N8/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x300000, +spiffs, data, spiffs, 0x310000,0xE0000, +coredump, data, coredump,0x3F0000,0x10000, \ No newline at end of file diff --git a/include/RGBPatternManager.h b/include/RGBPatternManager.h new file mode 100644 index 00000000..7f98c11e --- /dev/null +++ b/include/RGBPatternManager.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +#include + +namespace OpenShock { + class RGBPatternManager { + public: + RGBPatternManager(std::uint8_t gpioPin); + ~RGBPatternManager(); + + struct RGBState { + std::uint8_t red; + std::uint8_t green; + std::uint8_t blue; + std::uint32_t duration; + }; + + void SetPattern(nonstd::span pattern); + void SetBrightness(std::uint8_t brightness); + void ClearPattern(); + + private: + void ClearPatternInternal(); + void SendRGB(const RGBState state); + static void RunPattern(void* arg); + + std::uint8_t m_rgbPin; + std::uint8_t m_rgbBrightness; // 0-255 + rmt_obj_t* m_rmtHandle; + RGBState* m_pattern; + std::size_t m_patternLength; + TaskHandle_t m_taskHandle; + SemaphoreHandle_t m_taskSemaphore; + }; +} // namespace OpenShock diff --git a/include/VisualStateManager.h b/include/VisualStateManager.h index 9148dff6..c6174fdd 100644 --- a/include/VisualStateManager.h +++ b/include/VisualStateManager.h @@ -1,5 +1,6 @@ #pragma once +#include "EStopManager.h" #include namespace OpenShock::VisualStateManager { @@ -7,6 +8,6 @@ namespace OpenShock::VisualStateManager { void SetCriticalError(); void SetScanningStarted(); - void SetEmergencyStop(bool isStopped); + void SetEmergencyStop(OpenShock::EStopManager::EStopStatus status); void SetWebSocketConnected(bool isConnected); } // namespace OpenShock::VisualStateManager diff --git a/platformio.ini b/platformio.ini index 270ec875..d53697e0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,7 +21,7 @@ lib_deps = https://github.com/martinmoene/span-lite https://github.com/OpenShock/ESPAsyncWebServer https://github.com/Links2004/arduinoWebSockets -board_build.partitions = huge_app.csv ; Overridden per board +board_build.partitions = huge_app.csv ; Overridden per board by custom_openshock selections board_build.filesystem = littlefs board_build.embed_files = certificates/x509_crt_bundle extra_scripts = @@ -87,5 +87,17 @@ custom_openshock.chip_variant = N8R8 build_flags = -DOPENSHOCK_LED_GPIO=21 +; https://github.com/nullstalgia/OpenShock-Hardware/tree/main/Core +; 8MB Flash, assume no PSRAM. +[env:OpenShock-Core-V1] +board = esp32-s3-devkitc-1 ; builtin +custom_openshock.chip = ESP32-S3 +custom_openshock.chip_variant = N8 +build_flags = + -DOPENSHOCK_LED_WS2812B=48 + -DOPENSHOCK_LED_GPIO=35 + -DOPENSHOCK_TX_PIN=15 + -DOPENSHOCK_ESTOP_PIN=13 + ; TODO: ; https://docs.platformio.org/en/latest/boards/espressif32/upesy_wroom.html;upesy-esp32-wroom-devkit diff --git a/src/EStopManager.cpp b/src/EStopManager.cpp index 39da390e..49d85252 100644 --- a/src/EStopManager.cpp +++ b/src/EStopManager.cpp @@ -1,7 +1,7 @@ #include "EStopManager.h" -#include "Time.h" #include "Logging.h" +#include "Time.h" #include "VisualStateManager.h" #include @@ -10,11 +10,11 @@ const char* const TAG = "EStopManager"; using namespace OpenShock; -static EStopManager::EStopStatus s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; -static std::uint32_t s_estopHoldToClearTime = 5000; -static std::int64_t s_lastEStopButtonStateChange = 0; -static std::int64_t s_estoppedAt = 0; -static bool s_lastEStopButtonState = HIGH; +static EStopManager::EStopStatus s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; +static std::uint32_t s_estopHoldToClearTime = 5000; +static std::int64_t s_lastEStopButtonStateChange = 0; +static std::int64_t s_estoppedAt = 0; +static bool s_lastEStopButtonState = HIGH; static std::uint8_t s_estopPin; @@ -61,13 +61,14 @@ EStopManager::EStopStatus EStopManager::Update() { s_estopStatus = EStopManager::EStopStatus::ESTOPPED_AND_HELD; s_estoppedAt = s_lastEStopButtonStateChange; ESP_LOGI(TAG, "Emergency Stopped!!!"); - OpenShock::VisualStateManager::SetEmergencyStop(true); + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); } break; case EStopManager::EStopStatus::ESTOPPED_AND_HELD: if (buttonState == HIGH) { // User has released the button, now we can trust them holding to clear it. s_estopStatus = EStopManager::EStopStatus::ESTOPPED; + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); } break; case EStopManager::EStopStatus::ESTOPPED: @@ -75,7 +76,7 @@ EStopManager::EStopStatus EStopManager::Update() { if (buttonState == LOW && s_lastEStopButtonState == LOW && s_lastEStopButtonStateChange + s_estopHoldToClearTime <= OpenShock::millis()) { s_estopStatus = EStopManager::EStopStatus::ESTOPPED_CLEARED; ESP_LOGI(TAG, "Clearing EStop on button release!"); - OpenShock::VisualStateManager::SetEmergencyStop(false); + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); } break; case EStopManager::EStopStatus::ESTOPPED_CLEARED: @@ -83,6 +84,7 @@ EStopManager::EStopStatus EStopManager::Update() { if (buttonState == HIGH) { s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; ESP_LOGI(TAG, "All clear!"); + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); } break; diff --git a/src/RGBPatternManager.cpp b/src/RGBPatternManager.cpp new file mode 100644 index 00000000..6440b391 --- /dev/null +++ b/src/RGBPatternManager.cpp @@ -0,0 +1,134 @@ +#include "RGBPatternManager.h" + +#include "Logging.h" +#include "util/TaskUtils.h" + +#include + +const char* const TAG = "RGBPatternManager"; + +using namespace OpenShock; + +// Currently this assumes a single WS2812B LED +// TODO: Support multiple LEDs ? +// TODO: Support other LED types ? + +RGBPatternManager::RGBPatternManager(std::uint8_t rgbPin) : m_rgbPin(rgbPin), m_rgbBrightness(255), m_pattern(nullptr), m_patternLength(0), m_taskHandle(nullptr), m_taskSemaphore(xSemaphoreCreateBinary()) { + if ((m_rmtHandle = rmtInit(m_rgbPin, RMT_TX_MODE, RMT_MEM_64)) == NULL) { + ESP_LOGE(TAG, "[pin-%u] Failed to create rgb rmt object", m_rgbPin); + } + + float realTick = rmtSetTick(m_rmtHandle, 100); + ESP_LOGD(TAG, "[pin-%u] real tick set to: %fns", m_rgbPin, realTick); + + SetBrightness(20); + + xSemaphoreGive(m_taskSemaphore); +} + +RGBPatternManager::~RGBPatternManager() { + ClearPattern(); + + vSemaphoreDelete(m_taskSemaphore); +} + +void RGBPatternManager::SetPattern(nonstd::span pattern) { + ClearPatternInternal(); + + // Set new values + m_patternLength = pattern.size(); + m_pattern = new RGBState[m_patternLength]; + std::copy(pattern.begin(), pattern.end(), m_pattern); + + // Start the task + BaseType_t result = TaskUtils::TaskCreateExpensive(RunPattern, TAG, 4096, this, 1, &m_taskHandle); + if (result != pdPASS) { + ESP_LOGE(TAG, "[pin-%u] Failed to create task: %d", m_rgbPin, result); + + m_taskHandle = nullptr; + + if (m_pattern != nullptr) { + delete[] m_pattern; + m_pattern = nullptr; + } + m_patternLength = 0; + } + + // Give the semaphore back + xSemaphoreGive(m_taskSemaphore); +} + +void RGBPatternManager::ClearPattern() { + ClearPatternInternal(); + xSemaphoreGive(m_taskSemaphore); +} + +void RGBPatternManager::ClearPatternInternal() { + xSemaphoreTake(m_taskSemaphore, portMAX_DELAY); + + if (m_taskHandle != nullptr) { + vTaskDelete(m_taskHandle); + m_taskHandle = nullptr; + } + + if (m_pattern != nullptr) { + delete[] m_pattern; + m_pattern = nullptr; + } + m_patternLength = 0; +} + +// Range: 0-255 +void RGBPatternManager::SetBrightness(std::uint8_t brightness) { + m_rgbBrightness = brightness; +} + +void RGBPatternManager::SendRGB(const RGBState state) { + const std::uint16_t bitCount = (8 * 3) * (1); // 8 bits per color * 3 colors * 1 LED + + rmt_data_t led_data[bitCount]; + + std::uint8_t r = (std::uint8_t)((std::uint16_t)state.red * m_rgbBrightness / 255); + std::uint8_t g = (std::uint8_t)((std::uint16_t)state.green * m_rgbBrightness / 255); + std::uint8_t b = (std::uint8_t)((std::uint16_t)state.blue * m_rgbBrightness / 255); + + std::uint8_t led, col, bit; + std::uint8_t i = 0; + // WS2812B takes commands in GRB order + // https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf - Page 5 + std::uint8_t colors[3] = {g, r, b}; + for (led = 0; led < 1; led++) { + for (col = 0; col < 3; col++) { + for (bit = 0; bit < 8; bit++) { + if ((colors[col] & (1 << (7 - bit)))) { + led_data[i].level0 = 1; + led_data[i].duration0 = 8; + led_data[i].level1 = 0; + led_data[i].duration1 = 4; + } else { + led_data[i].level0 = 1; + led_data[i].duration0 = 4; + led_data[i].level1 = 0; + led_data[i].duration1 = 8; + } + i++; + } + } + } + // Send the data + rmtWriteBlocking(m_rmtHandle, led_data, bitCount); +} + +void RGBPatternManager::RunPattern(void* arg) { + RGBPatternManager* thisPtr = reinterpret_cast(arg); + + RGBPatternManager::RGBState* pattern = thisPtr->m_pattern; + std::size_t patternLength = thisPtr->m_patternLength; + + while (true) { + for (std::size_t i = 0; i < patternLength; ++i) { + thisPtr->SendRGB(pattern[i]); + vTaskDelay(pdMS_TO_TICKS(pattern[i].duration)); + } + } +} diff --git a/src/VisualStateManager.cpp b/src/VisualStateManager.cpp index 6f859c67..1038b28a 100644 --- a/src/VisualStateManager.cpp +++ b/src/VisualStateManager.cpp @@ -1,19 +1,16 @@ #include "VisualStateManager.h" #ifdef OPENSHOCK_LED_GPIO -#ifdef OPENSHOCK_LED_IMPLEMENTATION -#error "Only one LED type can be defined at a time" -#else -#define OPENSHOCK_LED_IMPLEMENTATION #include "PinPatternManager.h" #include +#ifndef OPENSHOCK_LED_IMPLEMENTATION +#define OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_GPIO #ifdef OPENSHOCK_LED_WS2812B -#ifdef OPENSHOCK_LED_IMPLEMENTATION -#error "Only one LED type can be defined at a time" -#else +#include "RGBPatternManager.h" +#ifndef OPENSHOCK_LED_IMPLEMENTATION #define OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_WS2812B @@ -30,14 +27,19 @@ const char* const TAG = "VisualStateManager"; -constexpr std::uint64_t kCriticalErrorFlag = 1 << 0; -constexpr std::uint64_t kEmergencyStoppedFlag = 1 << 1; -constexpr std::uint64_t kWebSocketConnectedFlag = 1 << 2; -constexpr std::uint64_t kWiFiConnectedWithoutWSFlag = 1 << 3; -constexpr std::uint64_t kWiFiScanningFlag = 1 << 4; +constexpr std::uint64_t kCriticalErrorFlag = 1 << 0; +constexpr std::uint64_t kEmergencyStoppedFlag = 1 << 1; +constexpr std::uint64_t kEmergencyStopClearedFlag = 1 << 2; +constexpr std::uint64_t kWebSocketConnectedFlag = 1 << 3; +constexpr std::uint64_t kWiFiConnectedFlag = 1 << 4; +constexpr std::uint64_t kWiFiScanningFlag = 1 << 5; static std::uint64_t s_stateFlags = 0; +// Bitmask of when the system is in a "all clear" state. + +constexpr std::uint64_t kStatusOKMask = kWebSocketConnectedFlag | kWiFiConnectedFlag; + using namespace OpenShock; #ifdef OPENSHOCK_LED_GPIO @@ -52,7 +54,14 @@ constexpr PinPatternManager::State kEmergencyStoppedPattern[] = { {false, 500} }; +constexpr PinPatternManager::State kEmergencyStopClearedPattern[] = { + { true, 150}, + {false, 150} +}; + constexpr PinPatternManager::State kWiFiDisconnectedPattern[] = { + { true, 100}, + {false, 100}, { true, 100}, {false, 100}, { true, 100}, @@ -60,8 +69,6 @@ constexpr PinPatternManager::State kWiFiDisconnectedPattern[] = { }; constexpr PinPatternManager::State kWiFiConnectedWithoutWSPattern[] = { - { true, 100}, - {false, 100}, { true, 100}, {false, 100}, { true, 100}, @@ -97,9 +104,21 @@ constexpr PinPatternManager::State kWebSocketConnectedPattern[] = { {false, 10'000} }; +constexpr PinPatternManager::State kSolidOnPattern[] = { + {true, 100'000} +}; + +constexpr PinPatternManager::State kSolidOffPattern[] = { + {false, 100'000} +}; + PinPatternManager s_builtInLedManager(OPENSHOCK_LED_GPIO); -void _updateVisualState() { +void _updateVisualStateGPIO(nonstd::span override) { + s_builtInLedManager.SetPattern(override); +} + +void _updateVisualStateGPIO() { if (s_stateFlags & kCriticalErrorFlag) { s_builtInLedManager.SetPattern(kCriticalErrorPattern); return; @@ -110,12 +129,17 @@ void _updateVisualState() { return; } + if (s_stateFlags & kEmergencyStopClearedFlag) { + s_builtInLedManager.SetPattern(kEmergencyStopClearedPattern); + return; + } + if (s_stateFlags & kWebSocketConnectedFlag) { s_builtInLedManager.SetPattern(kWebSocketConnectedPattern); return; } - if (s_stateFlags & kWiFiConnectedWithoutWSFlag) { + if (s_stateFlags & kWiFiConnectedFlag) { s_builtInLedManager.SetPattern(kWiFiConnectedWithoutWSPattern); return; } @@ -132,24 +156,130 @@ void _updateVisualState() { #ifdef OPENSHOCK_LED_WS2812B -void _updateVisualState() { - ESP_LOGE(TAG, "_updateVisualState: (but WS2812B is not implemented yet)"); +constexpr RGBPatternManager::RGBState kCriticalErrorRGBPattern[] = { + {255, 0, 0, 100}, // Red ON for 0.1 seconds + { 0, 0, 0, 100} // OFF for 0.1 seconds +}; + +constexpr RGBPatternManager::RGBState kEmergencyStoppedRGBPattern[] = { + {255, 0, 0, 500}, + { 0, 0, 0, 500} +}; + +constexpr RGBPatternManager::RGBState kEmergencyStopClearedRGBPattern[] = { + {0, 255, 0, 150}, + {0, 0, 0, 150} +}; + +constexpr RGBPatternManager::RGBState kWiFiDisconnectedRGBPattern[] = { + {0, 0, 255, 100}, + {0, 0, 0, 100}, + {0, 0, 255, 100}, + {0, 0, 0, 100}, + {0, 0, 255, 100}, + {0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kWiFiConnectedWithoutWSRGBPattern[] = { + {255, 165, 0, 100}, + { 0, 0, 0, 100}, + {255, 165, 0, 100}, + { 0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kPingNoResponseRGBPattern[] = { + {0, 50, 255, 100}, + {0, 0, 0, 100}, + {0, 50, 255, 100}, + {0, 0, 0, 100}, + {0, 50, 255, 100}, + {0, 0, 0, 100}, + {0, 50, 255, 100}, + {0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kWebSocketCantConnectRGBPattern[] = { + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kWebSocketConnectedRGBPattern[] = { + {0, 255, 0, 100}, + {0, 0, 0, 10'000}, +}; + +RGBPatternManager s_RGBLedManager(OPENSHOCK_LED_WS2812B); + +void _updateVisualStateRGB() { + if (s_stateFlags & kCriticalErrorFlag) { + s_RGBLedManager.SetPattern(kCriticalErrorRGBPattern); + return; + } + + if (s_stateFlags & kEmergencyStoppedFlag) { + s_RGBLedManager.SetPattern(kEmergencyStoppedRGBPattern); + return; + } + + if (s_stateFlags & kEmergencyStopClearedFlag) { + s_RGBLedManager.SetPattern(kEmergencyStopClearedRGBPattern); + return; + } + + if (s_stateFlags & kWebSocketConnectedFlag) { + s_RGBLedManager.SetPattern(kWebSocketConnectedRGBPattern); + return; + } + + if (s_stateFlags & kWiFiConnectedFlag) { + s_RGBLedManager.SetPattern(kWiFiConnectedWithoutWSRGBPattern); + return; + } + + if (s_stateFlags & kWiFiScanningFlag) { + s_RGBLedManager.SetPattern(kPingNoResponseRGBPattern); + return; + } + + s_RGBLedManager.SetPattern(kWiFiDisconnectedRGBPattern); } #endif // OPENSHOCK_LED_WS2812B -#ifndef OPENSHOCK_LED_IMPLEMENTATION - void _updateVisualState() { +#ifdef OPENSHOCK_LED_IMPLEMENTATION +#if defined(OPENSHOCK_LED_GPIO) && defined(OPENSHOCK_LED_WS2812B) + if (s_stateFlags == kStatusOKMask) { + _updateVisualStateGPIO(kSolidOnPattern); + } else { + _updateVisualStateGPIO(kSolidOffPattern); + } + _updateVisualStateRGB(); +#elif defined(OPENSHOCK_LED_GPIO) + _updateVisualStateGPIO(); +#elif defined(OPENSHOCK_LED_WS2812B) + _updateVisualStateRGB(); +#else +#error "No LED implementation selected but OPENSHOCK_LED_IMPLEMENTATION is defined" +#endif +#else ESP_LOGE(TAG, "_updateVisualState: (but no LED implementation is selected)"); + vTaskDelay(10); +#endif } -#endif // OPENSHOCK_LED_NONE - void _handleWiFiConnected(arduino_event_t* event) { std::uint64_t oldState = s_stateFlags; - s_stateFlags |= kWiFiConnectedWithoutWSFlag; + s_stateFlags |= kWiFiConnectedFlag; if (oldState != s_stateFlags) { _updateVisualState(); @@ -158,7 +288,7 @@ void _handleWiFiConnected(arduino_event_t* event) { void _handleWiFiDisconnected(arduino_event_t* event) { std::uint64_t oldState = s_stateFlags; - s_stateFlags &= ~kWiFiConnectedWithoutWSFlag; + s_stateFlags &= ~kWiFiConnectedFlag; if (oldState != s_stateFlags) { _updateVisualState(); @@ -204,13 +334,30 @@ void VisualStateManager::SetScanningStarted() { } } -void VisualStateManager::SetEmergencyStop(bool isStopped) { +void VisualStateManager::SetEmergencyStop(OpenShock::EStopManager::EStopStatus status) { std::uint64_t oldState = s_stateFlags; - if (isStopped) { - s_stateFlags |= kEmergencyStoppedFlag; - } else { - s_stateFlags &= ~kEmergencyStoppedFlag; + switch (status) { + // When there is no EStop currently active. + case OpenShock::EStopManager::EStopStatus::ALL_CLEAR: + s_stateFlags &= ~kEmergencyStoppedFlag; + s_stateFlags &= ~kEmergencyStopClearedFlag; + break; + // EStop has been triggered! + case OpenShock::EStopManager::EStopStatus::ESTOPPED_AND_HELD: + // EStop still active, and user has released the button from the initial trigger. + case OpenShock::EStopManager::EStopStatus::ESTOPPED: + s_stateFlags |= kEmergencyStoppedFlag; + s_stateFlags &= ~kEmergencyStopClearedFlag; + break; + // User has held and cleared the EStop, now we're waiting for them to release the button. + case OpenShock::EStopManager::EStopStatus::ESTOPPED_CLEARED: + s_stateFlags &= ~kEmergencyStoppedFlag; + s_stateFlags |= kEmergencyStopClearedFlag; + break; + default: + ESP_LOGE(TAG, "Unhandled EStop status: %d", status); + break; } if (oldState != s_stateFlags) {