diff --git a/src/MoonBase/Char.h b/src/MoonBase/Char.h index 6a6829ec..84cc949d 100644 --- a/src/MoonBase/Char.h +++ b/src/MoonBase/Char.h @@ -11,8 +11,6 @@ #pragma once -#include - #include "ArduinoJson.h" // See https://discord.com/channels/473448917040758787/718943978636050542/1357670679196991629 diff --git a/src/MoonBase/Utilities.h b/src/MoonBase/Utilities.h index 9113e2e4..4df978c4 100644 --- a/src/MoonBase/Utilities.h +++ b/src/MoonBase/Utilities.h @@ -11,7 +11,6 @@ #pragma once -#include #include #include "ArduinoJson.h" diff --git a/src/MoonBase/pal.h b/src/MoonBase/pal.h new file mode 100644 index 00000000..ecb782ce --- /dev/null +++ b/src/MoonBase/pal.h @@ -0,0 +1,104 @@ +/** + @title MoonBase + @file pal.h + @repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs + @Authors https://github.com/MoonModules/MoonLight/commits/main + @Doc https://moonmodules.org/MoonLight/moonlight/overview/ + @Copyright © 2025 Github MoonLight Commit Authors + @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + @license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information. +**/ + +/** + Platform Abstraction Layer (PAL) + + Goal: + * not depending on Arduino.h + * use esp-idf functions (instead of arduino.h) + * in a way to not create a new dependency on esp-idf + + Approach + * Incremental changes: you can still do it the old way but the new way is available as well + + Benefits + * Common way of working, within one repository, Possibly between different repo's + * No mix of arduino and esp-idf code in different places + * platform Arduino can be removed in the future + * Automated testing e.g. on Linux + **/ + +#pragma once + +#include +#include + +namespace pal { + +/* ================================================= + * Basic types + * ================================================= */ + +struct rgb_t { + uint8_t r; + uint8_t g; + uint8_t b; +}; + +/* ================================================= + * Time + * ================================================= */ + +uint32_t millis(); +uint64_t micros(); +void delay_ms(uint32_t ms); + +/* ================================================= + * LED output (hot path) + * ================================================= */ + +void led_submit(const rgb_t* buffer, size_t led_count); +void led_set_brightness(uint8_t brightness); + +/* ================================================= + * Memory + * ================================================= */ + +void* malloc(size_t size); +void free(void* ptr); + +/* ================================================= + * Logging + * ================================================= */ + +enum class LogLevel : uint8_t { Error, Warn, Info, Debug }; + +void log(LogLevel level, const char* tag, const char* message); + +/* ================================================= + * Capability hints (0 = no, 1 = yes) + * ================================================= */ + +int cap_led_dma(); +int cap_led_parallel(); + +/* ================================================= + * UDP socket (stateful) + * ================================================= */ + +class UdpSocket { + public: + virtual ~UdpSocket() = default; + + virtual bool open(uint16_t local_port) = 0; + virtual int send_to(const char* ip, uint16_t port, const uint8_t* data, size_t len) = 0; + virtual int recv_from(uint8_t* buffer, size_t max_len, + char* src_ip, // out + uint16_t* src_port // out + ) = 0; + virtual void close() = 0; +}; + +UdpSocket* udp_socket_create(); +void udp_socket_destroy(UdpSocket* socket); + +} // namespace pal diff --git a/src/MoonBase/pal_espidf.cpp b/src/MoonBase/pal_espidf.cpp new file mode 100644 index 00000000..147418d0 --- /dev/null +++ b/src/MoonBase/pal_espidf.cpp @@ -0,0 +1,175 @@ +/** + @title MoonBase + @file pal_espidf.cpp + @repo https://github.com/MoonModules/MoonLight, submit changes to this file as PRs + @Authors https://github.com/MoonModules/MoonLight/commits/main + @Doc https://moonmodules.org/MoonLight/moonlight/overview/ + @Copyright © 2025 Github MoonLight Commit Authors + @license GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + @license For non GPL-v3 usage, commercial licenses must be purchased. Contact us for more information. +**/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pal.h" + +namespace pal { + +/* ================================================= + * Time + * ================================================= */ + +uint32_t millis() { return static_cast(esp_timer_get_time() / 1000ULL); } + +uint64_t micros() { return static_cast(esp_timer_get_time()); } + +void delay_ms(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } + +/* ================================================= + * LED output (ESP-IDF backend placeholder) + * ================================================= */ + +// below functions is poc code. Need to be verified when actually used + +static uint8_t s_led_brightness = 255; // brightness control will be implemented when the actual LED driver is added. + +void led_submit(const rgb_t* buffer, size_t led_count) { + /* To be implemented with: + - RMT + - PARLIO + - DMA / PPA + - or external driver + */ + (void)buffer; + (void)led_count; +} + +void led_set_brightness(uint8_t brightness) { s_led_brightness = brightness; } + +/* ================================================= + * Memory + * ================================================= */ + +void* malloc(size_t size) { return heap_caps_malloc(size, MALLOC_CAP_8BIT); } + +void free(void* ptr) { heap_caps_free(ptr); } + +/* ================================================= + * Logging + * ================================================= */ + +void log(LogLevel level, const char* tag, const char* message) { + esp_log_level_t esp_level = ESP_LOG_INFO; + + switch (level) { + case LogLevel::Error: + esp_level = ESP_LOG_ERROR; + break; + case LogLevel::Warn: + esp_level = ESP_LOG_WARN; + break; + case LogLevel::Info: + esp_level = ESP_LOG_INFO; + break; + case LogLevel::Debug: + esp_level = ESP_LOG_DEBUG; + break; + } + + esp_log_write(esp_level, tag, "%s\n", message); +} + +/* ================================================= + * Capabilities + * ================================================= */ + +int cap_led_dma() { return 1; /* ESP32 supports DMA */ } + +int cap_led_parallel() { return 1; /* ESP32 generally supports parallel output */ } + +/* ================================================= + * UDP socket (ESP-IDF / lwIP) + * ================================================= */ + +class EspIdfUdpSocket : public UdpSocket { + public: + EspIdfUdpSocket() : sock_(-1) {} + + ~EspIdfUdpSocket() override { close(); } + + // below functions is poc code. Need to be verified when actually used + + bool open(uint16_t local_port) override { + sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sock_ < 0) { + return false; + } + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(local_port); + addr.sin_addr.s_addr = INADDR_ANY; + + if (bind(sock_, reinterpret_cast(&addr), sizeof(addr)) != 0) { + ::close(sock_); + sock_ = -1; + return false; + } + return true; + } + + int send_to(const char* ip, uint16_t port, const uint8_t* data, size_t len) override { + sockaddr_in dest{}; + dest.sin_family = AF_INET; + dest.sin_port = htons(port); + if (inet_aton(ip, &dest.sin_addr) == 0) { + return -1; // Invalid IP address + } + + return sendto(sock_, data, len, 0, reinterpret_cast(&dest), sizeof(dest)); + } + + int recv_from(uint8_t* buffer, size_t max_len, + char* src_ip, // out: must be at least 16 bytes (INET_ADDRSTRLEN) + uint16_t* src_port // out + ) { + sockaddr_in src_addr{}; + socklen_t src_len = sizeof(src_addr); + + int received = recvfrom(sock_, buffer, max_len, 0, reinterpret_cast(&src_addr), &src_len); + + if (received >= 0 && src_ip && src_port) { + inet_ntoa_r(src_addr.sin_addr, src_ip, 16); + *src_port = ntohs(src_addr.sin_port); + } + + return received; + } + + void close() override { + if (sock_ >= 0) { + ::close(sock_); + sock_ = -1; + } + } + + private: + int sock_; +}; + +/* ================================================= + * Factory + * ================================================= */ + +UdpSocket* udp_socket_create() { return new EspIdfUdpSocket(); } + +void udp_socket_destroy(UdpSocket* socket) { delete socket; } + +} // namespace pal diff --git a/src/MoonLight/Layers/PhysicalLayer.h b/src/MoonLight/Layers/PhysicalLayer.h index c8b9f6af..5a684e76 100644 --- a/src/MoonLight/Layers/PhysicalLayer.h +++ b/src/MoonLight/Layers/PhysicalLayer.h @@ -13,8 +13,6 @@ #if FT_MOONLIGHT - #include - #include #include "FastLED.h" diff --git a/src/MoonLight/Layers/VirtualLayer.h b/src/MoonLight/Layers/VirtualLayer.h index 9670594d..b3fab307 100644 --- a/src/MoonLight/Layers/VirtualLayer.h +++ b/src/MoonLight/Layers/VirtualLayer.h @@ -13,7 +13,6 @@ #if FT_MOONLIGHT - #include #include #include diff --git a/src/MoonLight/Nodes/Drivers/parlio.cpp b/src/MoonLight/Nodes/Drivers/parlio.cpp index f2bb834f..b76bdf75 100644 --- a/src/MoonLight/Nodes/Drivers/parlio.cpp +++ b/src/MoonLight/Nodes/Drivers/parlio.cpp @@ -11,6 +11,8 @@ #include "parlio.h" //so it is compiled before Parallel LED Driver use it +#include "soc/soc_caps.h" // for SOC_PARLIO_SUPPORTED + #ifdef SOC_PARLIO_SUPPORTED #include "driver/parlio_tx.h" diff --git a/src/MoonLight/Nodes/Drivers/parlio.h b/src/MoonLight/Nodes/Drivers/parlio.h index a719edfb..2a6934c1 100644 --- a/src/MoonLight/Nodes/Drivers/parlio.h +++ b/src/MoonLight/Nodes/Drivers/parlio.h @@ -10,7 +10,7 @@ **/ #pragma once -#include +#include // uint8... #if FT_MOONLIGHT diff --git a/src/MoonLight/Nodes/Effects/E_MoonLight.h b/src/MoonLight/Nodes/Effects/E_MoonLight.h index 611484c2..5fe4c768 100644 --- a/src/MoonLight/Nodes/Effects/E_MoonLight.h +++ b/src/MoonLight/Nodes/Effects/E_MoonLight.h @@ -11,6 +11,8 @@ #if FT_MOONLIGHT + #include "MoonBase/pal.h" + class SolidEffect : public Node { public: static const char* name() { return "Solid"; } @@ -130,7 +132,7 @@ class LinesEffect : public Node { pos = {0, 0, 0}; pos.y = ::map(beat16(bpm), 0, UINT16_MAX, 0, layer->size.y); // instead of call%height for (pos.x = 0; pos.x < layer->size.x; pos.x++) { - int colorNr = 1;//(frameNr / layer->size.x) % 3; + int colorNr = 1; //(frameNr / layer->size.x) % 3; layer->setRGB(pos, colorNr == 0 ? CRGB::Red : colorNr == 1 ? CRGB::Green : CRGB::Blue); } (frameNr)++; @@ -167,7 +169,7 @@ class RipplesEffect : public Node { void loop() override { float ripple_interval = 1.3f * ((255.0f - interval) / 128.0f) * sqrtf(layer->size.y); - float time_interval = millis() / (100.0 - speed) / ((256.0f - 128.0f) / 20.0f); + float time_interval = pal::millis() / (100.0 - speed) / ((256.0f - 128.0f) / 20.0f); layer->fadeToBlackBy(255); @@ -177,7 +179,7 @@ class RipplesEffect : public Node { float d = distance(layer->size.x / 2.0f, layer->size.z / 2.0f, 0.0f, (float)pos.x, (float)pos.z, 0.0f) / 9.899495f * layer->size.y; pos.y = floor(layer->size.y / 2.0f * (1 + sinf(d / ripple_interval + time_interval))); // between 0 and layer->size.y - layer->setRGB(pos, (CRGB)CHSV(millis() / 50 + random8(64), 200, 255)); + layer->setRGB(pos, (CRGB)CHSV(pal::millis() / 50 + random8(64), 200, 255)); } } } @@ -233,9 +235,9 @@ class ScrollingTextEffect : public Node { choice = preset; else { if (strlen(textIn) == 0) - choice = (millis() / 1000 % 8) + 2; + choice = (pal::millis() / 1000 % 8) + 2; else - choice = (millis() / 1000 % 9) + 1; + choice = (pal::millis() / 1000 % 9) + 1; } IPAddress activeIP = WiFi.isConnected() ? WiFi.localIP() : ETH.localIP(); @@ -256,21 +258,24 @@ class ScrollingTextEffect : public Node { #define MILLIS_PER_MINUTE (60 * 1000) #define MILLIS_PER_HOUR (MILLIS_PER_MINUTE * 60) #define MILLIS_PER_DAY (MILLIS_PER_HOUR * 24) - if (millis() < MILLIS_PER_MINUTE) // within 1 minute - text.format("%ds", millis() / 1000); // seconds - else if (millis() < MILLIS_PER_MINUTE * 10) // within 10 min - text.format("%dm%d", millis() / MILLIS_PER_MINUTE, (millis() / 1000) % 60); // minutes and seconds - else if (millis() < MILLIS_PER_HOUR) // within 1 hour - text.format("%dm", millis() / MILLIS_PER_MINUTE); // minutes - else if (millis() < MILLIS_PER_HOUR * 10) // within 10 hours - text.format("%dh%d", millis() / MILLIS_PER_HOUR, (millis() / MILLIS_PER_MINUTE) % 60); // hours and minutes - else if (millis() < MILLIS_PER_DAY) // within 1 day - text.format("%dh", millis() / MILLIS_PER_HOUR); // hours - else if (millis() < MILLIS_PER_DAY * 10) // within 10 days - text.format("%dd%d", millis() / (MILLIS_PER_DAY), (millis() / MILLIS_PER_HOUR) % 24); // days and hours - else // more than 10 days - text.format("%dh", millis() / (MILLIS_PER_DAY)); // days + { + uint32_t uptime = pal::millis(); + if (uptime < MILLIS_PER_MINUTE) // within one minute + text.format("%ds", uptime / 1000); + else if (uptime < MILLIS_PER_MINUTE * 10) // within 10 min + text.format("%dm%d", uptime / MILLIS_PER_MINUTE, (uptime / 1000) % 60); + else if (uptime < MILLIS_PER_HOUR) // within 1 hour + text.format("%dm", uptime / MILLIS_PER_MINUTE); + else if (uptime < MILLIS_PER_HOUR * 10) // within 10 hours + text.format("%dh%d", uptime / MILLIS_PER_HOUR, (uptime / MILLIS_PER_MINUTE) % 60); + else if (uptime < MILLIS_PER_DAY) // within 1 day + text.format("%dh", uptime / MILLIS_PER_HOUR); + else if (uptime < MILLIS_PER_DAY * 10) // within 10 days + text.format("%dd%d", uptime / MILLIS_PER_DAY, (uptime / MILLIS_PER_HOUR) % 24); + else // more than 10 days + text.format("%dd", uptime / MILLIS_PER_DAY); break; + } case 6: text.format("%s", sharedData.connectionStatus == 0 ? "Off" : sharedData.connectionStatus == 1 ? "AP-" : sharedData.connectionStatus == 2 ? "AP+" : sharedData.connectionStatus == 3 ? "Sta-" : sharedData.connectionStatus == 4 ? "Sta+" : "mqqt"); break; @@ -290,8 +295,8 @@ class ScrollingTextEffect : public Node { // Serial.printf(" %d:%s", choice-1, text.c_str()); // if (text && strnlen(text.c_str(), 2) > 0) { - layer->drawText(text.c_str(), 0, 1, font, CRGB::Red, -(millis() / 25 * speed / 256)); // instead of call - // } + layer->drawText(text.c_str(), 0, 1, font, CRGB::Red, -(pal::millis() / 25 * speed / 256)); // instead of call + // } #if USE_M5UNIFIEDDisplay M5.Display.fillRect(0, 0, 100, 15, BLACK); @@ -316,7 +321,7 @@ class SinusEffect : public Node { void loop() override { layer->fadeToBlackBy(70); - uint8_t hueOffset = millis() / 10; + uint8_t hueOffset = pal::millis() / 10; static uint16_t phase = 0; // Tracks the phase of the sine wave uint8_t brightness = 255; @@ -347,7 +352,7 @@ class SphereMoveEffect : public Node { void loop() override { layer->fadeToBlackBy(255); - float time_interval = millis() / (100 - speed) / ((256.0f - 128.0f) / 20.0f); + float time_interval = pal::millis() / (100 - speed) / ((256.0f - 128.0f) / 20.0f); Coord3D origin; origin.x = layer->size.x / 2.0 * (1.0 + sinf(time_interval)); @@ -363,7 +368,7 @@ class SphereMoveEffect : public Node { float d = distance(pos.x, pos.y, pos.z, origin.x, origin.y, origin.z); if (d > diameter && d < diameter + 1.0) { - layer->setRGB(pos, (CRGB)CHSV(millis() / 50 + random8(64), 200, 255)); + layer->setRGB(pos, (CRGB)CHSV(pal::millis() / 50 + random8(64), 200, 255)); } } } @@ -409,7 +414,7 @@ class StarFieldEffect : public Node { // Inspired by Daniel Shiffman's Coding T Star stars[255]; void loop() override { - if (!speed || millis() - step < 1000 / speed) return; // Not enough time passed + if (!speed || pal::millis() - step < 1000 / speed) return; // Not enough time passed layer->fadeToBlackBy(blur); @@ -441,7 +446,7 @@ class StarFieldEffect : public Node { // Inspired by Daniel Shiffman's Coding T } } - step = millis(); + step = pal::millis(); } }; // StarFieldEffect @@ -480,7 +485,7 @@ class PraxisEffect : public Node { // uint16_t micro_mutator = beatsin8(microMutatorFreq, microMutatorMin, microMutatorMax); // beatsin16(2, 550, 900); Coord3D pos = {0, 0, 0}; - uint8_t huebase = millis() / 40; // 1 + ~huespeed + uint8_t huebase = pal::millis() / 40; // 1 + ~huespeed for (pos.x = 0; pos.x < layer->size.x; pos.x++) { for (pos.y = 0; pos.y < layer->size.y; pos.y++) { @@ -519,7 +524,7 @@ class WaveEffect : public Node { void loop() override { layer->fadeToBlackBy(fade); // should only fade rgb ... - CRGB color = CHSV(millis() / 50, 255, 255); + CRGB color = CHSV(pal::millis() / 50, 255, 255); int prevPos = layer->size.x / 2; // somewhere in the middle @@ -546,7 +551,7 @@ class WaveEffect : public Node { pos = (bs8 + beatsin8(bpm * 0.65, 0, 255, y * 200) + beatsin8(bpm * 1.43, 0, 255, y * 300)) * layer->size.x / 256 / 3; break; case 5: - pos = inoise8(millis() * bpm / 256 + y * 1000) * layer->size.x / 256; + pos = inoise8(pal::millis() * bpm / 256 + y * 1000) * layer->size.x / 256; break; // bpm not really bpm, more speed default: pos = 0; @@ -594,7 +599,7 @@ class FreqSawsEffect : public Node { memset(lastBpm, 0, sizeof(lastBpm)); memset(phaseOffset, 0, sizeof(phaseOffset)); - lastTime = millis(); + lastTime = pal::millis(); } uint16_t bandSpeed[NUM_GEQ_CHANNELS]; @@ -606,7 +611,7 @@ class FreqSawsEffect : public Node { void loop() override { layer->fadeToBlackBy(fade); // Update timing for frame-rate independent phase - unsigned long currentTime = millis(); + unsigned long currentTime = pal::millis(); uint32_t deltaMs = currentTime - lastTime; lastTime = currentTime; @@ -937,13 +942,14 @@ class RubiksCubeEffect : public Node { RotateFunc rotateFuncs[6] = {&Cube::rotateFront, &Cube::rotateBack, &Cube::rotateLeft, &Cube::rotateRight, &Cube::rotateTop, &Cube::rotateBottom}; void loop() override { - if (doInit && millis() > step || step - 3100 > millis()) { // step - 3100 > millis() temp fix for default on boot - step = millis() + 1000; + uint32_t now = pal::millis(); + if ((doInit && now > step) || (step - 3100 > now)) { // step - 3100 > now: temp fix for default on boot + step = now + 1000; doInit = false; init(); } - if (!turnsPerSecond || millis() - step < 1000 / turnsPerSecond || millis() < step) return; + if (!turnsPerSecond || now - step < 1000 / turnsPerSecond || now < step) return; Move move = randomTurning ? createRandomMoveStruct(cubeSize, prevFaceMoved) : unpackMove(moveList[moveIndex]); @@ -952,12 +958,12 @@ class RubiksCubeEffect : public Node { cube.drawCube(layer); if (!randomTurning && moveIndex == 0) { - step = millis() + 3000; + step = now + 3000; doInit = true; return; } if (!randomTurning) moveIndex--; - step = millis(); + step = now; } }; @@ -1138,7 +1144,7 @@ class ParticlesEffect : public Node { layer->setRGB(initPos, particles[index].color); } EXT_LOGD(ML_TAG, "Particles Set Up\n"); - step = millis(); + step = pal::millis(); } Particle particles[255]; @@ -1147,7 +1153,7 @@ class ParticlesEffect : public Node { float gravity[3]; void loop() override { - if (!speed || millis() - step < 1000 / speed) return; // Not enough time passed + if (!speed || pal::millis() - step < 1000 / speed) return; // Not enough time passed float gravityX, gravityY, gravityZ; // Gravity if using gyro or random gravity @@ -1165,8 +1171,8 @@ class ParticlesEffect : public Node { #endif if (randomGravity) { - if (millis() - gravUpdate > gravityChangeInterval * 1000) { - gravUpdate = millis(); + if (pal::millis() - gravUpdate > gravityChangeInterval * 1000) { + gravUpdate = pal::millis(); float scale = 5.0f; // Generate Perlin noise values and scale them gravity[0] = (inoise8(step, 0, 0) / 128.0f - 1.0f) * scale; @@ -1192,7 +1198,7 @@ class ParticlesEffect : public Node { particles[index].updatePositionandDraw(layer, index, debugPrint); } - step = millis(); + step = pal::millis(); } }; @@ -1283,7 +1289,7 @@ class SpiralFireEffect : public Node { layer->fadeToBlackBy(40); Coord3D pos; - uint16_t time = millis() >> 4; + uint16_t time = pal::millis() >> 4; // Calculate center of the cone float centerX = layer->size.x / 2.0f; @@ -1506,7 +1512,7 @@ class PixelMapEffect : public Node { void loop() override { layer->fill_solid(CRGB::Black); - layer->setRGB(pos, CHSV(millis() / 50 + random8(64), 255, 255)); // ColorFromPalette(layerP.palette,call, bri); + layer->setRGB(pos, CHSV(pal::millis() / 50 + random8(64), 255, 255)); // ColorFromPalette(layerP.palette,call, bri); } }; // PixelMap