diff --git a/.gitignore b/.gitignore index 961869c6..aa779260 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.vscode/* +!/.vscode/launch.json *.log diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..907f42ea --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + }, + { + "type": "espidf", + "name": "Launch", + "request": "launch" + }, + { + "name": "Wokwi GDB", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/ugly-duckling.elf", + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "${command:espIdf.getToolchainGdb}", + "miDebuggerServerAddress": "localhost:3333" + } + ] +} diff --git a/README.md b/README.md index 2cd0953a..595021ed 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,22 @@ mkspiffs -c data -s 0x30000 build/data.bin; esptool write_flash 0x3D0000 build/d idf.py monitor ``` +### Simulation + +Can use [Wokwi](https://wokwi.com/) to run the firmware in a simulated environment. +For this the firmware must be built with `-DWOKWI=1`. + +```bash +idf.py -DUD_GEN=MK6 -DUD_DEBUG=0 -DFSUPLOAD=1 -DWOKWI=1 build +``` + +The opening a diagram in the [`wokwi`](wokwi) directory will start the simulation. + +#### Debugging with Wokwi + +To start the simulation with the debugger enabled, place a breakpoint, then hit `Cmd+Shift+P` and select `Wokwi: Start Simulator and Wait for Debugger`. +After that from the "Run and Debug" panel select the "Wokwi GDB" configuration and hit the play button. + ### Testing TBD diff --git a/lookup-backtrace.py b/lookup-backtrace.py new file mode 100755 index 00000000..a1ebb738 --- /dev/null +++ b/lookup-backtrace.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import re +import subprocess +import sys + +# Define regex patterns for the two formats +PATTERN1 = r"caller ((?:0x[0-9a-fA-F]+:)+0x[0-9a-fA-F]+)" +PATTERN2 = r"Backtrace: ((?:0x[0-9a-fA-F]+:[0-9a-fA-F]+ ?)+)" + +def parse_and_run_addr2line(line): + # Print the input line + print(f"Input: {line.strip()}") + + # Check for the first format + match1 = re.search(PATTERN1, line) + if match1: + addresses = match1.group(1).split(":") + process_addresses(addresses) + + # Check for the second format + match2 = re.search(PATTERN2, line) + if match2: + pairs = match2.group(1).split() + addresses = [pair.split(":")[0] for pair in pairs] + process_addresses(addresses) + +def process_addresses(addresses): + # Prepare the addr2line command + cmd = ["xtensa-esp32-elf-addr2line", "-f", "-e", "build/ugly-duckling.elf"] + addresses + try: + # Run the addr2line command + result = subprocess.run(cmd, text=True, capture_output=True) + # Split output into lines + lines = result.stdout.strip().splitlines() + # Process lines in pairs + for i in range(0, len(lines), 2): + function_name = lines[i].strip() if i < len(lines) else "unknown" + source_info = lines[i + 1].strip() if i + 1 < len(lines) else "unknown" + print(f" -- {source_info} -- {function_name}") + except Exception as e: + print(f"Error running addr2line: {e}") + +def main(): + try: + # Read lines from standard input + for line in sys.stdin: + parse_and_run_addr2line(line) + except KeyboardInterrupt: + print("\nTerminated by user.") + except EOFError: + print("\n") + +if __name__ == "__main__": + main() diff --git a/main/devices/Device.hpp b/main/devices/Device.hpp index 04e62441..581a46e5 100644 --- a/main/devices/Device.hpp +++ b/main/devices/Device.hpp @@ -250,7 +250,7 @@ class ConsoleProvider : public LogConsumer { void consumeLog(Level level, const char* message) override { if (level <= recordedLevel) { - logRecords.offer(LogRecord { level, message }); + logRecords.offer(level, message); } #ifdef FARMHUB_DEBUG consolePrinter.printLog(level, message); @@ -573,7 +573,7 @@ class Device { kernel.prepareUpdate(url); } }; - Queue telemetryPublishQueue { "telemetry-publish", 1 }; + CopyQueue telemetryPublishQueue { "telemetry-publish", 1 }; }; } // namespace farmhub::devices diff --git a/main/idf_component.yml b/main/idf_component.yml index 39e5d0ed..7f859505 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,19 +1,7 @@ ## IDF Component Manager Manifest File dependencies: - espressif/arduino-esp32: "^3.1.0-RC1" - espressif/mdns: "^1.4.0" - bblanchon/arduinojson: "^7.2.0" - ## Required IDF version + espressif/arduino-esp32: "3.1.0-rc1" + espressif/mdns: "1.4.2" + bblanchon/arduinojson: "7.2.1" idf: - version: ">=5.3.1,<5.4.0" - # # Put list of dependencies here - # # For components maintained by Espressif: - # component: "~1.0.0" - # # For 3rd party components: - # username/component: ">=1.0.0,<2.0.0" - # username2/component2: - # version: "~1.0.0" - # # For transient dependencies `public` flag can be set. - # # `public` flag doesn't have an effect dependencies of the `main` component. - # # All dependencies of `main` are public by default. - # public: true + version: "5.3.1" diff --git a/main/kernel/Concurrent.hpp b/main/kernel/Concurrent.hpp index b245ec6b..4109da4d 100644 --- a/main/kernel/Concurrent.hpp +++ b/main/kernel/Concurrent.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -65,12 +67,6 @@ class Queue : public BaseQueue { return sentWithoutDropping; } - template - void overwrite(Args&&... args) { - TMessage* copy = new TMessage(std::forward(args)...); - xQueueOverwrite(this->queue, ©); - } - typedef std::function MessageHandler; size_t drain(MessageHandler handler) { @@ -124,13 +120,29 @@ class Queue : public BaseQueue { }; template - class CopyQueue : public BaseQueue { public: CopyQueue(const String& name, size_t capacity = 16) : BaseQueue(name, sizeof(TMessage), capacity) { } + void put(const TMessage message) { + while (!offerIn(ticks::max(), message)) { } + } + + bool offer(const TMessage message) { + return offerIn(ticks::zero(), message); + } + + bool offerIn(ticks timeout, const TMessage message) { + bool sentWithoutDropping = xQueueSend(this->queue, &message, timeout.count()) == pdTRUE; + if (!sentWithoutDropping) { + ESP_LOGW("farmhub", "Overflow in queue '%s', dropping message", + this->name.c_str()); + } + return sentWithoutDropping; + } + bool IRAM_ATTR offerFromISR(const TMessage& message) { BaseType_t xHigherPriorityTaskWoken; bool sentWithoutDropping = xQueueSendFromISR(this->queue, &message, &xHigherPriorityTaskWoken) == pdTRUE; @@ -138,6 +150,10 @@ class CopyQueue : public BaseQueue { return sentWithoutDropping; } + void overwrite(const TMessage message) { + xQueueOverwrite(this->queue, &message); + } + void IRAM_ATTR overwriteFromISR(const TMessage& message) { BaseType_t xHigherPriorityTaskWoken; xQueueOverwriteFromISR(this->queue, &message, &xHigherPriorityTaskWoken); @@ -145,10 +161,24 @@ class CopyQueue : public BaseQueue { } TMessage take() { + while (true) { + auto message = pollIn(ticks::max()); + if (message.has_value()) { + return message.value(); + } + } + } + + std::optional poll() { + return pollIn(ticks::zero()); + } + + std::optional pollIn(ticks timeout) { TMessage message; - while (!xQueueReceive(this->queue, &message, ticks::max().count())) { + if (xQueueReceive(this->queue, &message, timeout.count())) { + return message; } - return message; + return std::nullopt; } void clear() { diff --git a/main/kernel/SleepManager.hpp b/main/kernel/SleepManager.hpp index 9e92dbfc..4925d360 100644 --- a/main/kernel/SleepManager.hpp +++ b/main/kernel/SleepManager.hpp @@ -37,6 +37,10 @@ class SleepManager { #if FARMHUB_DEBUG Log.warn("Light sleep is disabled in debug mode"); return false; +#elif WOKWI + // See https://github.com/wokwi/wokwi-features/issues/922 + Log.warn("Light sleep is disabled when running under Wokwi"); + return false; #elif not(CONFIG_PM_ENABLE) Log.info("Power management is disabled because CONFIG_PM_ENABLE is not set"); return false; diff --git a/main/kernel/Task.hpp b/main/kernel/Task.hpp index d6523e70..ba524d1a 100644 --- a/main/kernel/Task.hpp +++ b/main/kernel/Task.hpp @@ -164,7 +164,7 @@ class Task { return time - (currentTime - ticks(lastWakeTime)); } else { // 'currentTime' has surpassed our target time, indicating the delay has expired. - Log.printfToSerial("Task '%s' missed deadline by %lld ms\n", + Log.printfToSerial("Task '%s' is already past deadline by %lld ms\n", pcTaskGetName(nullptr), duration_cast(currentTime - ticks(lastWakeTime)).count()); return ticks::zero(); } diff --git a/main/kernel/drivers/LedDriver.hpp b/main/kernel/drivers/LedDriver.hpp index 18c8046a..6d8692ce 100644 --- a/main/kernel/drivers/LedDriver.hpp +++ b/main/kernel/drivers/LedDriver.hpp @@ -74,7 +74,7 @@ class LedDriver { } private: - void setPattern(BlinkPattern pattern) { + void setPattern(const BlinkPattern& pattern) { patternQueue.put(pattern); } diff --git a/main/kernel/drivers/WiFiDriver.hpp b/main/kernel/drivers/WiFiDriver.hpp index de9736cd..d2e7f2e3 100644 --- a/main/kernel/drivers/WiFiDriver.hpp +++ b/main/kernel/drivers/WiFiDriver.hpp @@ -112,8 +112,9 @@ class WiFiDriver { inline void runLoop() { int clients = 0; while (true) { - eventQueue.pollIn(WIFI_CHECK_INTERVAL, [&clients](const WiFiEvent event) { - switch (event) { + auto event = eventQueue.pollIn(WIFI_CHECK_INTERVAL); + if (event.has_value()) { + switch (event.value()) { case WiFiEvent::CONNECTED: break; case WiFiEvent::DISCONNECTED: @@ -125,7 +126,7 @@ class WiFiDriver { clients--; break; } - }); + } bool connected = WiFi.isConnected(); if (clients > 0) { @@ -179,7 +180,9 @@ class WiFiDriver { void disconnect() { networkReady.clear(); - WiFi.disconnect(true); + if (!WiFi.disconnect(true)) { + Log.error("WiFi: failed to shut down"); + } } StateSource& acquire() { @@ -205,7 +208,7 @@ class WiFiDriver { WANTS_DISCONNECT }; - Queue eventQueue { "wifi-events", 16 }; + CopyQueue eventQueue { "wifi-events", 16 }; static constexpr milliseconds WIFI_QUEUE_TIMEOUT = 1s; static constexpr milliseconds WIFI_CHECK_INTERVAL = 5s; diff --git a/main/main.cpp b/main/main.cpp index f41b5c53..5f4a7caf 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -4,8 +4,28 @@ #include #include +#include -#include +#ifdef CONFIG_HEAP_TRACING +#include +#include + +#define NUM_RECORDS 64 +static heap_trace_record_t trace_record[NUM_RECORDS]; // This buffer must be in internal RAM + +class HeapTrace { +public: + HeapTrace() { + ESP_ERROR_CHECK(heap_trace_start(HEAP_TRACE_LEAKS)); + } + + ~HeapTrace() { + ESP_ERROR_CHECK(heap_trace_stop()); + heap_trace_dump(); + printf("Free heap: %lu\n", esp_get_free_heap_size()); + } +}; +#endif #ifdef CONFIG_HEAP_TASK_TRACKING #include @@ -18,38 +38,38 @@ static heap_task_totals_t s_totals_arr[MAX_TASK_NUM]; static heap_task_block_t s_block_arr[MAX_BLOCK_NUM]; static void dumpPerTaskHeapInfo() { - heap_task_info_params_t heap_info = { 0 }; - heap_info.caps[0] = MALLOC_CAP_8BIT; // Gets heap with CAP_8BIT capabilities - heap_info.mask[0] = MALLOC_CAP_8BIT; - heap_info.caps[1] = MALLOC_CAP_32BIT; // Gets heap info with CAP_32BIT capabilities - heap_info.mask[1] = MALLOC_CAP_32BIT; - heap_info.tasks = NULL; // Passing NULL captures heap info for all tasks - heap_info.num_tasks = 0; - heap_info.totals = s_totals_arr; // Gets task wise allocation details - heap_info.num_totals = &s_prepopulated_num; - heap_info.max_totals = MAX_TASK_NUM; // Maximum length of "s_totals_arr" - heap_info.blocks = s_block_arr; // Gets block wise allocation details. For each block, gets owner task, address and size - heap_info.max_blocks = MAX_BLOCK_NUM; // Maximum length of "s_block_arr" - - heap_caps_get_per_task_info(&heap_info); - - for (int i = 0; i < *heap_info.num_totals; i++) { - printf("Task: %s -> CAP_8BIT: %d CAP_32BIT: %d\n", - heap_info.totals[i].task ? pcTaskGetName(heap_info.totals[i].task) : "Pre-Scheduler allocs", - heap_info.totals[i].size[0], // Heap size with CAP_8BIT capabilities - heap_info.totals[i].size[1]); // Heap size with CAP32_BIT capabilities + heap_task_info_params_t heapInfo = { + .caps = { MALLOC_CAP_8BIT, MALLOC_CAP_32BIT }, + .mask = { MALLOC_CAP_8BIT, MALLOC_CAP_32BIT }, + .tasks = nullptr, + .num_tasks = 0, + .totals = s_totals_arr, + .num_totals = &s_prepopulated_num, + .max_totals = MAX_TASK_NUM, + .blocks = s_block_arr, + .max_blocks = MAX_BLOCK_NUM + }; + + heap_caps_get_per_task_info(&heapInfo); + + for (int i = 0; i < *heapInfo.num_totals; i++) { + auto taskInfo = heapInfo.totals[i]; + std::string taskName = taskInfo.task + ? pcTaskGetName(taskInfo.task) + : "Pre-Scheduler allocs"; + taskName.resize(configMAX_TASK_NAME_LEN, ' '); + printf("Task %p: %s CAP_8BIT: %d, CAP_32BIT: %d\n", + taskInfo.task, + taskName.c_str(), + taskInfo.size[0], + taskInfo.size[1]); } printf("\n\n"); } #endif -#ifdef CONFIG_HEAP_TRACING -#include "esp_heap_trace.h" - -#define NUM_RECORDS 100 -static heap_trace_record_t trace_record[NUM_RECORDS]; // This buffer must be in internal RAM -#endif +#include extern "C" void app_main() { initArduino(); @@ -60,20 +80,12 @@ extern "C" void app_main() { new farmhub::devices::Device(); - while (true) { -#ifdef CONFIG_HEAP_TRACING #ifdef CONFIG_HEAP_TASK_TRACKING - dumpPerTaskHeapInfo(); + Task::loop("task-heaps", 8192, [](Task& task) { + while (true) { + dumpPerTaskHeapInfo(); + vTaskDelay(ticks(5s).count()); + } + }); #endif - - ESP_ERROR_CHECK(heap_trace_start(HEAP_TRACE_LEAKS)); - - vTaskDelay(5000 / portTICK_PERIOD_MS); - - ESP_ERROR_CHECK(heap_trace_stop()); - heap_trace_dump(); -#else - vTaskDelay(portMAX_DELAY); -#endif - } } diff --git a/wokwi/wokwi.toml b/wokwi/wokwi.toml index 54f63b75..d52b5977 100644 --- a/wokwi/wokwi.toml +++ b/wokwi/wokwi.toml @@ -2,3 +2,4 @@ version = 1 firmware = '../build/flasher_args.json' elf = '../build/ugly-duckling.elf' +gdbServerPort=3333