diff --git a/Datasheet/be729405d0b0e6411fd7e01e69acec34184d17a9e57c43ec413389783c890f71_optim.pdf b/Datasheet/be729405d0b0e6411fd7e01e69acec34184d17a9e57c43ec413389783c890f71_optim.pdf
new file mode 100644
index 0000000..593de36
Binary files /dev/null and b/Datasheet/be729405d0b0e6411fd7e01e69acec34184d17a9e57c43ec413389783c890f71_optim.pdf differ
diff --git a/RotaxMonitor/.gitignore b/RotaxMonitor/.gitignore
index 89cc49c..18aafbe 100644
--- a/RotaxMonitor/.gitignore
+++ b/RotaxMonitor/.gitignore
@@ -3,3 +3,4 @@
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+unpacked_fs
diff --git a/RotaxMonitor/.vscode/extensions.json b/RotaxMonitor/.vscode/extensions.json
index 411655e..b397401 100644
--- a/RotaxMonitor/.vscode/extensions.json
+++ b/RotaxMonitor/.vscode/extensions.json
@@ -1,8 +1,7 @@
{
"recommendations": [
"Jason2866.esp-decoder",
- "pioarduino.pioarduino-ide",
- "platformio.platformio-ide"
+ "pioarduino.pioarduino-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
diff --git a/RotaxMonitor/data/config.json b/RotaxMonitor/data/config.json
new file mode 100644
index 0000000..544b7b4
--- /dev/null
+++ b/RotaxMonitor/data/config.json
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/RotaxMonitor/data/index.html b/RotaxMonitor/data/index.html
new file mode 100644
index 0000000..71aa277
--- /dev/null
+++ b/RotaxMonitor/data/index.html
@@ -0,0 +1,188 @@
+
+
+
+
+
+ Astro Rotax Monitor
+
+
+
+
+
+
+
+ Waiting for data...
+
+
+
+
+
Box_A
+
+
Timestamp: -
+
Data Valid: -
+
Generator voltage: -
+
ADC read time: -
+
Queue errors: -
+
+
+ Engine RPM: -
+
+
+
+
+ | Property |
+ Pickup 12 |
+ Pickup 34 |
+
+
+
+
+ | Spark delay |
+ - |
+ - |
+
+
+ | Spark status |
+ - |
+ - |
+
+
+ | Soft start status |
+ - |
+ - |
+
+
+ | Peak P in |
+ - |
+ - |
+
+
+ | Peak N in |
+ - |
+ - |
+
+
+ | Peak P out |
+ - |
+ - |
+
+
+ | Peak N out |
+ - |
+ - |
+
+
+ | Level spark |
+ - |
+ - |
+
+
+ | Spark Events |
+ - |
+ - |
+
+
+ | Missed Events |
+ - |
+ - |
+
+
+
+
+
+
+
Box_B
+
+
Timestamp: -
+
Data Valid: -
+
Generator voltage: -
+
ADC read time: -
+
Queue errors: -
+
+
+ Engine RPM: -
+
+
+
+
+ | Property |
+ Pickup 12 |
+ Pickup 34 |
+
+
+
+
+ | Spark delay |
+ - |
+ - |
+
+
+ | Spark status |
+ - |
+ - |
+
+
+ | Soft start status |
+ - |
+ - |
+
+
+ | Peak P in |
+ - |
+ - |
+
+
+ | Peak N in |
+ - |
+ - |
+
+
+ | Peak P out |
+ - |
+ - |
+
+
+ | Peak N out |
+ - |
+ - |
+
+
+ | Level spark |
+ - |
+ - |
+
+
+ | Spark Events |
+ - |
+ - |
+
+
+ | Missed Events |
+ - |
+ - |
+
+
+
+
+
+
+
+
Upload file to Flash
+
Select a file and upload it to Flash.
+
+
+
No file uploaded yet.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RotaxMonitor/data/logo_astro_dev.svg b/RotaxMonitor/data/logo_astro_dev.svg
new file mode 100644
index 0000000..f504444
--- /dev/null
+++ b/RotaxMonitor/data/logo_astro_dev.svg
@@ -0,0 +1,306 @@
+
+
+
diff --git a/RotaxMonitor/data/script.js b/RotaxMonitor/data/script.js
new file mode 100644
index 0000000..7ab466c
--- /dev/null
+++ b/RotaxMonitor/data/script.js
@@ -0,0 +1,164 @@
+let ws;
+let lastMessageTimestamp = 0;
+const IDLE_THRESHOLD_MS = 1000;
+const loadingIndicator = document.getElementById("loadingIndicator");
+
+function setLoadingIndicator(visible) {
+ if (!loadingIndicator) {
+ return;
+ }
+
+ loadingIndicator.classList.toggle("hidden", !visible);
+}
+
+function updateLoadingState() {
+ const isConnected = ws && ws.readyState === WebSocket.OPEN;
+ const idle = Date.now() - lastMessageTimestamp >= IDLE_THRESHOLD_MS;
+
+ setLoadingIndicator(isConnected && idle);
+}
+
+function connectWS() {
+ ws = new WebSocket("ws://" + location.host + "/ws");
+
+ ws.onopen = () => {
+ console.log("WebSocket connesso");
+ lastMessageTimestamp = Date.now();
+ setLoadingIndicator(false);
+ };
+
+ ws.onclose = () => {
+ console.log("WebSocket disconnesso, retry...");
+ setLoadingIndicator(false);
+ setTimeout(connectWS, 5000);
+ };
+
+ ws.onmessage = (event) => {
+ let data;
+
+ try {
+ data = JSON.parse(event.data);
+ } catch (e) {
+ console.error("Invalid JSON received", e);
+ return;
+ }
+
+ lastMessageTimestamp = Date.now();
+ setLoadingIndicator(false);
+
+ // Update Box_A
+ if (data.box_a) {
+ const boxA = data.box_a;
+ document.getElementById("a_datavalid").textContent = boxA.datavalid ?? "-";
+ document.getElementById("a_timestamp").textContent = boxA.timestamp ?? "-";
+ document.getElementById("a_volts_gen").textContent = boxA.volts_gen ?? "-";
+ document.getElementById("a_eng_rpm").textContent = boxA.eng_rpm ?? "-";
+ document.getElementById("a_adc_read_time").textContent = boxA.adc_read_time ?? "-";
+ document.getElementById("a_n_queue_errors").textContent = boxA.n_queue_errors ?? "-";
+
+ const coils12A = boxA.coils12 || {};
+ const coils34A = boxA.coils34 || {};
+
+ document.getElementById("a_coils12_spark_delay").textContent = coils12A.spark_delay ?? "-";
+ document.getElementById("a_coils34_spark_delay").textContent = coils34A.spark_delay ?? "-";
+ document.getElementById("a_coils12_spark_status").textContent = coils12A.spark_status ?? "-";
+ document.getElementById("a_coils34_spark_status").textContent = coils34A.spark_status ?? "-";
+ document.getElementById("a_coils12_sstart_status").textContent = coils12A.sstart_status ?? "-";
+ document.getElementById("a_coils34_sstart_status").textContent = coils34A.sstart_status ?? "-";
+ document.getElementById("a_coils12_peak_p_in").textContent = coils12A.peak_p_in ?? "-";
+ document.getElementById("a_coils34_peak_p_in").textContent = coils34A.peak_p_in ?? "-";
+ document.getElementById("a_coils12_peak_n_in").textContent = coils12A.peak_n_in ?? "-";
+ document.getElementById("a_coils34_peak_n_in").textContent = coils34A.peak_n_in ?? "-";
+ document.getElementById("a_coils12_peak_p_out").textContent = coils12A.peak_p_out ?? "-";
+ document.getElementById("a_coils34_peak_p_out").textContent = coils34A.peak_p_out ?? "-";
+ document.getElementById("a_coils12_peak_n_out").textContent = coils12A.peak_n_out ?? "-";
+ document.getElementById("a_coils34_peak_n_out").textContent = coils34A.peak_n_out ?? "-";
+ document.getElementById("a_coils12_level_spark").textContent = coils12A.level_spark ?? "-";
+ document.getElementById("a_coils34_level_spark").textContent = coils34A.level_spark ?? "-";
+ document.getElementById("a_coils12_n_events").textContent = coils12A.n_events ?? "-";
+ document.getElementById("a_coils34_n_events").textContent = coils34A.n_events ?? "-";
+ document.getElementById("a_coils12_n_missed_firing").textContent = coils12A.n_missed_firing ?? "-";
+ document.getElementById("a_coils34_n_missed_firing").textContent = coils34A.n_missed_firing ?? "-";
+ }
+
+ // Update Box_B
+ if (data.box_b) {
+ const boxB = data.box_b;
+ document.getElementById("b_datavalid").textContent = boxB.datavalid ?? "-";
+ document.getElementById("b_timestamp").textContent = boxB.timestamp ?? "-";
+ document.getElementById("b_volts_gen").textContent = boxB.volts_gen ?? "-";
+ document.getElementById("b_eng_rpm").textContent = boxB.eng_rpm ?? "-";
+ document.getElementById("b_adc_read_time").textContent = boxB.adc_read_time ?? "-";
+ document.getElementById("b_n_queue_errors").textContent = boxB.n_queue_errors ?? "-";
+
+ const coils12B = boxB.coils12 || {};
+ const coils34B = boxB.coils34 || {};
+
+ document.getElementById("b_coils12_spark_delay").textContent = coils12B.spark_delay ?? "-";
+ document.getElementById("b_coils34_spark_delay").textContent = coils34B.spark_delay ?? "-";
+ document.getElementById("b_coils12_spark_status").textContent = coils12B.spark_status ?? "-";
+ document.getElementById("b_coils34_spark_status").textContent = coils34B.spark_status ?? "-";
+ document.getElementById("b_coils12_sstart_status").textContent = coils12B.sstart_status ?? "-";
+ document.getElementById("b_coils34_sstart_status").textContent = coils34B.sstart_status ?? "-";
+ document.getElementById("b_coils12_peak_p_in").textContent = coils12B.peak_p_in ?? "-";
+ document.getElementById("b_coils34_peak_p_in").textContent = coils34B.peak_p_in ?? "-";
+ document.getElementById("b_coils12_peak_n_in").textContent = coils12B.peak_n_in ?? "-";
+ document.getElementById("b_coils34_peak_n_in").textContent = coils34B.peak_n_in ?? "-";
+ document.getElementById("b_coils12_peak_p_out").textContent = coils12B.peak_p_out ?? "-";
+ document.getElementById("b_coils34_peak_p_out").textContent = coils34B.peak_p_out ?? "-";
+ document.getElementById("b_coils12_peak_n_out").textContent = coils12B.peak_n_out ?? "-";
+ document.getElementById("b_coils34_peak_n_out").textContent = coils34B.peak_n_out ?? "-";
+ document.getElementById("b_coils12_level_spark").textContent = coils12B.level_spark ?? "-";
+ document.getElementById("b_coils34_level_spark").textContent = coils34B.level_spark ?? "-";
+ document.getElementById("b_coils12_n_events").textContent = coils12B.n_events ?? "-";
+ document.getElementById("b_coils34_n_events").textContent = coils34B.n_events ?? "-";
+ document.getElementById("b_coils12_n_missed_firing").textContent = coils12B.n_missed_firing ?? "-";
+ document.getElementById("b_coils34_n_missed_firing").textContent = coils34B.n_missed_firing ?? "-";
+ }
+ };
+}
+
+function start() {
+ fetch("/start");
+}
+
+function stop() {
+ fetch("/stop");
+}
+
+function uploadLittleFS() {
+ const fileInput = document.getElementById("littlefsFile");
+ const status = document.getElementById("uploadStatus");
+
+ if (!fileInput || fileInput.files.length === 0) {
+ if (status) status.textContent = "Select a file first.";
+ return;
+ }
+
+ const file = fileInput.files[0];
+ const formData = new FormData();
+ formData.append("file", file, file.name);
+
+ if (status) status.textContent = "Uploading...";
+
+ fetch("/upload", {
+ method: "POST",
+ body: formData,
+ })
+ .then((resp) => {
+ if (!resp.ok) {
+ throw new Error("Upload failed: " + resp.status + " " + resp.statusText);
+ }
+ return resp.text();
+ })
+ .then(() => {
+ if (status) status.textContent = "Uploaded: " + file.name;
+ fileInput.value = "";
+ })
+ .catch((err) => {
+ if (status) status.textContent = err.message;
+ });
+}
+
+setInterval(updateLoadingState, 200);
+connectWS();
diff --git a/RotaxMonitor/data/style.css b/RotaxMonitor/data/style.css
new file mode 100644
index 0000000..749df1f
--- /dev/null
+++ b/RotaxMonitor/data/style.css
@@ -0,0 +1,221 @@
+:root {
+ --primary-dark: #0a1929;
+ --primary-blue: #003585;
+ --accent-blue: #1e88e5;
+ --light-bg: #f5f7fa;
+ --border-color: #d0d6dd;
+ --text-dark: #1a1a1a;
+ --text-muted: #666666;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: var(--light-bg);
+ color: var(--text-dark);
+}
+
+.page-header {
+ background: linear-gradient(135deg, var(--primary-dark) 0%, #1a3a52 100%);
+ color: white;
+ padding: 30px 20px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ margin-bottom: 30px;
+}
+
+.header-content {
+ max-width: 900px;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.logo {
+ height: 50px;
+ width: auto;
+ margin: auto;
+}
+
+.page-header h1 {
+ margin: auto;
+ margin-top: 20px;
+ text-align: center;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+table {
+ margin: auto;
+ border-collapse: collapse;
+ width: 100%;
+ max-width: 900px;
+ background: white;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+th, td {
+ border: 1px solid var(--border-color);
+ padding: 12px;
+ font-size: 14px;
+ text-align: center;
+}
+
+th {
+ background-color: var(--primary-blue);
+ color: white;
+ font-weight: 600;
+}
+
+tr:hover {
+ background-color: #f9fbfc;
+}
+
+button {
+ margin: 10px;
+ padding: 10px 20px;
+ font-size: 16px;
+ background-color: var(--primary-blue);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+button:hover {
+ background-color: var(--accent-blue);
+}
+
+.upload-section {
+ margin: 30px auto 20px;
+ max-width: 900px;
+ text-align: left;
+ padding: 20px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ background: white;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.upload-section h3 {
+ margin-top: 0;
+ margin-bottom: 8px;
+ color: var(--primary-blue);
+ font-size: 16px;
+}
+
+.upload-section p {
+ margin: 8px 0;
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+.upload-section input[type="file"] {
+ margin-top: 8px;
+ margin-bottom: 12px;
+}
+
+.upload-status {
+ margin-top: 10px;
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+.loading-indicator {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ margin: 0;
+ padding: 16px 20px;
+ font-size: 20px;
+ color: var(--primary-blue);
+ border-bottom: 1px solid var(--border-color);
+ background: white;
+ width: 100%;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.loading-indicator.hidden {
+ display: none;
+}
+
+.spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid transparent;
+ border-top-color: var(--primary-blue);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.tables-container {
+ display: flex;
+ gap: 20px;
+ max-width: 1800px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+.box {
+ flex: 1;
+ background: white;
+ padding: 20px;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.box h2 {
+ margin-top: 0;
+ margin-bottom: 16px;
+ color: var(--primary-blue);
+ font-size: 18px;
+ font-weight: 700;
+ text-align: center;
+}
+
+.box-data {
+ margin-bottom: 20px;
+}
+
+.box-data p {
+ margin: 8px 0;
+ font-size: 14px;
+}
+
+.box-data strong {
+ color: var(--primary-blue);
+}
+
+.rpm-highlight {
+ background: #c6e4fa;
+ border: 3px double var(--primary-blue);
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin-bottom: 20px;
+ text-align: center;
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--text-dark);
+}
+
+.rpm-highlight strong {
+ color: var(--primary-blue);
+}
+
+span {
+ color: var(--text-dark);
+}
diff --git a/RotaxMonitor/src/ADS1256.cpp b/RotaxMonitor/lib/ADS1256/ADS1256.cpp
similarity index 100%
rename from RotaxMonitor/src/ADS1256.cpp
rename to RotaxMonitor/lib/ADS1256/ADS1256.cpp
diff --git a/RotaxMonitor/src/ADS1256.h b/RotaxMonitor/lib/ADS1256/ADS1256.h
similarity index 100%
rename from RotaxMonitor/src/ADS1256.h
rename to RotaxMonitor/lib/ADS1256/ADS1256.h
diff --git a/RotaxMonitor/src/AD5292.cpp b/RotaxMonitor/lib/ADS5292/AD5292.cpp
similarity index 100%
rename from RotaxMonitor/src/AD5292.cpp
rename to RotaxMonitor/lib/ADS5292/AD5292.cpp
diff --git a/RotaxMonitor/src/AD5292.h b/RotaxMonitor/lib/ADS5292/AD5292.h
similarity index 100%
rename from RotaxMonitor/src/AD5292.h
rename to RotaxMonitor/lib/ADS5292/AD5292.h
diff --git a/RotaxMonitor/lib/led/led.cpp b/RotaxMonitor/lib/led/led.cpp
new file mode 100644
index 0000000..9a93a85
--- /dev/null
+++ b/RotaxMonitor/lib/led/led.cpp
@@ -0,0 +1,32 @@
+#include
+
+RGBled::RGBled(const uint8_t pin) : m_led(pin)
+{
+ pinMode(m_led, OUTPUT);
+ writeStatus(RGBled::ERROR);
+}
+
+RGBled::~RGBled()
+{
+ pinMode(m_led, INPUT);
+}
+
+void RGBled::setStatus(const LedStatus s)
+{
+ if (m_status == s)
+ return;
+ std::lock_guard lock(m_mutex);
+ m_status = s;
+ writeStatus(m_status);
+}
+
+const RGBled::LedStatus RGBled::getSatus(void)
+{
+ return m_status;
+}
+
+void RGBled::writeStatus(const RGBled::LedStatus s)
+{
+ RGBled::color_u u{.status = s};
+ rgbLedWrite(m_led, u.color.r, u.color.g, u.color.b);
+}
diff --git a/RotaxMonitor/lib/led/led.h b/RotaxMonitor/lib/led/led.h
new file mode 100644
index 0000000..a6da94e
--- /dev/null
+++ b/RotaxMonitor/lib/led/led.h
@@ -0,0 +1,63 @@
+#pragma once
+
+// System Inlcudes
+#include
+#include
+
+#define RED 0x00FF00
+#define GREEN 0xFF0000
+#define BLUE 0x0000FF
+#define WHITE 0xFFFFFF
+#define YELLOW 0xFFFF00
+#define CYAN 0xFF00FF
+#define MAGENTA 0x00FFFF
+#define ORANGE 0xA5FF00
+#define PURPLE 0x008080
+#define PINK 0x69FFB4
+#define LIME 0xCD3232
+#define SKY_BLUE 0xCE87EB
+#define GOLD 0xD7FF00
+#define TURQUOISE 0xE040D0
+#define INDIGO 0x004B82
+#define GRAY 0x808080
+
+class RGBled
+{
+public:
+ enum LedStatus
+ {
+ OK = GREEN,
+ ERROR = RED,
+ INIT = YELLOW,
+ DATA_A = CYAN,
+ DATA_B = MAGENTA,
+ DATA_ALL = ORANGE,
+ IDLE = GRAY
+ };
+
+ struct color_t
+ {
+ uint8_t a, g, r, b;
+ };
+
+ union color_u
+ {
+ uint32_t status;
+ color_t color;
+ };
+
+public:
+ RGBled(const uint8_t pin = 48);
+ ~RGBled();
+
+ void setStatus(const LedStatus s);
+ const LedStatus getSatus(void);
+
+private:
+ void writeStatus(const LedStatus s);
+
+private:
+ LedStatus m_status = LedStatus::IDLE;
+ std::mutex m_mutex;
+ const uint8_t m_led;
+};
\ No newline at end of file
diff --git a/RotaxMonitor/partitions/default_16MB.csv b/RotaxMonitor/partitions/default_16MB.csv
deleted file mode 100644
index de523c3..0000000
--- a/RotaxMonitor/partitions/default_16MB.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-# Name, Type, SubType, Offset, Size, Flags
-nvs, data, nvs, 0x9000, 0x5000,
-otadata, data, ota, 0xe000, 0x2000,
-app0, app, ota_0, 0x10000, 0x700000,
-app1, app, ota_1, 0x710000,0x700000,
-spiffs, data, spiffs, 0xE10000,0x1F0000,
diff --git a/RotaxMonitor/partitions/no_ota_10mb_littlefs.csv b/RotaxMonitor/partitions/no_ota_10mb_littlefs.csv
new file mode 100644
index 0000000..b449d64
--- /dev/null
+++ b/RotaxMonitor/partitions/no_ota_10mb_littlefs.csv
@@ -0,0 +1,6 @@
+# ESP32 Partition Table
+# Name, Type, SubType, Offset, Size
+nvs, data, nvs, 0x9000, 0x4000
+phy_init, data, phy, 0xd000, 0x1000
+factory, app, factory, 0x10000, 0x300000
+littlefs, data, littlefs, 0x310000, 0xCF0000
diff --git a/RotaxMonitor/partitions/no_ota_10mb_spiffs.csv b/RotaxMonitor/partitions/no_ota_10mb_spiffs.csv
new file mode 100644
index 0000000..c585eb5
--- /dev/null
+++ b/RotaxMonitor/partitions/no_ota_10mb_spiffs.csv
@@ -0,0 +1,6 @@
+# ESP32 Partition Table
+# Name, Type, SubType, Offset, Size
+nvs, data, nvs, 0x9000, 0x4000
+phy_init, data, phy, 0xd000, 0x1000
+factory, app, factory, 0x10000, 0x300000
+spiffs, data, spiffs, 0x310000, 0xCF0000
\ No newline at end of file
diff --git a/RotaxMonitor/platformio.ini b/RotaxMonitor/platformio.ini
index 117d9d2..e7fb4f6 100644
--- a/RotaxMonitor/platformio.ini
+++ b/RotaxMonitor/platformio.ini
@@ -10,59 +10,61 @@
[env:esp32-s3-devkitc1-n16r8]
board = esp32-s3-devkitc1-n16r8
+board_build.partitions = partitions/no_ota_10mb_littlefs.csv
+board_build.filesystem = littlefs
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
framework = arduino
lib_deps =
hideakitai/DebugLog@^0.8.4
bblanchon/ArduinoJson@^7.4.2
hideakitai/PCA95x5@^0.1.3
- adafruit/Adafruit SSD1306@^2.5.16
- garfius/Menu-UI@^1.2.0
-
-;Upload protocol configuration
+ me-no-dev/AsyncTCP@^3.3.2
+ me-no-dev/ESPAsyncWebServer@^3.6.0
+ adafruit/Adafruit NeoPixel@^1.15.4
upload_protocol = esptool
-upload_port = /dev/ttyACM2
+upload_port = /dev/ttyACM1
upload_speed = 921600
-
-;Monitor configuration
-monitor_port = /dev/ttyACM2
+monitor_port = /dev/ttyACM0
monitor_speed = 921600
-
-; Build configuration
build_type = release
build_flags =
- -DARDUINO_USB_CDC_ON_BOOT=0
- -DARDUINO_USB_MODE=0
- -fstack-protector-all
- -DCONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=1
- -DCONFIG_FREERTOS_USE_TRACE_FACILITY=1
+ -DCORE_DEBUG_LEVEL=1
+ -DARDUINO_USB_CDC_ON_BOOT=0
+ -DARDUINO_USB_MODE=0
+ -DCONFIG_ASYNC_TCP_MAX_ACK_TIME=5000
+ -DCONFIG_ASYNC_TCP_PRIORITY=21
+ -DCONFIG_ASYNC_TCP_QUEUE_SIZE=64
+ -DCONFIG_ASYNC_TCP_RUNNING_CORE=1
+ -DCONFIG_ASYNC_TCP_STACK_SIZE=4096
+ -fstack-protector-all
[env:esp32-s3-devkitc1-n16r8-debug]
board = ${env:esp32-s3-devkitc1-n16r8.board}
+board_build.partitions = ${env:esp32-s3-devkitc1-n16r8.board_build.partitions}
+board_build.filesystem = ${env:esp32-s3-devkitc1-n16r8.board_build.filesystem}
platform = ${env:esp32-s3-devkitc1-n16r8.platform}
framework = ${env:esp32-s3-devkitc1-n16r8.framework}
-lib_deps = ${env:esp32-s3-devkitc1-n16r8.lib_deps}
-
-;Upload protocol configuration
+lib_deps =
+ ${env:esp32-s3-devkitc1-n16r8.lib_deps}
+ adafruit/Adafruit NeoPixel@^1.15.4
upload_protocol = esptool
-upload_port = /dev/ttyACM2
+upload_port = /dev/ttyACM1
upload_speed = 921600
-
-;Monitor configuration
-monitor_port = /dev/ttyACM2
+monitor_port = /dev/ttyACM0
monitor_speed = 921600
-
-; Debug configuration
debug_tool = esp-builtin
debug_speed = 15000
-
-; Build configuration
build_type = debug
build_flags =
- -O0
- -g3
- -ggdb3
- -DCORE_DEBUG_LEVEL=5
- -DARDUINO_USB_CDC_ON_BOOT=0
- -DARDUINO_USB_MODE=0
- -fstack-protector-all
+ -O0
+ -g3
+ -ggdb3
+ -DCORE_DEBUG_LEVEL=3
+ -DARDUINO_USB_CDC_ON_BOOT=0
+ -DARDUINO_USB_MODE=0
+ -DCONFIG_ASYNC_TCP_MAX_ACK_TIME=5000
+ -DCONFIG_ASYNC_TCP_PRIORITY=21
+ -DCONFIG_ASYNC_TCP_QUEUE_SIZE=64
+ -DCONFIG_ASYNC_TCP_RUNNING_CORE=1
+ -DCONFIG_ASYNC_TCP_STACK_SIZE=4096
+ -fstack-protector-all
diff --git a/RotaxMonitor/src/channels.h b/RotaxMonitor/src/channels.h
deleted file mode 100644
index f305685..0000000
--- a/RotaxMonitor/src/channels.h
+++ /dev/null
@@ -1,11 +0,0 @@
-// ADC Channels
-
-#define A1_RAW 0
-#define A2_RAW 1
-#define B1_RAW 2
-#define B2_RAW 3
-
-#define A1_COND 4
-#define A2_COND 5
-#define B1_COND 6
-#define B2_COND 7
diff --git a/RotaxMonitor/src/datasave.cpp b/RotaxMonitor/src/datasave.cpp
new file mode 100644
index 0000000..49b9c81
--- /dev/null
+++ b/RotaxMonitor/src/datasave.cpp
@@ -0,0 +1,221 @@
+#include "datasave.h"
+#include
+
+static const size_t min_free = 1024 * 1024; // minimum free space in LittleFS to allow saving history (1MB)
+
+LITTLEFSGuard::LITTLEFSGuard()
+{
+ if (!LittleFS.begin(true, "/littlefs", 10, "littlefs"))
+ {
+ LOG_ERROR("Failed to mount LittleFS");
+ }
+ else
+ {
+ LOG_INFO("LittleFS mounted successfully");
+ LOG_INFO("LittleFS Free KBytes:", (LittleFS.totalBytes() - LittleFS.usedBytes()) /1024);
+ }
+}
+
+LITTLEFSGuard::~LITTLEFSGuard()
+{
+ LittleFS.end();
+ LOG_INFO("LittleFS unmounted successfully");
+}
+
+void ignitionBoxStatusAverage::filter(int32_t &old, const int32_t value, const uint32_t k)
+{
+ float alpha = 1.0f / (float)k;
+ old = old + (int32_t)(alpha * (float)(value - old));
+}
+
+void ignitionBoxStatusAverage::filter(float &old, const float value, const uint32_t k)
+{
+ float alpha = 1.0f / (float)k;
+ old = old + (float)(alpha * (float)(value - old));
+}
+
+void ignitionBoxStatusAverage::reset()
+{
+ m_last = ignitionBoxStatus();
+ m_count = 0;
+ m_data_valid = false;
+}
+
+void ignitionBoxStatusAverage::update(const ignitionBoxStatus &new_status)
+{
+ if (m_count == 0 && !m_data_valid)
+ {
+ m_last = new_status;
+ }
+ m_count++;
+ // simple moving average calculation
+ m_last.timestamp = new_status.timestamp; // keep timestamp of latest status
+
+ m_last.coils12.n_events = new_status.coils12.n_events; // sum events instead of averaging
+ m_last.coils12.n_missed_firing = new_status.coils12.n_missed_firing; // sum missed firings instead of averaging
+ m_last.coils12.spark_status = new_status.coils12.spark_status; // take latest spark status
+ m_last.coils12.sstart_status = new_status.coils12.sstart_status; // take latest soft start status
+ filter(m_last.coils12.spark_delay, new_status.coils12.spark_delay, m_max_count); // incremental average calculation
+ filter(m_last.coils12.peak_p_in, new_status.coils12.peak_p_in, m_max_count); // incremental average calculation
+ filter(m_last.coils12.peak_n_in, new_status.coils12.peak_n_in, m_max_count); // incremental average calculation
+ filter(m_last.coils12.peak_p_out, new_status.coils12.peak_p_out, m_max_count); // incremental average calculation
+ filter(m_last.coils12.peak_n_out, new_status.coils12.peak_n_out, m_max_count); // incremental average calculation
+
+ m_last.coils34.n_events = new_status.coils34.n_events; // sum events instead of averaging
+ m_last.coils34.n_missed_firing = new_status.coils34.n_missed_firing; // sum missed firings instead of averaging
+ m_last.coils34.spark_status = new_status.coils34.spark_status; // take latest spark status
+ m_last.coils34.sstart_status = new_status.coils34.sstart_status; // take latest soft start status
+ filter(m_last.coils34.spark_delay, new_status.coils34.spark_delay, m_max_count); // incremental average calculation
+ filter(m_last.coils34.peak_p_in, new_status.coils34.peak_p_in, m_max_count); // incremental average calculation
+ filter(m_last.coils34.peak_n_in, new_status.coils34.peak_n_in, m_max_count); // incremental average calculation
+ filter(m_last.coils34.peak_p_out, new_status.coils34.peak_p_out, m_max_count); // incremental average calculation
+ filter(m_last.coils34.peak_n_out, new_status.coils34.peak_n_out, m_max_count); // incremental average calculation
+ filter(m_last.eng_rpm, new_status.eng_rpm, m_max_count); // incremental average calculation // incremental average calculation
+ filter(m_last.adc_read_time, m_last.adc_read_time, m_max_count); // incremental average calculation
+ m_last.n_queue_errors = new_status.n_queue_errors; // take last of queue errors since it's a cumulative count of errors in the queue, not an average value
+
+ if (m_count >= m_max_count)
+ {
+ m_count = 0; // reset count after reaching max samples to average
+ m_data_valid = true; // set data valid flag after first average is calculated
+ }
+}
+
+const bool ignitionBoxStatusAverage::get(ignitionBoxStatus &status) const
+{
+ if (m_data_valid)
+ {
+ status = m_last;
+ }
+ return m_data_valid;
+}
+
+const ArduinoJson::JsonDocument ignitionBoxStatusAverage::toJson() const
+{
+ ArduinoJson::JsonDocument doc;
+ if (m_data_valid)
+ {
+ doc["timestamp"] = m_last.timestamp;
+ doc["datavalid"] = m_data_valid ? "TRUE" : "FALSE";
+
+ doc["coils12"]["n_events"] = m_last.coils12.n_events;
+ doc["coils12"]["n_missed_firing"] = m_last.coils12.n_missed_firing;
+ doc["coils12"]["spark_delay"] = m_last.coils12.spark_delay;
+ doc["coils12"]["spark_status"] = sparkStatusNames.at(m_last.coils12.spark_status);
+ doc["coils12"]["peak_p_in"] = m_last.coils12.peak_p_in;
+ doc["coils12"]["peak_n_in"] = m_last.coils12.peak_n_in;
+ doc["coils12"]["peak_p_out"] = m_last.coils12.peak_p_out;
+ doc["coils12"]["peak_n_out"] = m_last.coils12.peak_n_out;
+ doc["coils12"]["sstart_status"] = softStartStatusNames.at(m_last.coils12.sstart_status);
+
+ doc["coils34"]["n_events"] = m_last.coils34.n_events;
+ doc["coils34"]["n_missed_firing"] = m_last.coils34.n_missed_firing;
+ doc["coils34"]["spark_delay"] = m_last.coils34.spark_delay;
+ doc["coils34"]["spark_status"] = sparkStatusNames.at(m_last.coils34.spark_status);
+ doc["coils34"]["peak_p_in"] = m_last.coils34.peak_p_in;
+ doc["coils34"]["peak_n_in"] = m_last.coils34.peak_n_in;
+ doc["coils34"]["peak_p_out"] = m_last.coils34.peak_p_out;
+ doc["coils34"]["peak_n_out"] = m_last.coils34.peak_n_out;
+ doc["coils34"]["sstart_status"] = softStartStatusNames.at(m_last.coils34.sstart_status);
+
+ doc["eng_rpm"] = m_last.eng_rpm;
+ doc["adc_read_time"] = m_last.adc_read_time;
+ doc["n_queue_errors"] = m_last.n_queue_errors;
+ }
+ return doc;
+}
+
+void saveHistoryTask(void *pvParameters)
+{
+ const auto *params = static_cast(pvParameters);
+ const auto &history = *params->history;
+ const auto &file_path = params->file_path;
+ if (!params)
+ {
+ LOG_ERROR("Invalid parameters for saveHistoryTask");
+ return;
+ }
+ LOG_DEBUG("Starting saving: ", file_path.c_str());
+ save_history(history, file_path);
+ vTaskDelete(NULL);
+}
+
+void save_history(const PSRAMVector &history, const std::filesystem::path &file_name)
+{
+ // Initialize SPIFFS
+ if (!SAVE_HISTORY_TO_LITTLEFS)
+ return;
+
+ auto littlefs_guard = LITTLEFSGuard(); // use RAII guard to ensure LittleFS is properly mounted and unmounted
+
+ if (LittleFS.totalBytes() - LittleFS.usedBytes() < min_free) // check if at least 1MB is free for saving history
+ {
+ LOG_ERROR("Not enough space in SPIFFS to save history");
+ return;
+ }
+
+ std::filesystem::path file_path = file_name;
+ if (file_name.root_path() != "/littlefs")
+ file_path = std::filesystem::path("/littlefs") / file_name;
+
+ auto save_flags = std::ios::out;
+ if (first_save && LittleFS.exists(file_path.c_str()))
+ {
+ first_save = false;
+ save_flags |= std::ios::trunc; // overwrite existing file
+ LittleFS.remove(file_path.c_str()); // ensure file is removed before saving to avoid issues with appending to existing file in SPIFFS
+ LOG_INFO("Saving history to LittleFS, new file:", file_path.c_str());
+ }
+ else
+ {
+ save_flags |= std::ios::app; // append to new file
+ LOG_INFO("Saving history to LittleFS, appending to existing file:", file_path.c_str());
+ }
+
+ std::ofstream ofs(file_path, save_flags);
+ if (ofs.fail())
+ {
+ LOG_ERROR("Failed to open file for writing");
+ return;
+ }
+
+ // write csv header
+ if (first_save)
+ {
+ ofs << "TS,\
+ EVENTS_12,DLY_12,STAT_12,V_12_1,V_12_2,V_12_3,V_12_4,IGNITION_MODE_12,\
+ EVENTS_34,DLY_34,STAT_34,V_34_1,V_34_2,V_34_3,V_34_4,IGNITION_MODE_34,\
+ ENGINE_RPM,ADC_READTIME,N_QUEUE_ERRORS"
+ << std::endl;
+ ofs.flush();
+ }
+
+ for (const auto &entry : history)
+ {
+ ofs << std::to_string(entry.timestamp) << ","
+ << std::to_string(entry.coils12.n_events) << ","
+ << std::to_string(entry.coils12.spark_delay) << ","
+ << std::string(sparkStatusNames.at(entry.coils12.spark_status)) << ","
+ << std::to_string(entry.coils12.peak_p_in) << ","
+ << std::to_string(entry.coils12.peak_n_in) << ","
+ << std::to_string(entry.coils12.peak_p_out) << ","
+ << std::to_string(entry.coils12.peak_n_out) << ","
+ << std::string(softStartStatusNames.at(entry.coils12.sstart_status)) << ","
+ << std::to_string(entry.coils34.n_events) << ","
+ << std::to_string(entry.coils34.spark_delay) << ","
+ << std::string(sparkStatusNames.at(entry.coils34.spark_status)) << ","
+ << std::to_string(entry.coils34.peak_p_in) << ","
+ << std::to_string(entry.coils34.peak_n_in) << ","
+ << std::to_string(entry.coils34.peak_p_out) << ","
+ << std::to_string(entry.coils34.peak_n_out) << ","
+ << std::string(softStartStatusNames.at(entry.coils34.sstart_status)) << ","
+ << std::to_string(entry.eng_rpm) << ","
+ << std::to_string(entry.adc_read_time) << ","
+ << std::to_string(entry.n_queue_errors);
+ ofs << std::endl;
+ ofs.flush();
+ }
+
+ ofs.close();
+ LOG_INFO("Ignition A history saved to LittleFS, records written: ", history.size());
+}
diff --git a/RotaxMonitor/src/datasave.h b/RotaxMonitor/src/datasave.h
new file mode 100644
index 0000000..d834308
--- /dev/null
+++ b/RotaxMonitor/src/datasave.h
@@ -0,0 +1,62 @@
+#pragma once
+#define DEBUGLOG_DEFAULT_LOG_LEVEL_INFO
+
+// System Includes
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Project Includes
+#include "isr.h"
+#include "psvector.h"
+
+const uint32_t max_history = 256;
+const bool SAVE_HISTORY_TO_LITTLEFS = false; // Set to true to enable saving history to LittleFS, false to disable
+static bool first_save = true; // flag to indicate if this is the first save (to write header)
+
+struct dataSaveParams
+{
+ const PSRAMVector *history;
+ const std::filesystem::path file_path;
+};
+
+class LITTLEFSGuard
+{
+public:
+ LITTLEFSGuard();
+ ~LITTLEFSGuard();
+};
+
+class ignitionBoxStatusAverage
+{
+private:
+ ignitionBoxStatus m_last;
+ uint32_t m_count = 0;
+ uint32_t m_max_count = 100; // number of samples to average before resetting
+ bool m_data_valid = false; // flag to indicate if the average data is valid (i.e. at least one sample has been added)
+
+public:
+ ignitionBoxStatusAverage() = default;
+ ignitionBoxStatusAverage(const uint32_t max_count) : m_max_count(max_count)
+ {
+ m_data_valid = false;
+ m_count = 0;
+ }
+
+ void reset();
+ void update(const ignitionBoxStatus &new_status);
+ const bool get(ignitionBoxStatus &status) const;
+ const ArduinoJson::JsonDocument toJson() const;
+
+private:
+ void filter(int32_t &old, const int32_t value, const uint32_t k);
+ void filter(float &old, const float value, const uint32_t k);
+};
+
+// Task and function declarations
+void saveHistoryTask(void *pvParameters);
+void save_history(const PSRAMVector &history, const std::filesystem::path &file_path);
diff --git a/RotaxMonitor/src/datastruct.h b/RotaxMonitor/src/datastruct.h
new file mode 100644
index 0000000..5ded4d2
--- /dev/null
+++ b/RotaxMonitor/src/datastruct.h
@@ -0,0 +1,95 @@
+#pragma once
+#include
+#include