#include "wled.h" #ifdef WLED_ENABLE_INFINITY_CONTROLLER #ifdef ARDUINO_ARCH_ESP32 #include "esp_timer.h" #endif namespace { constexpr uint32_t INFINITY_MAGIC = 0x59534649UL; // "IFSY" little-endian constexpr uint8_t INFINITY_VERSION = 1; constexpr uint16_t INFINITY_DEFAULT_PORT = 21325; constexpr uint8_t INFINITY_NODE_COUNT = 6; constexpr uint8_t INFINITY_PANEL_COUNT = 3; constexpr uint16_t INFINITY_LEDS_PER_PANEL = 106; constexpr uint16_t INFINITY_LEDS_PER_NODE = INFINITY_PANEL_COUNT * INFINITY_LEDS_PER_PANEL; constexpr uint32_t INFINITY_CLOCK_INTERVAL_MS = 250; constexpr uint32_t INFINITY_SCENE_INTERVAL_MS = 100; constexpr uint32_t INFINITY_STATUS_INTERVAL_MS = 1000; constexpr uint32_t INFINITY_NODE_TIMEOUT_MS = 3000; constexpr uint32_t INFINITY_APPLY_DELAY_US = 120000; constexpr uint16_t INFINITY_DMX_FOOTPRINT = 32; constexpr uint8_t INFINITY_SPATIAL_OFF = 0; constexpr uint8_t INFINITY_SPATIAL_CENTER_PULSE = 1; constexpr uint8_t INFINITY_SPATIAL_CHECKER = 2; constexpr uint8_t INFINITY_SPATIAL_ARROW = 3; constexpr uint8_t INFINITY_SPATIAL_SCAN = 4; constexpr uint8_t INFINITY_SPATIAL_SNAKE = 5; constexpr uint8_t INFINITY_SPATIAL_WAVE_LINE = 6; constexpr uint8_t INFINITY_SPATIAL_BLEND_REPLACE = 0; constexpr uint8_t INFINITY_SPATIAL_BLEND_ADD = 1; constexpr uint8_t INFINITY_SPATIAL_BLEND_MULTIPLY_MASK = 2; constexpr uint8_t INFINITY_SPATIAL_BLEND_PALETTE_TINT = 3; constexpr uint8_t INFINITY_SPATIAL_VARIANT_EXPAND = 0; constexpr uint8_t INFINITY_SPATIAL_VARIANT_REVERSE = 1; constexpr uint8_t INFINITY_SPATIAL_VARIANT_OUTLINE = 2; constexpr uint8_t INFINITY_SPATIAL_VARIANT_OUTLINE_REVERSE = 3; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_LTR = 0; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_RTL = 1; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_TTB = 2; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_BTT = 3; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_OUTWARD = 4; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_INWARD = 5; constexpr uint8_t INFINITY_SPATIAL_DIRECTION_PINGPONG = 6; constexpr uint8_t INFINITY_SPATIAL_OPTION_LINE = 0; constexpr uint8_t INFINITY_SPATIAL_OPTION_BANDS = 1; constexpr uint8_t INFINITY_BPM_MIN = 20; constexpr uint8_t INFINITY_BPM_MAX = 240; enum InfinityPacketType : uint8_t { INFINITY_PACKET_CLOCK_SYNC = 1, INFINITY_PACKET_SCENE_STATE = 2, INFINITY_PACKET_BEAT_TRIGGER = 3, INFINITY_PACKET_NODE_STATUS = 4, }; struct __attribute__((packed)) InfinityPacketHeader { uint32_t magic; uint8_t version; uint8_t type; uint16_t payloadSize; uint32_t sequence; uint64_t masterTimeUs; }; struct __attribute__((packed)) InfinityScenePayload { uint8_t effectId; uint8_t presetId; uint8_t brightness; uint8_t speed; uint8_t intensity; uint8_t palette; uint8_t primary[3]; uint8_t secondary[3]; uint8_t tertiary[3]; uint8_t groupMask; uint8_t direction; uint8_t flags; uint16_t transitionMs; uint32_t seed; uint16_t phase; uint64_t applyAtUs; uint8_t rowDimmer[3]; uint8_t strobe; uint8_t safetyFade; uint8_t custom[3]; uint8_t spatialMode; uint8_t spatialStrength; uint8_t spatialBlend; uint8_t spatialVariant; uint8_t spatialDirection; uint8_t spatialSize; uint16_t spatialAngle; uint8_t spatialOption; }; constexpr uint16_t INFINITY_SCENE_PAYLOAD_LEGACY_SIZE = __builtin_offsetof(InfinityScenePayload, spatialDirection); struct __attribute__((packed)) InfinityStatusPayload { char nodeId[12]; uint32_t uptimeMs; int32_t masterOffsetUs; uint32_t lastSceneSequence; uint32_t packetsReceived; uint8_t flags; uint8_t activeEffect; uint8_t activePreset; uint8_t reserved; }; struct InfinityNodeStatus { IPAddress ip; char nodeId[12]; uint32_t lastSeenMs; uint32_t uptimeMs; int32_t masterOffsetUs; uint32_t lastSceneSequence; uint32_t packetsReceived; uint8_t flags; uint8_t activeEffect; uint8_t activePreset; }; WiFiUDP infinityUdp; bool infinityUdpStarted = false; bool infinityEnabled = true; uint16_t infinityPort = INFINITY_DEFAULT_PORT; IPAddress infinityMasterIp(192, 168, 178, 10); IPAddress infinityNodeIps[INFINITY_NODE_COUNT] = { IPAddress(192, 168, 178, 11), IPAddress(192, 168, 178, 12), IPAddress(192, 168, 178, 13), IPAddress(192, 168, 178, 14), IPAddress(192, 168, 178, 15), IPAddress(192, 168, 178, 16), }; char infinityConfiguredNodeId[12] = "auto"; char infinityRuntimeNodeId[12] = "node-xx"; InfinityScenePayload infinityScene = { 0, // effectId 0, // presetId 128, // brightness 128, // speed 128, // intensity 0, // palette {255, 160, 80}, {0, 32, 255}, {0, 0, 0}, 0x07, 0, 1, 750, 1, 0, 0, {255, 255, 255}, 0, 0, {0, 0, 0}, INFINITY_SPATIAL_OFF, 180, INFINITY_SPATIAL_BLEND_MULTIPLY_MASK, INFINITY_SPATIAL_VARIANT_EXPAND, INFINITY_SPATIAL_DIRECTION_LTR, 64, 0, INFINITY_SPATIAL_OPTION_LINE, }; InfinityScenePayload pendingScene; bool hasPendingScene = false; uint32_t infinitySequence = 0; uint32_t lastClockSentMs = 0; uint32_t lastSceneSentMs = 0; uint32_t lastStatusSentMs = 0; uint32_t lastDmxBeatValue = 0; uint32_t lastDmxSyncResetValue = 0; uint32_t packetsReceived = 0; uint32_t packetsSent = 0; uint32_t lastSceneSequence = 0; int64_t masterOffsetUs = 0; uint8_t lastAppliedPreset = 0; uint8_t lastAppliedEffect = 0; InfinityNodeStatus nodeStatuses[INFINITY_NODE_COUNT] = {}; bool isMasterBuild() { #ifdef WLED_INFINITY_MASTER return true; #else return false; #endif } bool isNodeBuild() { #ifdef WLED_INFINITY_NODE return true; #else return false; #endif } uint64_t localTimeUs() { #ifdef ARDUINO_ARCH_ESP32 return static_cast(esp_timer_get_time()); #else return static_cast(micros()); #endif } uint64_t masterTimeUs() { if (isMasterBuild()) return localTimeUs(); return static_cast(static_cast(localTimeUs()) + masterOffsetUs); } uint8_t clampMode(uint8_t mode) { const uint8_t count = strip.getModeCount(); if (count == 0) return 0; return mode < count ? mode : mode % count; } String ipToString(const IPAddress& ip) { return String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]); } void copyNodeId(char* dest, size_t len, const char* src) { if (!dest || len == 0) return; if (!src || src[0] == 0) src = "auto"; strlcpy(dest, src, len); } void refreshRuntimeNodeId() { if (strcmp(infinityConfiguredNodeId, "auto") != 0 && infinityConfiguredNodeId[0] != 0) { copyNodeId(infinityRuntimeNodeId, sizeof(infinityRuntimeNodeId), infinityConfiguredNodeId); return; } if (isMasterBuild()) { copyNodeId(infinityRuntimeNodeId, sizeof(infinityRuntimeNodeId), "master"); return; } IPAddress ip = Network.localIP(); if (ip[0] == 192 && ip[1] == 168 && ip[2] == 178 && ip[3] >= 11 && ip[3] <= 16) { snprintf(infinityRuntimeNodeId, sizeof(infinityRuntimeNodeId), "node-%02u", unsigned(ip[3] - 10)); } else { copyNodeId(infinityRuntimeNodeId, sizeof(infinityRuntimeNodeId), "node-xx"); } } uint16_t dmxTransitionMs(uint8_t value) { return static_cast(value) * 40U; } void hsvToRgb(uint8_t hue, uint8_t sat, uint8_t value, uint8_t rgb[3]) { byte temp[3] = {0, 0, 0}; colorHStoRGB(static_cast(hue) * 257U, sat, temp); rgb[0] = (static_cast(temp[0]) * value) / 255U; rgb[1] = (static_cast(temp[1]) * value) / 255U; rgb[2] = (static_cast(temp[2]) * value) / 255U; } uint32_t rgbToColor(const uint8_t rgb[3]) { return RGBW32(rgb[0], rgb[1], rgb[2], 0); } uint8_t scaleByte(uint8_t value, uint8_t scale) { return (static_cast(value) * scale) / 255U; } uint32_t scaleColor(uint32_t color, uint8_t scale) { return RGBW32(scaleByte(R(color), scale), scaleByte(G(color), scale), scaleByte(B(color), scale), scaleByte(W(color), scale)); } uint32_t addColorCapped(uint32_t base, uint32_t add) { return RGBW32(qadd8(R(base), R(add)), qadd8(G(base), G(add)), qadd8(B(base), B(add)), qadd8(W(base), W(add))); } uint32_t applySpatialBlend(uint32_t base, uint32_t layer, uint8_t amount, uint8_t strength, uint8_t blendMode) { const uint8_t mask = scaleByte(amount, strength); const uint32_t scaledLayer = scaleColor(layer, mask); switch (blendMode) { case INFINITY_SPATIAL_BLEND_REPLACE: return color_blend(base, layer, mask); case INFINITY_SPATIAL_BLEND_ADD: return addColorCapped(base, scaledLayer); case INFINITY_SPATIAL_BLEND_PALETTE_TINT: return color_blend(scaleColor(base, mask), layer, mask); case INFINITY_SPATIAL_BLEND_MULTIPLY_MASK: default: return scaleColor(base, mask); } } bool parseNodeColumn(uint8_t& column) { if (strncmp(infinityRuntimeNodeId, "node-", 5) == 0) { const uint8_t n = atoi(infinityRuntimeNodeId + 5); if (n >= 1 && n <= INFINITY_NODE_COUNT) { column = n - 1; return true; } } IPAddress ip = Network.localIP(); if (ip[0] == 192 && ip[1] == 168 && ip[2] == 178 && ip[3] >= 11 && ip[3] <= 16) { column = ip[3] - 11; return true; } column = 0; return false; } float smoothstepf(float edge0, float edge1, float x) { if (edge0 == edge1) return x < edge0 ? 0.0f : 1.0f; x = (x - edge0) / (edge1 - edge0); if (x < 0.0f) x = 0.0f; if (x > 1.0f) x = 1.0f; return x * x * (3.0f - 2.0f * x); } uint8_t floatToAmount(float value) { if (value <= 0.0f) return 0; if (value >= 1.0f) return 255; return static_cast(value * 255.0f + 0.5f); } uint8_t speedToBpm(uint8_t speed) { constexpr uint16_t range = INFINITY_BPM_MAX - INFINITY_BPM_MIN; return INFINITY_BPM_MIN + ((static_cast(speed) * range + 127U) / 255U); } float spatialPhase(uint64_t timeUs, uint8_t speed) { const float seconds = static_cast(timeUs % 60000000ULL) / 1000000.0f; const float cyclesPerSecond = static_cast(speedToBpm(speed)) / 60.0f; return seconds * cyclesPerSecond; } void panelLedPosition(uint16_t led, float& x, float& y, uint8_t& side) { if (led < 25) { side = 0; // top, left -> right x = (led + 0.5f) / 25.0f; y = 0.0f; return; } if (led < 52) { side = 1; // right, top -> bottom x = 1.0f; y = (led - 25 + 0.5f) / 27.0f; return; } if (led < 79) { side = 2; // bottom, right -> left x = 1.0f - ((led - 52 + 0.5f) / 27.0f); y = 1.0f; return; } side = 3; // left, bottom -> top x = 0.0f; y = 1.0f - ((led - 79 + 0.5f) / 27.0f); } uint8_t centerPulseAmount(uint8_t column, uint8_t row, uint64_t timeUs, uint8_t speed, uint8_t variant) { constexpr float centerRow = 1.0f; constexpr float centerCol = 2.5f; constexpr float maxDistance = 3.5f; const float distance = fabsf(static_cast(row) - centerRow) + fabsf(static_cast(column) - centerCol); const float span = maxDistance + 1.0f; float front = fmodf(spatialPhase(timeUs, speed) * span, span); if (variant == INFINITY_SPATIAL_VARIANT_REVERSE || variant == INFINITY_SPATIAL_VARIANT_OUTLINE_REVERSE) front = maxDistance - front; const float delta = fabsf(distance - front); return floatToAmount(1.0f - smoothstepf(0.0f, 0.70f, delta)); } uint8_t centerPulseOutlineAmount(uint8_t column, uint8_t row, uint16_t led, uint8_t panelAmount, uint8_t variant) { float x = 0.0f, y = 0.0f; uint8_t side = 0; panelLedPosition(led, x, y, side); if (variant == INFINITY_SPATIAL_VARIANT_OUTLINE || variant == INFINITY_SPATIAL_VARIANT_OUTLINE_REVERSE) { if (row == 1 && (column == 2 || column == 3)) { return (side == 0 || side == 2) ? panelAmount : 0; } if (column == 0) return side == 3 ? panelAmount : 0; if (column == 5) return side == 1 ? panelAmount : 0; if (row == 0) return side == 0 ? panelAmount : 0; if (row == 2) return side == 2 ? panelAmount : 0; return panelAmount; } return panelAmount; } uint8_t checkerdAmount(uint8_t column, uint8_t row, uint16_t led, uint64_t timeUs, uint8_t variant, uint8_t speed) { const uint8_t parity = (row + column) & 0x01; const uint16_t step = static_cast(floorf(spatialPhase(timeUs, speed))); if (variant == 1 || variant == 2) { float x = 0.0f, y = 0.0f; uint8_t side = 0; panelLedPosition(led, x, y, side); const bool slash = variant == 2 && (step & 0x01); const bool first = slash ? (y <= 1.0f - x) : (y <= x); return ((parity == 0) == first) ? 255 : 0; } return (((parity + step) & 0x01) == 0) ? 255 : 0; } uint8_t waveLineAmount(uint8_t column, uint8_t row, uint64_t timeUs, uint8_t direction, uint8_t speed) { static const uint8_t triangle[4] = {0, 1, 2, 1}; const int32_t step = static_cast(floorf(spatialPhase(timeUs, speed))); if (direction == INFINITY_SPATIAL_DIRECTION_TTB || direction == INFINITY_SPATIAL_DIRECTION_BTT) { const int32_t phase = direction == INFINITY_SPATIAL_DIRECTION_TTB ? step : -step; const uint8_t target = static_cast(roundf(triangle[((static_cast(row) - phase) % 4 + 4) % 4] * ((INFINITY_NODE_COUNT - 1) / 2.0f) / 2.0f)); return column == min(target, INFINITY_NODE_COUNT - 1) ? 255 : 0; } const int32_t phase = direction == INFINITY_SPATIAL_DIRECTION_RTL ? -step : step; const uint8_t target = static_cast(roundf(triangle[((static_cast(column) - phase) % 4 + 4) % 4] * ((INFINITY_PANEL_COUNT - 1) / 2.0f))); return row == min(target, INFINITY_PANEL_COUNT - 1) ? 255 : 0; } uint8_t arrowAmount(uint8_t column, uint8_t row, uint64_t timeUs, uint8_t direction, uint8_t speed, uint8_t size) { const bool horizontal = !(direction == INFINITY_SPATIAL_DIRECTION_TTB || direction == INFINITY_SPATIAL_DIRECTION_BTT); const uint8_t majorCount = horizontal ? INFINITY_NODE_COUNT : INFINITY_PANEL_COUNT; const uint8_t minorCount = horizontal ? INFINITY_PANEL_COUNT : INFINITY_NODE_COUNT; const uint8_t major = horizontal ? column : row; const uint8_t minor = horizontal ? row : column; const uint8_t gap = max(1, 1 + (size / 86)) - 1; const uint8_t span = 3 + gap; const int32_t movement = static_cast(floorf(spatialPhase(timeUs, speed))); const float middleMinor = (static_cast(minorCount) - 1.0f) / 2.0f; const uint8_t band = fabsf(static_cast(minor) - middleMinor) <= 0.55f ? 0 : 1; const bool orientationRight = direction == INFINITY_SPATIAL_DIRECTION_LTR || direction == INFINITY_SPATIAL_DIRECTION_TTB || direction == INFINITY_SPATIAL_DIRECTION_OUTWARD; const uint8_t target = orientationRight ? (band == 0 ? 1 : 0) : (band == 0 ? 1 : 2); const int32_t local = orientationRight ? (static_cast(major) - movement) : (static_cast(major) + movement); return majorCount > 0 && ((local % span + span) % span) == target ? 255 : 0; } void scanVector(uint16_t angle, float& vx, float& vy) { const float radians = static_cast(angle) * 0.01745329252f; vx = cosf(radians); vy = sinf(radians); } uint8_t scanAmount(uint8_t column, uint8_t row, uint16_t led, uint64_t timeUs, uint8_t speed, uint8_t size, uint16_t angle, uint8_t option, uint8_t direction) { float x = 0.0f, y = 0.0f; uint8_t side = 0; panelLedPosition(led, x, y, side); float vx = 0.0f, vy = 0.0f; const bool vertical = direction == INFINITY_SPATIAL_DIRECTION_TTB || direction == INFINITY_SPATIAL_DIRECTION_BTT; scanVector((angle + (vertical ? 90 : 0)) % 360, vx, vy); const float progress = (static_cast(column) + x) * vx + (static_cast(row) + y) * vy; const float minProgress = -3.0f; const float maxProgress = 8.0f; const float width = 0.15f + (static_cast(size) / 255.0f) * (option == INFINITY_SPATIAL_OPTION_BANDS ? 1.60f : 0.85f); const float travel = (maxProgress - minProgress) + width; float phase = fmodf(spatialPhase(timeUs, speed) * travel, travel); if (direction == INFINITY_SPATIAL_DIRECTION_PINGPONG) { phase = fmodf(spatialPhase(timeUs, speed) * travel, travel * 2.0f); if (phase > travel) phase = (travel * 2.0f) - phase; } else if (direction == INFINITY_SPATIAL_DIRECTION_RTL || direction == INFINITY_SPATIAL_DIRECTION_BTT) { phase = travel - phase; } const float center = minProgress + phase; if (option == INFINITY_SPATIAL_OPTION_BANDS) { const float period = width * 2.0f + 0.35f; const float d = fabsf(fmodf(progress - center + period * 64.0f, period) - period * 0.5f); return floatToAmount(1.0f - smoothstepf(width * 0.45f, width * 0.75f, d)); } return floatToAmount(1.0f - smoothstepf(width * 0.5f, width * 0.5f + 0.55f, fabsf(progress - center))); } uint8_t snakeAmount(uint8_t column, uint8_t row, uint64_t timeUs, uint8_t speed, uint8_t size) { const uint8_t pathLen = INFINITY_NODE_COUNT * INFINITY_PANEL_COUNT; const uint8_t panelIndex = (row & 0x01) ? (row * INFINITY_NODE_COUNT + (INFINITY_NODE_COUNT - 1 - column)) : (row * INFINITY_NODE_COUNT + column); const uint32_t step = static_cast(floorf(spatialPhase(timeUs, speed))); const uint8_t head = (step + (infinityScene.seed % pathLen)) % pathLen; const uint8_t length = 3 + max(1, size / 64); for (uint8_t i = 0; i < length; i++) { if (panelIndex == (head + pathLen - (i % pathLen)) % pathLen) return 255 - min(200, i * 38); } const uint8_t apple = (static_cast(infinityScene.seed * 17U + (step / pathLen) * 11U + 7U) % pathLen); return panelIndex == apple ? 180 : 0; } uint8_t spatialAmount(uint8_t column, uint8_t row, uint16_t led, uint64_t timeUs) { switch (infinityScene.spatialMode) { case INFINITY_SPATIAL_CENTER_PULSE: return centerPulseOutlineAmount(column, row, led, centerPulseAmount(column, row, timeUs, infinityScene.speed, infinityScene.spatialVariant), infinityScene.spatialVariant); case INFINITY_SPATIAL_CHECKER: return checkerdAmount(column, row, led, timeUs, infinityScene.spatialVariant, infinityScene.speed); case INFINITY_SPATIAL_ARROW: return arrowAmount(column, row, timeUs, infinityScene.spatialDirection, infinityScene.speed, infinityScene.spatialSize); case INFINITY_SPATIAL_SCAN: return scanAmount(column, row, led, timeUs, infinityScene.speed, infinityScene.spatialSize, infinityScene.spatialAngle, infinityScene.spatialOption, infinityScene.spatialDirection); case INFINITY_SPATIAL_SNAKE: return snakeAmount(column, row, timeUs, infinityScene.speed, infinityScene.spatialSize); case INFINITY_SPATIAL_WAVE_LINE: return waveLineAmount(column, row, timeUs, infinityScene.spatialDirection, infinityScene.speed); default: return 0; } } uint32_t spatialLayerColor(Segment& seg, uint16_t led, uint8_t amount) { if (infinityScene.palette > 0) { seg.setCurrentPalette(); const uint16_t idx = (static_cast(led) * 255U) / (INFINITY_LEDS_PER_PANEL - 1U); return seg.color_from_palette(idx + ((masterTimeUs() / 20000ULL) & 0xFF), false, true, 0, amount); } return color_blend(rgbToColor(infinityScene.secondary), rgbToColor(infinityScene.primary), amount); } void setSegmentName(Segment& seg, const char* name) { if (seg.name && strcmp(seg.name, name) == 0) return; if (seg.name) { delete[] seg.name; seg.name = nullptr; } const size_t len = strlen(name); seg.name = new(std::nothrow) char[len + 1]; if (seg.name) strlcpy(seg.name, name, len + 1); } void markSceneChanged(uint32_t delayUs = INFINITY_APPLY_DELAY_US) { infinityScene.applyAtUs = masterTimeUs() + delayUs; infinitySequence++; lastSceneSentMs = 0; } void updateStripTimebase(uint16_t phase) { const int64_t remoteMs = static_cast(masterTimeUs() / 1000ULL) + phase; strip.timebase = static_cast(remoteMs - static_cast(millis())); } void applySceneToStrip(const InfinityScenePayload& scene) { updateStripTimebase(scene.phase); const bool outputEnabled = (scene.flags & 0x01) != 0; bri = outputEnabled ? scene.brightness : 0; strip.setBrightness(scaledBri(bri), true); transitionDelayTemp = scene.transitionMs; if (scene.presetId > 0 && scene.presetId != lastAppliedPreset) { applyPreset(scene.presetId, CALL_MODE_NO_NOTIFY); lastAppliedPreset = scene.presetId; } const uint8_t effect = clampMode(scene.effectId); const uint32_t colors[3] = { rgbToColor(scene.primary), rgbToColor(scene.secondary), rgbToColor(scene.tertiary), }; const uint8_t segmentCount = strip.getSegmentsNum(); for (uint8_t i = 0; i < segmentCount; i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive()) continue; const uint8_t rowBit = (i < 3) ? (1U << i) : 0x07; const bool rowEnabled = (scene.groupMask == 0) || ((scene.groupMask & rowBit) != 0); const uint8_t rowDimmer = (i < 3) ? scene.rowDimmer[i] : 255; seg.setOpacity(rowEnabled ? rowDimmer : 0); const bool effectChanged = seg.mode != effect; seg.setMode(effect, effectChanged, false); seg.speed = scene.speed; seg.intensity = scene.intensity; seg.setPalette(scene.palette); seg.setColor(0, colors[0]); seg.setColor(1, colors[1]); seg.setColor(2, colors[2]); seg.setOption(SEG_OPTION_REVERSED, (scene.direction & 0x01) != 0); seg.custom1 = scene.custom[0]; seg.custom2 = scene.custom[1]; seg.custom3 = scene.custom[2]; } lastAppliedEffect = effect; stateUpdated(CALL_MODE_NO_NOTIFY); } void maybeApplyPendingScene() { if (!hasPendingScene) return; if (pendingScene.applyAtUs != 0 && masterTimeUs() + 2000ULL < pendingScene.applyAtUs) return; infinityScene = pendingScene; hasPendingScene = false; applySceneToStrip(infinityScene); } bool sendPacket(const IPAddress& target, InfinityPacketType type, const void* payload, uint16_t payloadSize) { if (!infinityUdpStarted || !infinityEnabled) return false; InfinityPacketHeader header; header.magic = INFINITY_MAGIC; header.version = INFINITY_VERSION; header.type = static_cast(type); header.payloadSize = payloadSize; header.sequence = ++infinitySequence; header.masterTimeUs = masterTimeUs(); if (infinityUdp.beginPacket(target, infinityPort) == 0) return false; infinityUdp.write(reinterpret_cast(&header), sizeof(header)); if (payload != nullptr && payloadSize > 0) { infinityUdp.write(reinterpret_cast(payload), payloadSize); } const bool sent = infinityUdp.endPacket() != 0; if (sent) packetsSent++; return sent; } void sendToAllNodes(InfinityPacketType type, const void* payload, uint16_t payloadSize) { for (uint8_t i = 0; i < INFINITY_NODE_COUNT; i++) { sendPacket(infinityNodeIps[i], type, payload, payloadSize); } } void sendClockSync() { sendToAllNodes(INFINITY_PACKET_CLOCK_SYNC, nullptr, 0); } void sendSceneState() { sendToAllNodes(INFINITY_PACKET_SCENE_STATE, &infinityScene, sizeof(infinityScene)); } void sendBeatTrigger() { sendToAllNodes(INFINITY_PACKET_BEAT_TRIGGER, &infinityScene, sizeof(infinityScene)); } void sendNodeStatus() { InfinityStatusPayload status = {}; refreshRuntimeNodeId(); strlcpy(status.nodeId, infinityRuntimeNodeId, sizeof(status.nodeId)); status.uptimeMs = millis(); status.masterOffsetUs = static_cast(masterOffsetUs); status.lastSceneSequence = lastSceneSequence; status.packetsReceived = packetsReceived; status.flags = infinityEnabled ? 0x01 : 0x00; status.activeEffect = lastAppliedEffect; status.activePreset = lastAppliedPreset; sendPacket(infinityMasterIp, INFINITY_PACKET_NODE_STATUS, &status, sizeof(status)); } void storeNodeStatus(const IPAddress& remote, const InfinityStatusPayload& payload) { uint8_t slot = INFINITY_NODE_COUNT; for (uint8_t i = 0; i < INFINITY_NODE_COUNT; i++) { if (nodeStatuses[i].ip == remote || strncmp(nodeStatuses[i].nodeId, payload.nodeId, sizeof(nodeStatuses[i].nodeId)) == 0) { slot = i; break; } } if (slot == INFINITY_NODE_COUNT) { for (uint8_t i = 0; i < INFINITY_NODE_COUNT; i++) { if (nodeStatuses[i].lastSeenMs == 0) { slot = i; break; } } } if (slot == INFINITY_NODE_COUNT) return; nodeStatuses[slot].ip = remote; strlcpy(nodeStatuses[slot].nodeId, payload.nodeId, sizeof(nodeStatuses[slot].nodeId)); nodeStatuses[slot].lastSeenMs = millis(); nodeStatuses[slot].uptimeMs = payload.uptimeMs; nodeStatuses[slot].masterOffsetUs = payload.masterOffsetUs; nodeStatuses[slot].lastSceneSequence = payload.lastSceneSequence; nodeStatuses[slot].packetsReceived = payload.packetsReceived; nodeStatuses[slot].flags = payload.flags; nodeStatuses[slot].activeEffect = payload.activeEffect; nodeStatuses[slot].activePreset = payload.activePreset; } void handleIncomingPacket(const IPAddress& remote, const uint8_t* data, uint16_t len) { if (len < sizeof(InfinityPacketHeader)) return; const InfinityPacketHeader* header = reinterpret_cast(data); if (header->magic != INFINITY_MAGIC || header->version != INFINITY_VERSION) return; if (sizeof(InfinityPacketHeader) + header->payloadSize > len) return; const uint8_t* payload = data + sizeof(InfinityPacketHeader); packetsReceived++; if (isNodeBuild() && (header->type == INFINITY_PACKET_CLOCK_SYNC || header->type == INFINITY_PACKET_SCENE_STATE || header->type == INFINITY_PACKET_BEAT_TRIGGER)) { masterOffsetUs = static_cast(header->masterTimeUs) - static_cast(localTimeUs()); } switch (header->type) { case INFINITY_PACKET_CLOCK_SYNC: updateStripTimebase(infinityScene.phase); break; case INFINITY_PACKET_SCENE_STATE: if (isNodeBuild() && header->payloadSize >= INFINITY_SCENE_PAYLOAD_LEGACY_SIZE && header->payloadSize <= sizeof(InfinityScenePayload)) { pendingScene = infinityScene; memcpy(&pendingScene, payload, header->payloadSize); hasPendingScene = true; lastSceneSequence = header->sequence; } break; case INFINITY_PACKET_BEAT_TRIGGER: if (isNodeBuild()) { strip.timebase = -static_cast(millis()); for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).markForReset(); } break; case INFINITY_PACKET_NODE_STATUS: if (isMasterBuild() && header->payloadSize == sizeof(InfinityStatusPayload)) { InfinityStatusPayload status; memcpy(&status, payload, sizeof(status)); storeNodeStatus(remote, status); } break; default: break; } } void receivePackets() { if (!infinityUdpStarted || !infinityEnabled) return; for (uint8_t i = 0; i < 6; i++) { const int packetSize = infinityUdp.parsePacket(); if (packetSize <= 0) return; if (packetSize > 256) { while (infinityUdp.available()) infinityUdp.read(); continue; } uint8_t buffer[256]; const int len = infinityUdp.read(buffer, sizeof(buffer)); if (len > 0) handleIncomingPacket(infinityUdp.remoteIP(), buffer, static_cast(len)); } } uint8_t jsonU8(JsonVariant value, uint8_t current) { if (value.isNull()) return current; int v = value.as(); if (v < 0) v = 0; if (v > 255) v = 255; return static_cast(v); } uint16_t jsonU16(JsonVariant value, uint16_t current) { if (value.isNull()) return current; long v = value.as(); if (v < 0) v = 0; if (v > 65535) v = 65535; return static_cast(v); } uint32_t jsonU32(JsonVariant value, uint32_t current) { if (value.isNull()) return current; long v = value.as(); if (v < 0) v = 0; return static_cast(v); } void readRgb(JsonVariant value, uint8_t rgb[3]) { if (!value.is()) return; JsonArray arr = value.as(); for (uint8_t i = 0; i < 3 && i < arr.size(); i++) rgb[i] = jsonU8(arr[i], rgb[i]); } void writeRgb(JsonArray arr, const uint8_t rgb[3]) { arr.add(rgb[0]); arr.add(rgb[1]); arr.add(rgb[2]); } void serializeNodeIps(JsonArray arr) { for (uint8_t i = 0; i < INFINITY_NODE_COUNT; i++) arr.add(ipToString(infinityNodeIps[i])); } void deserializeNodeIps(JsonVariant value) { if (!value.is()) return; JsonArray arr = value.as(); for (uint8_t i = 0; i < INFINITY_NODE_COUNT && i < arr.size(); i++) { if (!arr[i].is()) continue; IPAddress parsed; if (parsed.fromString(arr[i].as())) infinityNodeIps[i] = parsed; } } void serializeScene(JsonObject scene) { scene["effect"] = infinityScene.effectId; scene["preset"] = infinityScene.presetId; scene["brightness"] = infinityScene.brightness; scene["speed"] = infinityScene.speed; scene["intensity"] = infinityScene.intensity; scene["palette"] = infinityScene.palette; writeRgb(scene.createNestedArray("primary"), infinityScene.primary); writeRgb(scene.createNestedArray("secondary"), infinityScene.secondary); writeRgb(scene.createNestedArray("tertiary"), infinityScene.tertiary); scene["group_mask"] = infinityScene.groupMask; scene["direction"] = infinityScene.direction; scene["flags"] = infinityScene.flags; scene["transition_ms"] = infinityScene.transitionMs; scene["seed"] = infinityScene.seed; scene["phase"] = infinityScene.phase; scene["apply_at_us"] = static_cast(infinityScene.applyAtUs & 0xFFFFFFFFUL); JsonArray rows = scene.createNestedArray("row_dimmer"); rows.add(infinityScene.rowDimmer[0]); rows.add(infinityScene.rowDimmer[1]); rows.add(infinityScene.rowDimmer[2]); scene["strobe"] = infinityScene.strobe; scene["safety_fade"] = infinityScene.safetyFade; JsonObject spatial = scene.createNestedObject("spatial"); spatial["mode"] = infinityScene.spatialMode; spatial["strength"] = infinityScene.spatialStrength; spatial["blend"] = infinityScene.spatialBlend; spatial["variant"] = infinityScene.spatialVariant; spatial["direction"] = infinityScene.spatialDirection; spatial["size"] = infinityScene.spatialSize; spatial["angle"] = infinityScene.spatialAngle; spatial["option"] = infinityScene.spatialOption; } void deserializeScene(JsonObject scene) { if (scene.isNull()) return; infinityScene.effectId = jsonU8(scene["effect"], infinityScene.effectId); infinityScene.presetId = jsonU8(scene["preset"], infinityScene.presetId); infinityScene.brightness = jsonU8(scene["brightness"], infinityScene.brightness); infinityScene.speed = jsonU8(scene["speed"], infinityScene.speed); infinityScene.intensity = jsonU8(scene["intensity"], infinityScene.intensity); infinityScene.palette = jsonU8(scene["palette"], infinityScene.palette); readRgb(scene["primary"], infinityScene.primary); readRgb(scene["secondary"], infinityScene.secondary); readRgb(scene["tertiary"], infinityScene.tertiary); infinityScene.groupMask = jsonU8(scene["group_mask"], infinityScene.groupMask) & 0x07; if (infinityScene.groupMask == 0) infinityScene.groupMask = 0x07; infinityScene.direction = jsonU8(scene["direction"], infinityScene.direction); infinityScene.flags = jsonU8(scene["flags"], infinityScene.flags); infinityScene.transitionMs = jsonU16(scene["transition_ms"], infinityScene.transitionMs); infinityScene.seed = jsonU32(scene["seed"], infinityScene.seed); infinityScene.phase = jsonU16(scene["phase"], infinityScene.phase); if (scene["row_dimmer"].is()) { JsonArray rows = scene["row_dimmer"].as(); for (uint8_t i = 0; i < 3 && i < rows.size(); i++) infinityScene.rowDimmer[i] = jsonU8(rows[i], infinityScene.rowDimmer[i]); } infinityScene.strobe = jsonU8(scene["strobe"], infinityScene.strobe); infinityScene.safetyFade = jsonU8(scene["safety_fade"], infinityScene.safetyFade); if (scene["spatial"].is()) { JsonObject spatial = scene["spatial"].as(); infinityScene.spatialMode = jsonU8(spatial["mode"], infinityScene.spatialMode); infinityScene.spatialStrength = jsonU8(spatial["strength"], infinityScene.spatialStrength); infinityScene.spatialBlend = jsonU8(spatial["blend"], infinityScene.spatialBlend); infinityScene.spatialVariant = jsonU8(spatial["variant"], infinityScene.spatialVariant); infinityScene.spatialDirection = jsonU8(spatial["direction"], infinityScene.spatialDirection); infinityScene.spatialSize = jsonU8(spatial["size"], infinityScene.spatialSize); infinityScene.spatialAngle = jsonU16(spatial["angle"], infinityScene.spatialAngle); infinityScene.spatialOption = jsonU8(spatial["option"], infinityScene.spatialOption); } else { infinityScene.spatialMode = jsonU8(scene["spatial_mode"], infinityScene.spatialMode); infinityScene.spatialStrength = jsonU8(scene["spatial_strength"], infinityScene.spatialStrength); infinityScene.spatialBlend = jsonU8(scene["spatial_blend"], infinityScene.spatialBlend); infinityScene.spatialVariant = jsonU8(scene["spatial_variant"], infinityScene.spatialVariant); infinityScene.spatialDirection = jsonU8(scene["spatial_direction"], infinityScene.spatialDirection); infinityScene.spatialSize = jsonU8(scene["spatial_size"], infinityScene.spatialSize); infinityScene.spatialAngle = jsonU16(scene["spatial_angle"], infinityScene.spatialAngle); infinityScene.spatialOption = jsonU8(scene["spatial_option"], infinityScene.spatialOption); } if (infinityScene.spatialMode > INFINITY_SPATIAL_WAVE_LINE) infinityScene.spatialMode = INFINITY_SPATIAL_OFF; if (infinityScene.spatialBlend > INFINITY_SPATIAL_BLEND_PALETTE_TINT) infinityScene.spatialBlend = INFINITY_SPATIAL_BLEND_MULTIPLY_MASK; if (infinityScene.spatialVariant > INFINITY_SPATIAL_VARIANT_OUTLINE_REVERSE) infinityScene.spatialVariant = INFINITY_SPATIAL_VARIANT_EXPAND; if (infinityScene.spatialDirection > INFINITY_SPATIAL_DIRECTION_PINGPONG) infinityScene.spatialDirection = INFINITY_SPATIAL_DIRECTION_LTR; if (infinityScene.spatialAngle >= 360) infinityScene.spatialAngle %= 360; if (infinityScene.spatialOption > INFINITY_SPATIAL_OPTION_BANDS) infinityScene.spatialOption = INFINITY_SPATIAL_OPTION_LINE; markSceneChanged(); } } // namespace void infinityInit() { refreshRuntimeNodeId(); if (infinityScene.applyAtUs == 0) infinityScene.applyAtUs = masterTimeUs() + INFINITY_APPLY_DELAY_US; } void infinityNetworkBegin() { if (!infinityEnabled || infinityPort == 0) return; if (infinityUdpStarted) infinityUdp.stop(); infinityUdpStarted = infinityUdp.begin(infinityPort); refreshRuntimeNodeId(); USER_PRINTF("Infinity Sync %s on UDP %u as %s\n", infinityUdpStarted ? "started" : "failed", infinityPort, infinityRuntimeNodeId); } void infinityLoop() { refreshRuntimeNodeId(); receivePackets(); maybeApplyPendingScene(); const uint32_t now = millis(); if (isMasterBuild()) { if (now - lastClockSentMs >= INFINITY_CLOCK_INTERVAL_MS) { lastClockSentMs = now; sendClockSync(); } if (now - lastSceneSentMs >= INFINITY_SCENE_INTERVAL_MS) { lastSceneSentMs = now; sendSceneState(); } } else if (isNodeBuild()) { if (now - lastStatusSentMs >= INFINITY_STATUS_INTERVAL_MS) { lastStatusSentMs = now; sendNodeStatus(); } } } void infinityPostStripInit() { if (!isNodeBuild()) return; if (busses.getNumBusses() < 3 || strip.getLengthTotal() != INFINITY_LEDS_PER_NODE) return; bool expectedBusses = true; for (uint8_t i = 0; i < 3; i++) { Bus* bus = busses.getBus(i); if (bus == nullptr || bus->getLength() != INFINITY_LEDS_PER_PANEL || bus->getStart() != i * INFINITY_LEDS_PER_PANEL) { expectedBusses = false; break; } } if (!expectedBusses) return; if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; strip._segments.clear(); strip._segments.reserve(3); strip._segments.push_back(Segment(0, 106)); strip._segments.push_back(Segment(106, 212)); strip._segments.push_back(Segment(212, 318)); setSegmentName(strip._segments[0], "top"); setSegmentName(strip._segments[1], "middle"); setSegmentName(strip._segments[2], "bottom"); for (segment& seg : strip._segments) seg.refreshLightCapabilities(); strip.setMainSegmentId(0); esp32SemGive(segmentMux); } void infinityApplyDmx(uint8_t* data, uint16_t availableChannels) { if (!isMasterBuild() || data == nullptr || availableChannels < 1) return; uint8_t ch[INFINITY_DMX_FOOTPRINT] = {0}; const uint16_t count = availableChannels < INFINITY_DMX_FOOTPRINT ? availableChannels : INFINITY_DMX_FOOTPRINT; for (uint16_t i = 0; i < count; i++) ch[i] = data[i]; infinityScene.brightness = ch[0]; if (ch[1] == 0) infinityScene.flags &= ~0x01; else infinityScene.flags |= 0x01; infinityScene.presetId = ch[2]; infinityScene.effectId = ch[3]; infinityScene.speed = ch[4]; infinityScene.intensity = ch[5]; infinityScene.palette = ch[6]; hsvToRgb(ch[7], ch[8], ch[9], infinityScene.primary); infinityScene.direction = ch[10]; infinityScene.transitionMs = dmxTransitionMs(ch[11]); infinityScene.groupMask = ch[12] ? (ch[12] & 0x07) : 0x07; infinityScene.rowDimmer[0] = ch[13]; infinityScene.rowDimmer[1] = ch[14]; infinityScene.rowDimmer[2] = ch[15]; infinityScene.custom[0] = ch[18]; infinityScene.custom[1] = ch[19]; infinityScene.custom[2] = ch[20]; infinityScene.seed = static_cast(ch[21]) * 0x01010101UL; infinityScene.strobe = ch[22]; infinityScene.safetyFade = ch[23]; infinityScene.spatialMode = map(ch[24], 0, 255, 0, INFINITY_SPATIAL_WAVE_LINE); infinityScene.spatialVariant = map(ch[25], 0, 255, 0, INFINITY_SPATIAL_VARIANT_OUTLINE_REVERSE); infinityScene.spatialDirection = map(ch[26], 0, 255, 0, INFINITY_SPATIAL_DIRECTION_PINGPONG); infinityScene.spatialBlend = map(ch[27], 0, 255, 0, INFINITY_SPATIAL_BLEND_PALETTE_TINT); infinityScene.spatialStrength = ch[28]; infinityScene.spatialSize = ch[29]; infinityScene.spatialAngle = map(ch[30], 0, 255, 0, 359); infinityScene.spatialOption = ch[31] > 127 ? INFINITY_SPATIAL_OPTION_BANDS : INFINITY_SPATIAL_OPTION_LINE; markSceneChanged(); if (ch[16] > 127 && lastDmxBeatValue <= 127) sendBeatTrigger(); if (ch[17] > 127 && lastDmxSyncResetValue <= 127) { masterOffsetUs = 0; infinityScene.phase = 0; markSceneChanged(50000); sendClockSync(); } lastDmxBeatValue = ch[16]; lastDmxSyncResetValue = ch[17]; } void infinityHandleOverlayDraw() { if (!infinityEnabled || !isNodeBuild()) return; if (infinityScene.spatialMode == INFINITY_SPATIAL_OFF || infinityScene.spatialStrength == 0) return; if (strip.getSegmentsNum() < INFINITY_PANEL_COUNT || strip.getLengthTotal() < INFINITY_LEDS_PER_NODE) return; uint8_t column = 0; parseNodeColumn(column); const uint64_t nowUs = masterTimeUs() + (static_cast(infinityScene.phase) * 1000ULL); for (uint8_t row = 0; row < INFINITY_PANEL_COUNT; row++) { Segment& seg = strip.getSegment(row); if (!seg.isActive()) continue; const uint16_t len = min(seg.length(), INFINITY_LEDS_PER_PANEL); for (uint16_t led = 0; led < len; led++) { const uint8_t amount = spatialAmount(column, row, led, nowUs); if (amount == 0 && infinityScene.spatialBlend != INFINITY_SPATIAL_BLEND_REPLACE) { if (infinityScene.spatialBlend == INFINITY_SPATIAL_BLEND_MULTIPLY_MASK || infinityScene.spatialBlend == INFINITY_SPATIAL_BLEND_PALETTE_TINT) { seg.setPixelColor(led, BLACK); } continue; } const uint32_t base = seg.getPixelColor(led); const uint32_t layer = spatialLayerColor(seg, led, amount); seg.setPixelColor(led, applySpatialBlend(base, layer, amount, infinityScene.spatialStrength, infinityScene.spatialBlend)); } } } void infinitySerializeJson(JsonObject root) { refreshRuntimeNodeId(); root["enabled"] = infinityEnabled; root["role"] = isMasterBuild() ? "master" : "node"; root["node_id"] = infinityRuntimeNodeId; root["configured_node_id"] = infinityConfiguredNodeId; root["port"] = infinityPort; root["udp_started"] = infinityUdpStarted; root["master_ip"] = ipToString(infinityMasterIp); root["master_time_us"] = static_cast(masterTimeUs() & 0xFFFFFFFFUL); root["master_offset_us"] = static_cast(masterOffsetUs); root["packets_received"] = packetsReceived; root["packets_sent"] = packetsSent; serializeNodeIps(root.createNestedArray("node_ips")); JsonObject scene = root.createNestedObject("scene"); serializeScene(scene); JsonArray nodes = root.createNestedArray("nodes"); const uint32_t now = millis(); for (uint8_t i = 0; i < INFINITY_NODE_COUNT; i++) { JsonObject node = nodes.createNestedObject(); node["expected_ip"] = ipToString(infinityNodeIps[i]); node["ip"] = nodeStatuses[i].lastSeenMs ? ipToString(nodeStatuses[i].ip) : ""; node["node_id"] = nodeStatuses[i].nodeId; node["online"] = nodeStatuses[i].lastSeenMs && (now - nodeStatuses[i].lastSeenMs <= INFINITY_NODE_TIMEOUT_MS); node["last_seen_ms"] = nodeStatuses[i].lastSeenMs ? now - nodeStatuses[i].lastSeenMs : 0; node["uptime_ms"] = nodeStatuses[i].uptimeMs; node["master_offset_us"] = nodeStatuses[i].masterOffsetUs; node["scene_sequence"] = nodeStatuses[i].lastSceneSequence; node["packets_received"] = nodeStatuses[i].packetsReceived; node["effect"] = nodeStatuses[i].activeEffect; node["preset"] = nodeStatuses[i].activePreset; } } bool infinityDeserializeJson(JsonObject root) { if (root.isNull()) return false; const bool hadSceneObject = root["scene"].is(); const bool wasEnabled = infinityEnabled; if (!root["enabled"].isNull()) infinityEnabled = root["enabled"].as(); if (root["node_id"].is()) copyNodeId(infinityConfiguredNodeId, sizeof(infinityConfiguredNodeId), root["node_id"].as()); if (root["master_ip"].is()) { IPAddress parsed; if (parsed.fromString(root["master_ip"].as())) infinityMasterIp = parsed; } if (!root["port"].isNull()) { uint16_t newPort = jsonU16(root["port"], infinityPort); if (newPort != infinityPort && newPort > 0) { infinityPort = newPort; infinityNetworkBegin(); } } deserializeNodeIps(root["node_ips"]); if (wasEnabled != infinityEnabled) { if (infinityEnabled) { infinityNetworkBegin(); } else if (infinityUdpStarted) { infinityUdp.stop(); infinityUdpStarted = false; } } JsonObject scene = root["scene"].is() ? root["scene"].as() : root; deserializeScene(scene); if (isMasterBuild() && infinityUdpStarted) { sendClockSync(); if (hadSceneObject || !root["node_ips"].isNull()) sendSceneState(); } doSerializeConfig = true; return true; } void infinityDeserializeConfig(JsonObject interfaces) { JsonObject cfg = interfaces["infinity"]; if (cfg.isNull()) return; if (!cfg["enabled"].isNull()) infinityEnabled = cfg["enabled"].as(); if (!cfg["port"].isNull()) infinityPort = jsonU16(cfg["port"], infinityPort); if (cfg["node_id"].is()) copyNodeId(infinityConfiguredNodeId, sizeof(infinityConfiguredNodeId), cfg["node_id"].as()); if (cfg["master_ip"].is()) { IPAddress parsed; if (parsed.fromString(cfg["master_ip"].as())) infinityMasterIp = parsed; } deserializeNodeIps(cfg["node_ips"]); } void infinitySerializeConfig(JsonObject interfaces) { JsonObject cfg = interfaces.createNestedObject("infinity"); cfg["enabled"] = infinityEnabled; cfg["role"] = isMasterBuild() ? "master" : "node"; cfg["node_id"] = infinityConfiguredNodeId; cfg["master_ip"] = ipToString(infinityMasterIp); cfg["port"] = infinityPort; serializeNodeIps(cfg.createNestedArray("node_ips")); } void serveInfinityJson(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(6144, false); JsonObject root = response->getRoot().as(); infinitySerializeJson(root); response->setLength(); request->send(response); } void serveInfinityPage(AsyncWebServerRequest* request) { static const char PAGE_infinity[] PROGMEM = R"rawliteral( Infinity Controller

Infinity Controller

WLED-style master surface for your installation

WLED UI Sync

Master

Loading master status…

Sync

Checking node target layout…

Scene

Output Enabled
Master sends scene state, nodes render locally on apply time.

Global 2D

Reverse Direction
Maps to the Infinity direction flag.

Colors

Rows

Nodes

Node Targets

These are the unicast destinations the master sends Infinity Sync to.

Usage

This page complements WLED. Use the regular WLED UI for generic device settings, Wi-Fi and firmware basics.

Ready. Changes stay local until you press Apply Scene or Save Targets.
)rawliteral"; request->send_P(200, "text/html", PAGE_infinity); } #endif // WLED_ENABLE_INFINITY_CONTROLLER