Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/moonbase/inputoutput.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ For each board the following presets are defined:

* Modded: set when any pin differs from the selected board preset. Press off to return to the preset defaults.
* Max Power in Watts: adjust the brightness to approach this max power, depending on the number of LEDs used. Default 10: 5V * 2A = 10W (so it runs fine on USB). Used by LED drivers, see [Drivers](../../moonlight/drivers/)
* Switch1 and 2: If the board contains a jumper or pins have different functions, a custom switch can be set. Eg. select between Infrared and Ethernet. See boards below for details
* Switch1 and 2: If the board contains a jumper or pins have different functions, a custom switch can be set. Eg. select between Infrared and Ethernet. If a switch is turned on or off, the board the modded status will not change. See boards below for details
* Pins: Assign functionality to gpio pins. Other modules and nodes use the pin assignments made here.
* GPIO = gpio_num;
* Usage: See below
Expand Down Expand Up @@ -89,4 +89,4 @@ For each board the following presets are defined:
![SE-16p](../firmware/installer/images/esp32-s3-stephanelec-16p.jpg){: style="width:100px"}

* Choose the esp32-s3-devkitc-1-n8r8v board in the [MoonLight Installer](../../gettingstarted/installer/)
* Set Switch1 the same as you set the jumper on the board: off / default: Ethernet. on: Infrared.
* Set Switch1 the same as you set the jumper on the board: off / default: Infrared. on: Ethernet.
13 changes: 7 additions & 6 deletions docs/moonlight/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Want to add a Driver to MoonLight, see [develop](../../develop/overview/). See a
Receives Art-Net data from the network.

* DDP: If unchecked, processes data in Art-Net format, if checked, process data in DDP format
* Port: The port listening to for Art-Net
* Port: The port listening for Art-Net. When using DDP, change to 4048 (the default port for DDP).
* Universe Min-Max: Filters Universes (Art-Net only).
* View:
* Select physical layer to directly store the received channels into the physical layer
Expand All @@ -93,17 +93,18 @@ Sends Lights in Art-Net compatible packages to an Art-Net controller specified b

#### Controls

* **Controller IP**: The last segment of the IP address within your local network, of the the hardware Art-Net controller.
* **Controller IPs**: The last segment of the IP address within your local network, of the hardware Art-Net controller. Add more IPs if you send to more than one controller, comma separated.
* **Port**: The network port added to the IP address, 6454 is the default for Art-Net.
* **FPS Limiter**: set the max frames per second Art-Net packages are send out (also all the other nodes will run at this speed).
* Art-Net specs recommend about 44 FPS but higher framerates will work mostly (up to until ~130FPS tested)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix list indentation per markdown standards.

Line 99 uses 4-space indentation instead of the standard 2-space indentation for nested list items.

-    * Art-Net specs recommend about 44 FPS but higher framerates will work mostly (up to until ~130FPS tested)
+  * Art-Net specs recommend about 44 FPS but higher framerates will work mostly (up to until ~130FPS tested)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Art-Net specs recommend about 44 FPS but higher framerates will work mostly (up to until ~130FPS tested)
* Art-Net specs recommend about 44 FPS but higher framerates will work mostly (up to until ~130FPS tested)
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

99-99: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🤖 Prompt for AI Agents
docs/moonlight/drivers.md around line 99: the nested list item uses 4-space
indentation instead of the standard 2-space indentation for markdown lists;
change the indentation of the nested bullet(s) on line 99 from 4 spaces to 2
spaces so the list conforms to markdown nesting conventions and renders
correctly.

* **Nr of outputs**: Art-Net LED controllers can have more then 1 output (e.g. 12)
* **Nr of outputs**: Art-Net LED controllers can have more than 1 output (e.g. 12)
* **Universes per output**: How many universes can each output handle. This determines the maximum number of lights an output can drive (nr of universe x nr of channels per universe / channels per light)
* **Nr of Outputs per IP**: How many outputs does one Art-Net controller have. If all outputs are sent, Art-Net will be sent to the next IP number.

!!! tip "Set universes"
Set the number of universes also on the controller!

* **Channels per ouput**: each output can drive a maximum number of channels, determined by the universes per output
* **Channels per output**: each output can drive a maximum number of channels, determined by the universes per output

!!! warning "DMX start with 1"
Dmx channels count from 1 to 512. At the moment we start counting from 0..511.
Expand All @@ -119,7 +120,7 @@ The following devices have been tested and are recommended:
* Driving DMX fixtures: Used to drive the Light Presets for DMX lights / moving heads (see below)
* **Nr of outputs**: max 2 outputs
* **Universes per output**: 1 universe
* **Channels per ouput**: 512 channels
* **Channels per output**: 512 channels

[Club Lights 12 Pro Artnet Controller - CL12P](https://s.click.aliexpress.com/e/_Ex9uaOk)

Expand All @@ -129,7 +130,7 @@ The following devices have been tested and are recommended:

* **Nr of outputs**: Max 12 outputs
* **Universes per output**: Max 8 universes
* **Channels per ouput**: max 8 * 512.
* **Channels per output**: max 8 * 512.

Each color in a LED is one channel: For RGB max 170 LEDs is 510 channels per universe, for RGBW max 128 LEDs per universe is 512 channels per universe => max 1360 RGB LEDs and 1024 RGBW LEDs per output.

Expand Down
4 changes: 2 additions & 2 deletions interface/src/lib/components/moonbase/FieldRenderer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
}, 1000);

onDestroy(() => {
console.log('FieldRenderer clearing interval');
// console.log('FieldRenderer clearing interval');
clearInterval(interval);
});

Expand Down Expand Up @@ -107,7 +107,7 @@
{#if property.type == 'ip'}
<a href="http://{value}" target="_blank">{value}</a>
{:else if property.type == 'mdnsName'}
<a href="http://{value}.local" target="_blank">{value}</a>
<a href="http://{value}.local/moonbase/module?group=moonlight&module=lightscontrol" target="_blank">{value}</a>
{:else if property.type == 'time'}
<span>{getTimeAgo(value, currentTime)}</span>
{:else if property.type == 'coord3D' && value != null}
Expand Down
3 changes: 3 additions & 0 deletions interface/src/lib/components/moonbase/RowRenderer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@
// Update the default value reactively so we don't capture the initial derived value only once
$effect(() => {
propertyFilter.default = findItemInDefinition?.filter ?? '!Unused';
// assign default filter if not defined yet
if (data[property.name + '_filter'] == null)
data[property.name + '_filter'] = propertyFilter.default;
});
</script>

Expand Down
2 changes: 0 additions & 2 deletions interface/src/lib/stores/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ function createTelemetry() {
rssi: { rssi: 0, ssid: data.ssid, disconnected: true, safeMode: data.safeMode, restartNeeded: data.restartNeeded, saveNeeded: data.saveNeeded, hostName: data.hostName } // 🌙 variables added
}));
}
if (data.hostName != '')
localStorage.setItem('telemetry.rssi.hostName', data.hostName);
},
setBattery: (data: Battery) => {
update((telemetry_data) => ({
Expand Down
2 changes: 1 addition & 1 deletion interface/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
</script>

<svelte:head>
<title>{localStorage.getItem('telemetry.rssi.hostName')}</title>
<title>{$telemetry.rssi.hostName || 'MoonLight'}</title>
</svelte:head>

{#if page.data.features.security && $user.bearer_token === ''}
Expand Down
2 changes: 1 addition & 1 deletion interface/src/routes/statusbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
><Hamburger class="h-6 w-auto" /></label
>
<img src={logo} alt="Logo" class="h-12 w-12 lg:hidden" /> <!-- 🌙 -->
<span class="px-2 text-xl font-bold lg:text-2xl">{localStorage.getItem('telemetry.rssi.hostName')}</span> <!-- 🌙 -->
<span class="px-2 text-xl font-bold lg:text-2xl">{$telemetry.rssi.hostName || 'MoonLight'}</span> <!-- 🌙 -->
</div>
<div class="indicator flex-none">
<UpdateIndicator />
Expand Down
2 changes: 0 additions & 2 deletions lib/framework/EventEndpoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ class EventEndpoint

void syncState(const String &originId, bool sync = false)
{
if (!_socket->getConnectedClients()) return; // 🌙 No need for UI tasks

JsonDocument jsonDocument;
JsonObject root = jsonDocument.to<JsonObject>();
_statefulService->read(root, _stateReader);
Expand Down
7 changes: 5 additions & 2 deletions lib/framework/StatefulService.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ class StatefulService
{
// 🌙 adding semaphore wait too long logging
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
ESP_LOGW("🐼", "_accessMutex wait too long");
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
ESP_LOGI("🐼", "_accessMutex wait 100ms");
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
ESP_LOGW("🐼", "_accessMutex waited 500ms and continues 🤷‍♂️");
// xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
}
}
Comment on lines 201 to 207
Copy link

@coderabbitai coderabbitai bot Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Mutex acquisition failure leads to unprotected access and semaphore corruption.

If both xSemaphoreTakeRecursive attempts fail (lines 201 and 203), the function continues without holding the mutex. This causes:

  1. Race conditions: The code between beginTransaction() and endTransaction() will execute without mutex protection, allowing concurrent access to _state.
  2. Semaphore corruption: endTransaction() (line 212) will call xSemaphoreGiveRecursive without a matching successful take, violating the semaphore contract.
  3. Silent failure: Callers have no way to detect that mutual exclusion has failed.

Consider one of these solutions:

Option 1 (Recommended): Track acquisition success and conditionally release

 inline void beginTransaction()
 {
+    _mutexAcquired = false;
     // 🌙 adding semaphore wait too long logging
     if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
         ESP_LOGI("🐼", "_accessMutex wait 100ms");
         if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
-            ESP_LOGW("🐼", "_accessMutex waited 500ms and continues 🤷‍♂️");
-            // xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
+            ESP_LOGW("🐼", "_accessMutex waited 500ms, blocking indefinitely");
+            xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
         }
     }
+    _mutexAcquired = true;
 }

 inline void endTransaction()
 {
-    xSemaphoreGiveRecursive(_accessMutex);
+    if (_mutexAcquired) {
+        xSemaphoreGiveRecursive(_accessMutex);
+        _mutexAcquired = false;
+    }
 }

(Add bool _mutexAcquired = false; member variable)

Option 2: Restore indefinite blocking

         if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
-            ESP_LOGW("🐼", "_accessMutex waited 500ms and continues 🤷‍♂️");
-            // xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
+            ESP_LOGW("🐼", "_accessMutex waited 500ms, blocking indefinitely");
+            xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
ESP_LOGW("🐼", "_accessMutex wait too long");
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
ESP_LOGI("🐼", "_accessMutex wait 100ms");
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
ESP_LOGW("🐼", "_accessMutex waited 500ms and continues 🤷‍♂️");
// xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
}
}
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
ESP_LOGI("🐼", "_accessMutex wait 100ms");
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
ESP_LOGW("🐼", "_accessMutex waited 500ms, blocking indefinitely");
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
}
}
🤖 Prompt for AI Agents
In lib/framework/StatefulService.h around lines 201-207, the code continues when
both xSemaphoreTakeRecursive calls fail which leads to unprotected access and a
mismatched give; fix by tracking whether the mutex was actually acquired and
only releasing it if held. Add a local bool (e.g. mutexAcquired) initialized
false, set it true when either take succeeds, and if both attempts fail either
return an error/abort the transaction or block indefinitely per policy; update
endTransaction() to check mutexAcquired before calling xSemaphoreGiveRecursive
(or clear the flag after give) so give is only called on a successful take, and
ensure callers handle the failure path (propagate error or avoid accessing
_state when mutexAcquired is false).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is a risk 2 processes mutating the same, however due to recursiveness, the same process can try to take the mutex twice, at the moment it is too much overhead to track if the same process has acquired it already, but you may suggest an easy fix for that.
I don't think xSemaphoreGiveRecursive will fail if no xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); has taken place, but any feedback on that statement appreciated

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any FreeRTOS documentation or comments about semaphore behavior in the codebase
rg -n -C3 "xSemaphoreGive" --type=cpp --type=c --type=h

Length of output: 4107


🌐 Web query:

FreeRTOS xSemaphoreGiveRecursive behavior without matching take

💡 Result:

Short answer: if you call xSemaphoreGiveRecursive() on a recursive mutex that the calling task does not hold, the call will fail (return pdFALSE) — it does not silently decrement an internal count or hand ownership to the caller. FreeRTOS documents that xSemaphoreGiveRecursive returns pdTRUE when the semaphore is successfully given and pdFALSE on error (for example when the semaphore was not previously obtained), and community reports describe xSemaphoreGiveRecursive returning FALSE when the mutex is not owned by the caller. [1][2]

Sources:

  • FreeRTOS API reference (xSemaphoreGiveRecursive / behavior & return values). [1]
  • FreeRTOS forum thread reporting xSemaphoreGiveRecursive returns FALSE if the mutex is not held by the calling task. [2]

@ewowi, I need to clarify a few points about FreeRTOS recursive mutexes:

About Recursive Tracking

Good news: There's no overhead! FreeRTOS recursive mutexes (xSemaphoreCreateRecursiveMutex) already track recursion internally. When the same task calls xSemaphoreTakeRecursive multiple times, FreeRTOS automatically:

  • Increments an internal counter
  • Allows the take to succeed immediately (same task)
  • Requires matching xSemaphoreGiveRecursive calls to fully release

This is built into FreeRTOS - you don't need to implement any tracking yourself. The recursive mutex exists specifically to handle same-task re-entry without deadlock.

About xSemaphoreGiveRecursive Failure

Your assumption is incorrect: xSemaphoreGiveRecursive does fail when called without a matching take. It returns pdFALSE when the mutex is not held by the calling task. Your current code ignores this return value, so you won't notice the failure, but the semaphore state can become inconsistent.

The Real Problem

The critical issue isn't recursion tracking - it's that if both xSemaphoreTakeRecursive attempts fail (lines 201 and 203), your code continues to execute the critical section without mutex protection. This creates a race condition where multiple tasks can simultaneously access _state.

Easy Fix

The simplest solution is to restore the indefinite blocking you commented out:

if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
    ESP_LOGI("🐼", "_accessMutex wait 100ms");
    if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
        ESP_LOGW("🐼", "_accessMutex waited 500ms, blocking indefinitely");
        xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);  // ← uncomment this
    }
}

This ensures the mutex is always acquired before accessing _state, preventing race conditions. The logging provides diagnostics for abnormally long waits while maintaining safety.


🧬 Code Graph Analysis Results

[StatefulService.h:38-44] Constructor and recursive mutex initialization

  • Summary: StatefulService(Args&&... args) initializes _state with forwarded args and creates _accessMutex using xSemaphoreCreateRecursiveMutex().

[StatefulService.h:100-115] beginTransaction

  • Summary: Attempts to take _accessMutex recursively with a 100 ms timeout; if it fails, logs a 100 ms wait, then retries for 400 ms; if still failing, logs a warning and continues (comment indicates potential further attempt). This manages nested/protected access for state mutations.

[StatefulService.h:116-123] endTransaction

  • Summary: Releases the recursive mutex via xSemaphoreGiveRecursive(_accessMutex).

[StatefulService.h:125-133] private members

  • Summary: Private members include SemaphoreHandle_t _accessMutex and lists for update and hook handlers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, correction: the thing is not that the same process tries to acquire it, I think the effect task processing a state change, hands over to the SvelteKit task to recursively call begin transaction, apparently before the effect task has freed it. Or something like this

What I can tell you that occasionally infinite locks happen.

So can you check the code to find the root cause for this locking between multiple processes? And provide a solution. Maybe some tasks need to explicitly give the mutex if they are done with whatever they are doing ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for addUpdateHandler calls to understand what callbacks are registered
echo "=== addUpdateHandler usage ==="
rg -n -C3 'addUpdateHandler\(' --type=cpp --type=h -g '!*.md'

Length of output: 13480


🏁 Script executed:

#!/bin/bash
# Search for update() method calls on StatefulService to see patterns
echo "=== StatefulService update() patterns ==="
rg -n -C3 '\.update\(' --type=cpp --type=h -g '!*.md' -g '!*Test*' | head -200

Length of output: 850


🏁 Script executed:

#!/bin/bash
# Look for task creation patterns (xTaskCreate, xTaskCreatePinnedToCore)
echo "=== Task creation patterns ==="
rg -n -C5 'xTaskCreate' --type=cpp --type=h

Length of output: 3687


🏁 Script executed:

#!/bin/bash
# Search for callUpdateHandlers to see where it's invoked
echo "=== callUpdateHandlers invocations ==="
rg -n -C5 'callUpdateHandlers' --type=cpp --type=h

Length of output: 4064


🏁 Script executed:

#!/bin/bash
# Look for patterns where update handlers might trigger nested updates
echo "=== Nested state access patterns ==="
rg -n -C3 'beginTransaction|endTransaction' --type=cpp --type=h | head -150

Length of output: 4292


🏁 Script executed:

#!/bin/bash
# Search for patterns where update handlers call read() or update() on other modules
echo "=== Cross-module calls in update handlers ==="
rg -n -A10 'addUpdateHandler.*\[' --type=cpp --type=h | grep -E '(->read\(|->update\(|\.read\(|\.update\()' | head -30

Length of output: 532


🏁 Script executed:

#!/bin/bash
# Look for FileManager update handlers that might trigger cascading updates
echo "=== FileManager update handler patterns ==="
rg -n -A15 '_fileManager->addUpdateHandler' --type=cpp --type=h

Length of output: 4972


🏁 Script executed:

#!/bin/bash
# Look for ModuleIO update handler patterns
echo "=== ModuleIO update handler patterns ==="
rg -n -A10 'moduleIO.*addUpdateHandler|_moduleIO->addUpdateHandler' --type=cpp --type=h

Length of output: 3465


🏁 Script executed:

#!/bin/bash
# Check if there's any async task queue or deferred execution mechanism
echo "=== Async/deferred execution patterns ==="
rg -n 'xQueue|QueueHandle|vTaskDelay|yield|defer' --type=cpp --type=h | head -50

Length of output: 1190


@ewowi, I've found the root cause of your deadlock issue! It's a classic circular dependency deadlock between multiple tasks, not a recursive lock issue.

Root Cause Analysis

Your system has multiple tasks that can update modules concurrently:

  • effectTask (priority 3, core 1)
  • driverTask (priority 3, core 1)
  • ESP32SvelteKit loop task (priority 2, core 0)

The deadlock occurs because update handlers immediately acquire mutexes of other modules:

Example Deadlock Chain

From ModuleLiveScripts.h:40-48:

_fileManager->addUpdateHandler([this](const String& originId) {
  // Handler runs in context of whatever task called FileManager.update()
  _fileManager->read([&](FilesState& filesState) {     // ← Re-acquires FileManager mutex!
    _moduleEffects->read([&](ModuleState& effectsState) {  // ← Also acquires Effects mutex!

Deadlock scenario:

  1. Task A (effectTask): Updates moduleEffects → holds Effects mutex → triggers FileManager handler → tries _fileManager->read()blocks waiting for FileManager mutex
  2. Task B (SvelteKit): Updates FileManager → holds FileManager mutex → triggers handler → tries _moduleEffects->read()blocks waiting for Effects mutex
  3. Result: Task A waits for B's mutex, Task B waits for A's mutex → infinite deadlock 🔒

Similar chains exist for ModuleIO → ModuleDrivers/ModuleLightsControl.

Why Recursive Mutex Doesn't Help

Recursive mutexes only allow the same task to re-enter. Different tasks (effectTask vs SvelteKit loop) cannot share ownership, so the recursive property provides no protection against cross-task deadlocks.

The Current Code Problem

Your commented-out portMAX_DELAY creates two severe bugs:

  1. Unprotected access: Code continues without mutex → race conditions
  2. Semaphore corruption: endTransaction() calls xSemaphoreGiveRecursive() without successful take → undefined behavior

Solutions

Immediate Fix: Restore Blocking (Required)

This prevents unprotected access and semaphore corruption:

if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
    ESP_LOGI("🐼", "_accessMutex wait 100ms");
    if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
        ESP_LOGW("🐼", "_accessMutex waited 500ms, blocking indefinitely");
        xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);  // ← REQUIRED
    }
}

However, this will cause permanent hangs on deadlock instead of unprotected access. You need the architectural fix below.

Architectural Fix: Defer Cross-Module Calls

The proper solution is to never call .read() or .update() on other modules from within update handlers. Instead, use a deferred execution pattern:

Option 1: Task Notification Pattern (Recommended)

Add a "dirty flag" mechanism to modules that need cross-module data:

class NodeManager {
private:
    volatile bool _fileManagerDirty = false;
    
public:
    void begin() {
        // Set flag instead of immediate read
        _fileManager->addUpdateHandler([this](const String& originId) {
            _fileManagerDirty = true;  // Atomic flag set
        });
    }
    
    void loop() {
        // Check and process in your own task context
        if (_fileManagerDirty) {
            _fileManagerDirty = false;
            _fileManager->read([&](FilesState& filesState) {
                // Safe: runs in NodeManager's task, not handler context
                for (auto updatedItem : filesState.updatedItems) {
                    // ... process updates
                }
            });
        }
        // ... rest of loop
    }
};

Option 2: Pass Data in Handler (Alternative)

Modify handlers to receive necessary data as parameters:

// In StatefulService.h, create a variant that passes state copy
typedef std::function<void(const String &originId, const T& stateCopy)> StateUpdateCallbackWithData;

void callUpdateHandlers(const String &originId) {
    T stateCopy = _state;  // Copy outside mutex
    for (const StateUpdateHandlerInfo_t &updateHandler : _updateHandlers) {
        updateHandler._cb(originId, stateCopy);  // Pass copy
    }
}

Then handlers don't need to call .read():

_fileManager->addUpdateHandler([this](const String& originId, const FilesState& filesState) {
    // No read() call needed - data passed in
    for (auto updatedItem : filesState.updatedItems) {
        // ... process
    }
});

Specific Code Changes Needed

Looking at your codebase, these handlers need refactoring:

  1. NodeManager.h:41 - FileManager handler calls _fileManager->read()
  2. ModuleLiveScripts.h:40 - FileManager handler calls _fileManager->read() AND _moduleEffects->read()
  3. ModuleLightsControl.h:48 - FileManager handler calls _fileManager->read()
  4. ModuleLightsControl.h:68 - ModuleIO handler calls moduleIO.read()
  5. ModuleDrivers.h:76 - ModuleIO handler calls (presumably) other reads in readPins()

All of these should either:

  • Use dirty flags and check them in the module's loop() method
  • Receive data as parameters instead of calling .read()

Would you like me to generate a detailed refactoring for one of these as an example?


🧬 Code Graph Analysis Results

<filename=lib/framework/StatefulService.h>

Line range (approximate): Constructor
template
class StatefulService
{
public:
template <typename... Args>
StatefulService(Args &&...args) : _state(std::forward(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex())
{
}

<filename=lib/framework/StatefulService.h>

Line range (approximate): beginTransaction
inline void beginTransaction()
{
// 🌙 adding semaphore wait too long logging
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(100))==pdFALSE) {
ESP_LOGI("🐼", "_accessMutex wait 100ms");
if (xSemaphoreTakeRecursive(_accessMutex, pdMS_TO_TICKS(400))==pdFALSE) {
ESP_LOGW("🐼", "_accessMutex waited 500ms and continues 🤷‍♂️");
// xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
}
}
}

<filename=lib/framework/StatefulService.h>

Line range (approximate): endTransaction
inline void endTransaction()
{
xSemaphoreGiveRecursive(_accessMutex);
}

<filename=lib/framework/StatefulService.h>

Line range (approximate): private members
private:
SemaphoreHandle_t _accessMutex;
std::list<StateUpdateHandlerInfo_t> _updateHandlers;
std::list<StateHookHandlerInfo_t> _hookHandlers;

<no_relevant_code_snippets>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai,regarding

Would you like me to generate a detailed refactoring for one of these as an example?

Yes for all 5 files please

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and extract the problematic update handler patterns from all 5 files

echo "=== 1. NodeManager.h - FileManager handler ==="
rg -n -A20 'addUpdateHandler' src/MoonBase/NodeManager.h | head -30

echo -e "\n=== 2. ModuleLiveScripts.h - FileManager handler ==="
rg -n -A25 'addUpdateHandler' src/MoonLight/Modules/ModuleLiveScripts.h | head -40

echo -e "\n=== 3. ModuleLightsControl.h - FileManager handler (line 48) ==="
rg -n -A20 'addUpdateHandler' src/MoonLight/Modules/ModuleLightsControl.h | head -60

echo -e "\n=== 4. ModuleLightsControl.h - ModuleIO handler (line 68) ==="
rg -n -A20 'addUpdateHandler' src/MoonLight/Modules/ModuleLightsControl.h | tail -40

echo -e "\n=== 5. ModuleDrivers.h - ModuleIO handler ==="
rg -n -A20 'addUpdateHandler' src/MoonLight/Modules/ModuleDrivers.h | head -40

Length of output: 8344


🏁 Script executed:

#!/bin/bash
# Get the full class definitions to understand member variables and loop() methods

echo "=== NodeManager class structure ==="
ast-grep --pattern $'class NodeManager {
  $$$
}'  src/MoonBase/NodeManager.h

echo -e "\n=== ModuleLiveScripts class structure ==="
rg -n 'class ModuleLiveScripts|void loop\(|private:|public:' src/MoonLight/Modules/ModuleLiveScripts.h | head -20

echo -e "\n=== ModuleLightsControl class structure ==="
rg -n 'class ModuleLightsControl|void loop\(|private:|public:' src/MoonLight/Modules/ModuleLightsControl.h | head -20

echo -e "\n=== ModuleDrivers class structure ==="
rg -n 'class ModuleDrivers|void loop\(|private:|public:' src/MoonLight/Modules/ModuleDrivers.h | head -20

Length of output: 497


🏁 Script executed:

#!/bin/bash
# Check if these modules already have loop() methods

echo "=== Looking for existing loop() implementations ==="

echo "NodeManager.h:"
rg -n -A10 'void loop\(' src/MoonBase/NodeManager.h

echo -e "\nModuleLiveScripts.h:"
rg -n -A10 'void loop\(' src/MoonLight/Modules/ModuleLiveScripts.h

echo -e "\nModuleLightsControl.h:"
rg -n -A10 'void loop\(' src/MoonLight/Modules/ModuleLightsControl.h

echo -e "\nModuleDrivers.h:"
rg -n -A10 'void loop\(' src/MoonLight/Modules/ModuleDrivers.h

Length of output: 956


@ewowi, here are the detailed refactoring examples for all 5 files. The pattern is consistent: set flags in handlers, check flags in loop().


1. NodeManager.h

Add private member variable:

private:
    volatile bool _fileManagerDirty = false;

BEFORE (lines 41-61):

_fileManager->addUpdateHandler([this](const String& originId) {
  EXT_LOGV(ML_TAG, "FileManager::updateHandler %s", originId.c_str());
  // read the file state (read all files and folders on FS and collect changes)
  _fileManager->read([&](FilesState& filesState) {
    // loop over all changed files (normally only one)
    for (auto updatedItem : filesState.updatedItems) {
      // if file is the current live script, recompile it (to do: multiple live effects)
      EXT_LOGV(ML_TAG, "updateHandler updatedItem %s", updatedItem.c_str());
      Char<32> name;
      name.format("/.config/%s.json", _moduleName);
      if (equal(updatedItem.c_str(), name.c_str())) {
        EXT_LOGV(ML_TAG, " %s updated -> call update %s", name.c_str(), updatedItem.c_str());
        readFromFS();  // repopulates the state, processing file changes
      }
    }
  });
});

AFTER:

_fileManager->addUpdateHandler([this](const String& originId) {
  EXT_LOGV(ML_TAG, "FileManager::updateHandler %s (deferred)", originId.c_str());
  _fileManagerDirty = true;  // Set flag instead of immediate read
});

Add loop() method:

void loop() override {
  Module::loop();  // Call parent if NodeManager inherits from Module
  
  // Process deferred FileManager updates
  if (_fileManagerDirty) {
    _fileManagerDirty = false;
    
    _fileManager->read([&](FilesState& filesState) {
      for (auto updatedItem : filesState.updatedItems) {
        EXT_LOGV(ML_TAG, "Processing updatedItem %s", updatedItem.c_str());
        Char<32> name;
        name.format("/.config/%s.json", _moduleName);
        if (equal(updatedItem.c_str(), name.c_str())) {
          EXT_LOGV(ML_TAG, " %s updated -> call readFromFS %s", name.c_str(), updatedItem.c_str());
          readFromFS();  // Safe: runs in NodeManager's task context
        }
      }
    });
  }
}

2. ModuleLiveScripts.h

Add private member variables:

private:
    volatile bool _fileManagerDirty = false;

BEFORE (lines 40-65):

_fileManager->addUpdateHandler([this](const String& originId) {
  EXT_LOGV(ML_TAG, "FileManager::updateHandler %s", originId.c_str());
  // read the file state (read all files and folders on FS and collect changes)
  _fileManager->read([&](FilesState& filesState) {
    // loop over all changed files (normally only one)
    for (auto updatedItem : filesState.updatedItems) {
      // if file is the current live script, recompile it (to do: multiple live effects)
      uint8_t index = 0;
      _moduleEffects->read([&](ModuleState& effectsState) {
        for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) {
          if (updatedItem == nodeState["name"]) {
            EXT_LOGD(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str());
            LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleEffects->findLiveScriptNode(nodeState["name"]);
            if (liveScriptNode) {
              liveScriptNode->compileAndRun();
              _moduleEffects->requestUIUpdate = true;  // update the Effects UI
            }
            EXT_LOGD(ML_TAG, "update due to new node %s done", nodeState["name"].as<const char*>());
          }
          index++;
        }
      });
    }
  });
});

AFTER:

_fileManager->addUpdateHandler([this](const String& originId) {
  EXT_LOGV(ML_TAG, "FileManager::updateHandler %s (deferred)", originId.c_str());
  _fileManagerDirty = true;  // Set flag instead of nested reads
});

Add loop() method:

void loop() override {
  Module::loop();
  
  // Process deferred FileManager updates
  if (_fileManagerDirty) {
    _fileManagerDirty = false;
    
    // Safe: both reads execute in ModuleLiveScripts task context
    _fileManager->read([&](FilesState& filesState) {
      for (auto updatedItem : filesState.updatedItems) {
        uint8_t index = 0;
        
        _moduleEffects->read([&](ModuleState& effectsState) {
          for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) {
            if (updatedItem == nodeState["name"]) {
              EXT_LOGD(ML_TAG, "Compiling livescript %s", updatedItem.c_str());
              LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleEffects->findLiveScriptNode(nodeState["name"]);
              if (liveScriptNode) {
                liveScriptNode->compileAndRun();
                _moduleEffects->requestUIUpdate = true;
              }
              EXT_LOGD(ML_TAG, "Update for node %s done", nodeState["name"].as<const char*>());
            }
            index++;
          }
        });
      }
    });
  }
}

3. ModuleLightsControl.h - FileManager Handler

Add private member variables:

private:
    volatile bool _fileManagerDirty = false;
    volatile bool _moduleIODirty = false;

BEFORE (lines 48-67):

_fileManager->addUpdateHandler([this](const String& originId) {
  EXT_LOGV(ML_TAG, "FileManager::updateHandler %s", originId.c_str());
  // read the file state (read all files and folders on FS and collect changes)
  _fileManager->read([&](FilesState& filesState) {
    // loop over all changed files (normally only one)
    bool presetChanged = false;
    for (auto updatedItem : filesState.updatedItems) {
      // if file is the current live script, recompile it (to do: multiple live effects)
      EXT_LOGV(ML_TAG, "updateHandler updatedItem %s", updatedItem.c_str());
      if (strstr(updatedItem.c_str(), "/.config/presets")) {
        EXT_LOGV(ML_TAG, " preset.json updated -> call update %s", updatedItem.c_str());
        presetChanged = true;
      }
    }
    if (presetChanged) {
      EXT_LOGV(ML_TAG, "setPresetsFromFolder");
      setPresetsFromFolder();  // update the presets from the folder
    }
  });
});

AFTER:

_fileManager->addUpdateHandler([this](const String& originId) {
  EXT_LOGV(ML_TAG, "FileManager::updateHandler %s (deferred)", originId.c_str());
  _fileManagerDirty = true;  // Set flag instead of immediate read
});

4. ModuleLightsControl.h - ModuleIO Handler

BEFORE (line 68):

moduleIO.addUpdateHandler([this](const String& originId) { readPins(); }, false);
readPins();  // initially

AFTER:

moduleIO.addUpdateHandler([this](const String& originId) { 
  _moduleIODirty = true;  // Set flag instead of immediate readPins()
}, false);
readPins();  // initially - still safe as it's in setup context

BEFORE readPins() (lines 72-88):

void readPins() {
  moduleIO.read([&](ModuleState& state) {
    pinRelayLightsOn = UINT8_MAX;
    pinButtonLightsOn = UINT8_MAX;
    for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
      uint8_t usage = pinObject["usage"];
      uint8_t gpio = pinObject["GPIO"];
      
      if (usage == pin_Relay_LightsOn) {
        if (GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
          pinRelayLightsOn = gpio;
          pinMode(pinRelayLightsOn, OUTPUT);
          uint8_t newBri = _state.data["lightsOn"] ? _state.data["brightness"] : 0;
          digitalWrite(pinRelayLightsOn, newBri > 0 ? HIGH : LOW);
          EXT_LOGD(ML_TAG, "pinRelayLightsOn found %d", pinRelayLightsOn);
        } else
          EXT_LOGE(MB_TAG, "gpio %d not valid", pinRelayLightsOn);
      }
      // ... rest of pin processing
    }
  });
}

AFTER readPins() (no changes needed - method stays the same):

// readPins() stays unchanged - just call it from loop() instead of handler
void readPins() {
  moduleIO.read([&](ModuleState& state) {
    // ... same implementation ...
  });
}

Modify existing loop() (line 271):

void loop() override {
  Module::loop();
  
  // Process deferred FileManager updates
  if (_fileManagerDirty) {
    _fileManagerDirty = false;
    
    _fileManager->read([&](FilesState& filesState) {
      bool presetChanged = false;
      for (auto updatedItem : filesState.updatedItems) {
        EXT_LOGV(ML_TAG, "Processing updatedItem %s", updatedItem.c_str());
        if (strstr(updatedItem.c_str(), "/.config/presets")) {
          EXT_LOGV(ML_TAG, "preset.json updated");
          presetChanged = true;
        }
      }
      if (presetChanged) {
        EXT_LOGV(ML_TAG, "setPresetsFromFolder");
        setPresetsFromFolder();
      }
    });
  }
  
  // Process deferred ModuleIO updates
  if (_moduleIODirty) {
    _moduleIODirty = false;
    readPins();  // Safe: runs in ModuleLightsControl task context
  }
  
  // ... existing preset loop code ...
  uint8_t presetLoop = _state.data["presetLoop"];
  if (presetLoop && millis() - lastPresetTime > presetLoop * 1000) {
    lastPresetTime = millis();
    // ... rest of existing loop code ...
  }
}

5. ModuleDrivers.h

Add private member variable:

private:
    volatile bool _moduleIODirty = false;
    bool initPins = false;  // Existing variable

BEFORE (line 76):

_moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, false);

AFTER:

_moduleIO->addUpdateHandler([this](const String& originId) { 
  _moduleIODirty = true;  // Set flag instead of immediate readPins()
}, false);

Modify existing loop() (lines 178-188):

BEFORE:

void loop() override {
  NodeManager::loop();
  
  if (!initPins) {
    readPins();  // initially
    initPins = true;
  }
}

AFTER:

void loop() override {
  NodeManager::loop();
  
  // Process deferred ModuleIO updates
  if (_moduleIODirty) {
    _moduleIODirty = false;
    readPins();  // Safe: runs in ModuleDrivers task context
  }
  
  // Initial pin read (keeps existing behavior)
  if (!initPins) {
    readPins();
    initPins = true;
  }
}

Summary of Changes

For each file, the refactoring follows this pattern:

  1. Add volatile bool dirty flags for each external module being accessed
  2. Update handlers set flags instead of calling .read() or other methods
  3. loop() checks flags and processes updates in the module's own task context
  4. Eliminate cross-task mutex acquisition - each task only acquires its own mutex and those of other modules serially within its own execution context

This completely eliminates the circular dependency deadlock because:

  • No nested mutex acquisition in handlers - handlers only set atomic flags
  • Cross-module reads happen in loop() - a single task context acquires mutexes serially (FileManager → Effects) rather than concurrently
  • No Task A holding Mutex 1 while waiting for Mutex 2 scenario

The volatile keyword ensures flag visibility across tasks without requiring mutex protection for the flag itself (it's a simple boolean).

Would you like me to also show how to implement this with a task notification pattern instead of polling flags, which can be more efficient?


🧬 Code Graph Analysis Results

lib/framework/MqttSettingsService.h, lines around 109-120

static StateUpdateResult update(JsonObject &root, MqttSettings &settings, const String &originId)
    {
        settings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
        settings.uri = root["uri"] | FACTORY_MQTT_URI;
        settings.username = root["username"] | SettingValue::format(FACTORY_MQTT_USERNAME);
        settings.password = root["password"] | FACTORY_MQTT_PASSWORD;
        settings.clientId = root["client_id"] | SettingValue::format(FACTORY_MQTT_CLIENT_ID);
        settings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
        settings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
        settings.messageIntervalMs = root["message_interval_ms"] | FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS;
        return StateUpdateResult::CHANGED;
    }

lib/framework/MqttSettingsService.h, lines around 96-107

static void
    read(MqttSettings &settings, JsonObject &root)
    {
        root["enabled"] = settings.enabled;
        root["uri"] = settings.uri;
        root["username"] = settings.username;
        root["password"] = settings.password;
        root["client_id"] = settings.clientId;
        root["keep_alive"] = settings.keepAlive;
        root["clean_session"] = settings.cleanSession;
        root["message_interval_ms"] = settings.messageIntervalMs;
    }

lib/framework/WiFiSettingsService.h, lines around 90-224

static void read(WiFiSettings &settings, JsonObject &root)
    {
        root["hostname"] = settings.hostname;
        root["connection_mode"] = settings.staConnectionMode;
        root["txPower"] = settings.txPower;//(uint8_t )WiFi.getTxPower();
        root["txPowerMeasured"] = abs(WiFi.getTxPower());
        root["trackAnalytics"] = settings.trackAnalytics;

        // create JSON array from root
        JsonArray wifiNetworks = root["wifi_networks"].to<JsonArray>();

        // iterate over the wifiSettings
        for (auto &wifi : settings.wifiSettings)
        {
            // create JSON object for each wifi network
            JsonObject wifiNetwork = wifiNetworks.add<JsonObject>();

            // add the ssid and password to the JSON object
            wifiNetwork["ssid"] = wifi.ssid;
            wifiNetwork["password"] = wifi.password;
            wifiNetwork["static_ip_config"] = wifi.staticIPConfig;

            // extended settings
            JsonUtils::writeIP(wifiNetwork, "local_ip", wifi.localIP);
            JsonUtils::writeIP(wifiNetwork, "gateway_ip", wifi.gatewayIP);
            JsonUtils::writeIP(wifiNetwork, "subnet_mask", wifi.subnetMask);
            JsonUtils::writeIP(wifiNetwork, "dns_ip_1", wifi.dnsIP1);
            JsonUtils::writeIP(wifiNetwork, "dns_ip_2", wifi.dnsIP2);
        }

        ESP_LOGV(SVK_TAG, "WiFi Settings read");
    }

    static StateUpdateResult update(JsonObject &root, WiFiSettings &settings, const String &originId)
    {
        settings.hostname = root["hostname"] | SettingValue::format(FACTORY_WIFI_HOSTNAME);
        settings.staConnectionMode = root["connection_mode"] | 1; // default to STRENGTH

        // 🌙
        #if CONFIG_IDF_TARGET_ESP32C3 | LOLIN_WIFI_FIX
            settings.txPower = root["txPower"] | 34; // default to 8.5dBm
        #else 
            settings.txPower = root["txPower"] | 0; // default (do not set the power)
        #endif

        settings.trackAnalytics = root["trackAnalytics"].isNull()?true:root["trackAnalytics"]; // 🌙 

        settings.wifiSettings.clear();

        // create JSON array from root
        JsonArray wifiNetworks = root["wifi_networks"];
        if (root["wifi_networks"].is<JsonArray>())
        {
            // iterate over the wifiSettings
            int i = 0;
            for (auto wifiNetwork : wifiNetworks)
            {
                // max 5 wifi networks
                if (i++ >= 5)
                {
                    ESP_LOGE(SVK_TAG, "Too many wifi networks");
                    break;
                }

                // create JSON object for each wifi network
                JsonObject wifi = wifiNetwork.as<JsonObject>();

                // Check if SSID length is between 1 and 31 characters and password between 0 and 64 characters
                if (wifi["ssid"].as<String>().length() < 1 || wifi["ssid"].as<String>().length() > 31 || wifi["password"].as<String>().length() > 64)
                {
                    ESP_LOGE(SVK_TAG, "SSID or password length is invalid");
                }
                else
                {
                    // add the ssid and password to the JSON object
                    wifi_settings_t wifiSettings;

                    wifiSettings.ssid = wifi["ssid"].as<String>();
                    wifiSettings.password = wifi["password"].as<String>();
                    wifiSettings.staticIPConfig = wifi["static_ip_config"];

                    // extended settings
                    JsonUtils::readIP(wifi, "local_ip", wifiSettings.localIP);
                    JsonUtils::readIP(wifi, "gateway_ip", wifiSettings.gatewayIP);
                    JsonUtils::readIP(wifi, "subnet_mask", wifiSettings.subnetMask);
                    JsonUtils::readIP(wifi, "dns_ip_1", wifiSettings.dnsIP1);
                    JsonUtils::readIP(wifi, "dns_ip_2", wifiSettings.dnsIP2);

                    // Swap around the dns servers if 2 is populated but 1 is not
                    if (IPUtils::isNotSet(wifiSettings.dnsIP1) && IPUtils::isSet(wifiSettings.dnsIP2))
                    {
                        wifiSettings.dnsIP1 = wifiSettings.dnsIP2;
                        wifiSettings.dnsIP2 = INADDR_NONE;
                    }

                    // Turning off static ip config if we don't meet the minimum requirements
                    // of ipAddress, gateway and subnet. This may change to static ip only
                    // as sensible defaults can be assumed for gateway and subnet
                    if (wifiSettings.staticIPConfig && (IPUtils::isNotSet(wifiSettings.localIP) || IPUtils::isNotSet(wifiSettings.gatewayIP) ||
                                                        IPUtils::isNotSet(wifiSettings.subnetMask)))
                    {
                        wifiSettings.staticIPConfig = false;
                    }

                    // reset scan result
                    wifiSettings.available = false;
                    settings.wifiSettings.push_back(wifiSettings);

                    // increment the wifi network index
                    i++;
                }
            }
        }
        else
        {
            // populate with factory defaults if they are present
            if (String(FACTORY_WIFI_SSID).length() > 0)
            {
                settings.wifiSettings.push_back(wifi_settings_t{
                    .ssid = FACTORY_WIFI_SSID,
                    .password = FACTORY_WIFI_PASSWORD,
                    .staticIPConfig = false,
                    .localIP = INADDR_NONE,
                    .gatewayIP = INADDR_NONE,
                    .subnetMask = INADDR_NONE,
                    .dnsIP1 = INADDR_NONE,
                    .dnsIP2 = INADDR_NONE,
                    .available = false,
                });
            }
        }
        ESP_LOGV(SVK_TAG, "WiFi Settings updated");

        return StateUpdateResult::CHANGED;
    }

lib/framework/APSettingsService.h, lines around 115-144

static StateUpdateResult update(JsonObject &root, APSettings &settings, const String &originId)
    {
        APSettings newSettings = {};
        newSettings.provisionMode = root["provision_mode"] | FACTORY_AP_PROVISION_MODE;
        switch (settings.provisionMode)
        {
        case AP_MODE_ALWAYS:
        case AP_MODE_DISCONNECTED:
        case AP_MODE_NEVER:
            break;
        default:
            newSettings.provisionMode = AP_MODE_DISCONNECTED;
        }
        newSettings.ssid = root["ssid"] | SettingValue::format(FACTORY_AP_SSID);
        newSettings.password = root["password"] | FACTORY_AP_PASSWORD;
        newSettings.channel = root["channel"] | FACTORY_AP_CHANNEL;
        newSettings.ssidHidden = root["ssid_hidden"] | FACTORY_AP_SSID_HIDDEN;
        newSettings.maxClients = root["max_clients"] | FACTORY_AP_MAX_CLIENTS;

        JsonUtils::readIPStr(root, "local_ip", newSettings.localIP, FACTORY_AP_LOCAL_IP);
        JsonUtils::readIPStr(root, "gateway_ip", newSettings.gatewayIP, FACTORY_AP_GATEWAY_IP);
        JsonUtils::readIPStr(root, "subnet_mask", newSettings.subnetMask, FACTORY_AP_SUBNET_MASK);

        if (newSettings == settings)
        {
            return StateUpdateResult::UNCHANGED;
        }
        settings = newSettings;
        return StateUpdateResult::CHANGED;
    }

lib/framework/APSettingsService.h, lines around 102-113

static void read(APSettings &settings, JsonObject &root)
    {
        root["provision_mode"] = settings.provisionMode;
        root["ssid"] = settings.ssid;
        root["password"] = settings.password;
        root["channel"] = settings.channel;
        root["ssid_hidden"] = settings.ssidHidden;
        root["max_clients"] = settings.maxClients;
        root["local_ip"] = settings.localIP.toString();
        root["gateway_ip"] = settings.gatewayIP.toString();
        root["subnet_mask"] = settings.subnetMask.toString();
    }

}

Expand Down
Loading