Compare commits

14 Commits

Author SHA1 Message Date
Emanuele Trabattoni
97bce90ba6 changed partition to littlefs, not working yet 2026-04-08 17:10:30 +02:00
Emanuele Trabattoni
12e1e8e7a4 Fixed Filters, split file in html, script and css 2026-04-08 16:55:03 +02:00
Emanuele Trabattoni
4dc45954e9 Webpage is OK, html in memory since SPIFFS is to slow.
Moving average to be fixed
2026-04-08 15:23:21 +02:00
Emanuele Trabattoni
07eb06f67b Save History Sync on flash, still some issues in deleting previous file 2026-04-08 10:27:18 +02:00
Emanuele Trabattoni
481f12f526 Enable/Disable pickup simulation 2026-04-08 10:26:44 +02:00
Emanuele Trabattoni
7c96101cdd SPIFFS mount sometimes fail 2026-04-07 17:33:08 +02:00
Emanuele Trabattoni
877236ee4e Save files appending on same session and new file on new session 2026-04-07 15:53:52 +02:00
Emanuele Trabattoni
668b590d7c CSV file save on SPIFF filesystem, 10MB on internal flash 2026-04-07 13:21:27 +02:00
Emanuele Trabattoni
f36cb96f21 Improved task code 2026-04-07 10:51:53 +02:00
Emanuele Trabattoni
dc44decd64 Moved files in libraries 2026-04-07 10:19:28 +02:00
Emanuele Trabattoni
0d0db29bba Merge branch 'adc' 2026-04-07 10:12:50 +02:00
5c9ef7e93b Fast ADC readings ok, to verify timing and settling time 2026-04-05 17:05:10 +02:00
a2d0afa0c9 adc read ok, very slow 2026-04-05 11:16:10 +02:00
1109681eb5 Merge branch 'debug' 2026-04-05 10:32:08 +02:00
28 changed files with 1121 additions and 337 deletions

View File

@@ -3,3 +3,4 @@
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
unpacked_fs

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32 Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h2>RotaxMonitor realtime data</h2>
<button onclick="start()">Start</button>
<button onclick="stop()">Stop</button>
<div style="max-width: 900px; margin: 0 auto; text-align: left;">
<p><strong>Timestamp:</strong> <span id="timestamp">-</span></p>
<p><strong>Data Valid:</strong> <span id="datavalid">-</span></p>
<p><strong>Generator voltage:</strong> <span id="volts_gen">-</span></p>
<p><strong>Engine RPM:</strong> <span id="eng_rpm">-</span></p>
<p><strong>ADC read time:</strong> <span id="adc_read_time">-</span></p>
<p><strong>Queue errors:</strong> <span id="n_queue_errors">-</span></p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Coils 12</th>
<th>Coils 34</th>
</tr>
</thead>
<tbody>
<tr>
<td>Spark delay</td>
<td id="coils12_spark_delay">-</td>
<td id="coils34_spark_delay">-</td>
</tr>
<tr>
<td>Spark status</td>
<td id="coils12_spark_status">-</td>
<td id="coils34_spark_status">-</td>
</tr>
<tr>
<td>Soft start status</td>
<td id="coils12_sstart_status">-</td>
<td id="coils34_sstart_status">-</td>
</tr>
<tr>
<td>Peak P in</td>
<td id="coils12_peak_p_in">-</td>
<td id="coils34_peak_p_in">-</td>
</tr>
<tr>
<td>Peak N in</td>
<td id="coils12_peak_n_in">-</td>
<td id="coils34_peak_n_in">-</td>
</tr>
<tr>
<td>Peak P out</td>
<td id="coils12_peak_p_out">-</td>
<td id="coils34_peak_p_out">-</td>
</tr>
<tr>
<td>Peak N out</td>
<td id="coils12_peak_n_out">-</td>
<td id="coils34_peak_n_out">-</td>
</tr>
<tr>
<td>Level spark</td>
<td id="coils12_level_spark">-</td>
<td id="coils34_level_spark">-</td>
</tr>
<tr>
<td>Events</td>
<td id="coils12_n_events">-</td>
<td id="coils34_n_events">-</td>
</tr>
<tr>
<td>Missed firings</td>
<td id="coils12_n_missed_firing">-</td>
<td id="coils34_n_missed_firing">-</td>
</tr>
</tbody>
</table>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
let ws;
function connectWS() {
ws = new WebSocket("ws://" + location.host + "/ws");
ws.onopen = () => {
console.log("WebSocket connesso");
};
ws.onclose = () => {
console.log("WebSocket disconnesso, retry...");
setTimeout(connectWS, 5000);
};
ws.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
console.error("Invalid JSON received", e);
return;
}
document.getElementById("datavalid").textContent = data.datavalid ?? "-";
document.getElementById("timestamp").textContent = data.timestamp ?? "-";
document.getElementById("volts_gen").textContent = data.volts_gen ?? "-";
document.getElementById("eng_rpm").textContent = data.eng_rpm ?? "-";
document.getElementById("adc_read_time").textContent = data.adc_read_time ?? "-";
document.getElementById("n_queue_errors").textContent = data.n_queue_errors ?? "-";
const coils12 = data.coils12 || {};
const coils34 = data.coils34 || {};
document.getElementById("coils12_spark_delay").textContent = coils12.spark_delay ?? "-";
document.getElementById("coils34_spark_delay").textContent = coils34.spark_delay ?? "-";
document.getElementById("coils12_spark_status").textContent = coils12.spark_status ?? "-";
document.getElementById("coils34_spark_status").textContent = coils34.spark_status ?? "-";
document.getElementById("coils12_sstart_status").textContent = coils12.sstart_status ?? "-";
document.getElementById("coils34_sstart_status").textContent = coils34.sstart_status ?? "-";
document.getElementById("coils12_peak_p_in").textContent = coils12.peak_p_in ?? "-";
document.getElementById("coils34_peak_p_in").textContent = coils34.peak_p_in ?? "-";
document.getElementById("coils12_peak_n_in").textContent = coils12.peak_n_in ?? "-";
document.getElementById("coils34_peak_n_in").textContent = coils34.peak_n_in ?? "-";
document.getElementById("coils12_peak_p_out").textContent = coils12.peak_p_out ?? "-";
document.getElementById("coils34_peak_p_out").textContent = coils34.peak_p_out ?? "-";
document.getElementById("coils12_peak_n_out").textContent = coils12.peak_n_out ?? "-";
document.getElementById("coils34_peak_n_out").textContent = coils34.peak_n_out ?? "-";
document.getElementById("coils12_level_spark").textContent = coils12.level_spark ?? "-";
document.getElementById("coils34_level_spark").textContent = coils34.level_spark ?? "-";
document.getElementById("coils12_n_events").textContent = coils12.n_events ?? "-";
document.getElementById("coils34_n_events").textContent = coils34.n_events ?? "-";
document.getElementById("coils12_n_missed_firing").textContent = coils12.n_missed_firing ?? "-";
document.getElementById("coils34_n_missed_firing").textContent = coils34.n_missed_firing ?? "-";
};
}
function start() {
fetch("/start");
}
function stop() {
fetch("/stop");
}
connectWS();

View File

@@ -0,0 +1,29 @@
body {
font-family: Arial;
text-align: center;
margin-top: 40px;
}
table {
margin: auto;
border-collapse: collapse;
width: 100%;
max-width: 900px;
}
th, td {
border: 1px solid #ccc;
padding: 10px;
font-size: 16px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
button {
margin: 10px;
padding: 10px 20px;
font-size: 16px;
}

View File

@@ -120,14 +120,14 @@ void ADS1256::setDRATE(uint8_t drate) //Setting DRATE (sampling frequency)
{
writeRegister(DRATE_REG, drate);
_DRATE = drate;
delay(200);
delayMicroseconds(500);
}
void ADS1256::setMUX(uint8_t mux) //Setting MUX (input channel)
{
writeRegister(MUX_REG, mux);
_MUX = mux;
delay(200);
//delayMicroseconds(500);
}
void ADS1256::setPGA(uint8_t pga) //Setting PGA (input voltage range)
@@ -138,7 +138,7 @@ void ADS1256::setPGA(uint8_t pga) //Setting PGA (input voltage range)
_ADCON = (_ADCON & 0b11111000) | (_PGA & 0b00000111); // Clearing and then setting bits 2-0 based on pga
writeRegister(ADCON_REG, _ADCON);
delay(200);
delayMicroseconds(1000); //Delay to allow the PGA to settle after changing its value
updateConversionParameter(); //Update the multiplier according top the new PGA value
}
@@ -501,8 +501,6 @@ void ADS1256::writeRegister(uint8_t registerAddress, uint8_t registerValueToWrit
CS_HIGH();
_spi->endTransaction();
delay(100);
}
long ADS1256::readRegister(uint8_t registerAddress) //Reading a register
@@ -524,7 +522,7 @@ long ADS1256::readRegister(uint8_t registerAddress) //Reading a register
CS_HIGH();
_spi->endTransaction();
delay(100);
return regValue;
}

View File

@@ -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
1 # ESP32 Partition Table
2 # Name, Type, SubType, Offset, Size
3 nvs, data, nvs, 0x9000, 0x4000
4 phy_init, data, phy, 0xd000, 0x1000
5 factory, app, factory, 0x10000, 0x300000
6 littlefs, data, littlefs, 0x310000, 0xCF0000

View File

@@ -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
1 # ESP32 Partition Table
2 # Name, Type, SubType, Offset, Size
3 nvs, data, nvs, 0x9000, 0x4000
4 phy_init, data, phy, 0xd000, 0x1000
5 factory, app, factory, 0x10000, 0x300000
6 spiffs, data, spiffs, 0x310000, 0xCF0000

View File

@@ -10,59 +10,63 @@
[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
upload_protocol = esptool
upload_port = /dev/ttyACM2
upload_port = COM4
upload_speed = 921600
;Monitor configuration
monitor_speed = 115200
monitor_port = /dev/ttyACM2
; Build configuration
monitor_port = COM4
monitor_speed = 921600
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=5
-DARDUINO_USB_CDC_ON_BOOT=0
-DARDUINO_USB_MODE=0
-DCONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=1
-DCONFIG_FREERTOS_USE_TRACE_FACILITY=1
-DCONFIG_ASYNC_TCP_MAX_ACK_TIME=5000
-DCONFIG_ASYNC_TCP_PRIORITY=20
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
-DCONFIG_ASYNC_TCP_RUNNING_CORE=1
-DCONFIG_ASYNC_TCP_STACK_SIZE=8192
-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}
upload_protocol = esptool
upload_port = /dev/ttyACM2
upload_port = COM8
upload_speed = 921600
;Monitor configuration
monitor_speed = 115200
monitor_port = /dev/ttyACM2
; Debug configuration
monitor_port = COM4
monitor_speed = 921600
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=5
-DARDUINO_USB_CDC_ON_BOOT=0
-DARDUINO_USB_MODE=0
-DCONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=1
-DCONFIG_FREERTOS_USE_TRACE_FACILITY=1
-DCONFIG_ASYNC_TCP_MAX_ACK_TIME=5000
-DCONFIG_ASYNC_TCP_PRIORITY=20
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
-DCONFIG_ASYNC_TCP_RUNNING_CORE=1
-DCONFIG_ASYNC_TCP_STACK_SIZE=8192
-fstack-protector-all

View File

@@ -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

View File

@@ -0,0 +1,201 @@
#include "datasave.h"
#include <math.h>
static const size_t min_free = 1024 * 1024; // minimum free space in SPIFFS to allow saving history (1MB)
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<dataSaveParams *>(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<ignitionBoxStatus> &history, const std::filesystem::path &file_name)
{
// Initialize SPIFFS
if (!SAVE_HISTORY_TO_SPIFFS)
return;
// auto spiffs_guard = LITTLEFSGuard(); // use RAII guard to ensure SPIFFS 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());
}

View File

@@ -0,0 +1,73 @@
#pragma once
#define DEBUGLOG_DEFAULT_LOG_LEVEL_INFO
// System Includes
#include <Arduino.h>
#include <DebugLog.h>
#include <LittleFS.h>
#include <string>
#include <fstream>
#include <filesystem>
#include <ArduinoJson.h>
// Project Includes
#include "isr.h"
#include "psvector.h"
const uint32_t max_history = 256;
const bool SAVE_HISTORY_TO_SPIFFS = false; // Set to true to enable saving history to SPIFFS, false to disable
static bool first_save = true; // flag to indicate if this is the first save (to write header)
struct dataSaveParams
{
const PSRAMVector<ignitionBoxStatus> *history;
const std::filesystem::path file_path;
};
class LITTLEFSGuard
{
public:
LITTLEFSGuard()
{
if (!LittleFS.begin(true))
{
LOG_ERROR("Failed to mount LittleFS");
}
LOG_INFO("SPIFFS mounted successfully");
}
~LITTLEFSGuard()
{
LittleFS.end();
LOG_INFO("LittleFS unmounted successfully");
}
};
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<ignitionBoxStatus> &history, const std::filesystem::path &file_path);

View File

@@ -0,0 +1,90 @@
#pragma once
#include <Arduino.h>
#include <map>
// =====================
// Event Flags (bitmask)
// =====================
static const uint32_t TRIG_FLAG_12P = (1 << 0);
static const uint32_t TRIG_FLAG_12N = (1 << 1);
static const uint32_t TRIG_FLAG_34P = (1 << 2);
static const uint32_t TRIG_FLAG_34N = (1 << 3);
static const uint32_t SPARK_FLAG_TIMEOUT = (1 << 8);
static const uint32_t SPARK_FLAG_12 = (1 << 9);
static const uint32_t SPARK_FLAG_34 = (1 << 10);
// Spark Status
enum sparkStatus
{
SPARK_POS_OK,
SPARK_NEG_OK,
SPARK_POS_SKIP,
SPARK_NEG_SKIP,
SPARK_POS_WAIT,
SPARK_NEG_WAIT,
SPARK_POS_FAIL,
SPARK_NEG_FAIL,
SPARK_POS_UNEXPECTED,
SPARK_NEG_UNEXPECTED,
SPARK_SYNC_FAIL,
};
static const std::map<const sparkStatus, const char *> sparkStatusNames = {
{SPARK_POS_OK, "SPARK_POS_OK"},
{SPARK_NEG_OK, "SPARK_NEG_OK"},
{SPARK_POS_SKIP, "SPARK_POS_SKIP"},
{SPARK_NEG_SKIP, "SPARK_NEG_SKIP"},
{SPARK_POS_WAIT, "SPARK_POS_WAIT"},
{SPARK_NEG_WAIT, "SPARK_NEG_WAIT"},
{SPARK_POS_FAIL, "SPARK_POS_FAIL"},
{SPARK_NEG_FAIL, "SPARK_NEG_FAIL"},
{SPARK_POS_UNEXPECTED, "SPARK_POS_UNEXPECTED"},
{SPARK_NEG_UNEXPECTED, "SPARK_NEG_UNEXPECTED"},
{SPARK_SYNC_FAIL, "SPARK_SYNC_FAIL"},
};
enum softStartStatus
{
NORMAL,
SOFT_START,
ERROR,
};
const std::map<const softStartStatus, const char *> softStartStatusNames = {
{NORMAL, "NORMAL"},
{SOFT_START, "SOFT_START"},
{ERROR, "ERROR"},
};
struct coilsStatus
{
int64_t trig_time = 0;
int64_t spark_time = 0;
int32_t spark_delay = 0; // in microseconds
sparkStatus spark_status = sparkStatus::SPARK_POS_OK;
softStartStatus sstart_status = softStartStatus::NORMAL;
float peak_p_in = 0.0;
float peak_n_in = 0.0;
float peak_p_out = 0.0;
float peak_n_out = 0.0;
float level_spark = 0.0;
uint32_t n_events = 0;
uint32_t n_missed_firing = 0;
};
// Task internal Status
struct ignitionBoxStatus
{
int64_t timestamp = 0;
// coils pairs for each ignition
coilsStatus coils12;
coilsStatus coils34;
// voltage from generator
float volts_gen = 0.0;
// enine rpm
int32_t eng_rpm = 0;
// debug values
uint32_t n_queue_errors = 0;
int32_t adc_read_time = 0;
};

View File

@@ -1,9 +1,11 @@
#pragma once
// Library defines
#define ADS1256_SPI_ALREADY_STARTED
// Device Libraries
#include <ADS1256.h>
#include <AD5292.h>
#include <Adafruit_SSD1306.h>
#include <PCA95x5.h>
// ADC Channel mapping
@@ -20,7 +22,6 @@
struct Devices {
AD5292 *pot_a = NULL, *pot_b = NULL;
ADS1256 *adc_a = NULL, *adc_b = NULL;
Adafruit_SSD1306* lcd = NULL;
PCA9555* io = NULL;
};

View File

@@ -36,18 +36,13 @@ void trig_isr(void *arg)
xTaskNotifyFromISR(task_handle, params->flag, eSetValueWithOverwrite, &xHigherPriorityTaskWoken);
break;
case SPARK_FLAG_12:
box->coils34.spark_ok = false;
box->coils12.spark_ok = true;
box->coils12.spark_time = time_us;
xTaskNotifyFromISR(task_handle, params->flag, eSetValueWithOverwrite, &xHigherPriorityTaskWoken);
// vTaskNotifyGiveFromISR(task_handle, &xHigherPriorityTaskWoken);
break;
case SPARK_FLAG_34:
box->coils12.spark_ok = false;
box->coils34.spark_ok = true;
box->coils34.spark_time = time_us;
xTaskNotifyFromISR(task_handle, params->flag, eSetValueWithOverwrite, &xHigherPriorityTaskWoken);
// vTaskNotifyGiveFromISR(task_handle, &xHigherPriorityTaskWoken);
break;
default:
break;

View File

@@ -12,94 +12,12 @@
#else
#include "pins_test.h"
#endif
#include "datastruct.h"
#define CORE_0 0
#define CORE_1 1
#define TASK_STACK 4096 // in words
#define TASK_PRIORITY (configMAX_PRIORITIES - 4) // highest priority after wifi tasks
// =====================
// Event Flags (bitmask)
// =====================
static const uint32_t TRIG_FLAG_12P = (1 << 0);
static const uint32_t TRIG_FLAG_12N = (1 << 1);
static const uint32_t TRIG_FLAG_34P = (1 << 2);
static const uint32_t TRIG_FLAG_34N = (1 << 3);
static const uint32_t SPARK_FLAG_NIL = (1 << 8);
static const uint32_t SPARK_FLAG_12 = (1 << 9);
static const uint32_t SPARK_FLAG_34 = (1 << 10);
static const uint32_t SPARK_FLAG_TIMEOUT = (1 << 11);
// Spark Status
enum sparkStatus
{
SPARK_POS_OK,
SPARK_NEG_OK,
SPARK_POS_SKIP,
SPARK_NEG_SKIP,
SPARK_POS_WAIT,
SPARK_NEG_WAIT,
SPARK_POS_FAIL,
SPARK_NEG_FAIL,
SPARK_POS_UNEXPECTED,
SPARK_NEG_UNEXPECTED,
SPARK_SYNC_FAIL,
};
static const std::map<const sparkStatus, const char *> sparkStatusNames = {
{SPARK_POS_OK, "SPARK_POS_OK"},
{SPARK_NEG_OK, "SPARK_NEG_OK"},
{SPARK_POS_SKIP, "SPARK_POS_SKIP"},
{SPARK_NEG_SKIP, "SPARK_NEG_SKIP"},
{SPARK_POS_WAIT, "SPARK_POS_WAIT"},
{SPARK_NEG_WAIT, "SPARK_NEG_WAIT"},
{SPARK_POS_FAIL, "SPARK_POS_FAIL"},
{SPARK_NEG_FAIL, "SPARK_NEG_FAIL"},
{SPARK_POS_UNEXPECTED, "SPARK_POS_UNEXPECTED"},
{SPARK_NEG_UNEXPECTED, "SPARK_NEG_UNEXPECTED"},
{SPARK_SYNC_FAIL, "SPARK_SYNC_FAIL"},
};
enum softStartStatus
{
NORMAL,
SOFT_START,
ERROR,
};
const std::map<const softStartStatus, const char *> softStartStatusNames = {
{NORMAL, "NORMAL"},
{SOFT_START, "SOFT_START"},
{ERROR, "ERROR"},
};
struct coilsStatus
{
int64_t trig_time = 0;
int64_t spark_time = 0;
int64_t spark_delay = 0; // in microseconds
sparkStatus spark_status = sparkStatus::SPARK_POS_OK;
softStartStatus sstart_status = softStartStatus::NORMAL;
float peak_p_in = 0.0, peak_n_in = 0.0;
float peak_p_out = 0.0, peak_n_out = 0.0;
float trigger_spark = 0.0;
bool spark_ok = false;
uint32_t n_events = 0;
};
// Task internal Status
struct ignitionBoxStatus
{
int64_t timestamp = 0;
// coils pairs for each ignition
coilsStatus coils12;
coilsStatus coils34;
// voltage from generator
float volts_gen = 0.0;
uint32_t n_queue_errors = 0;
};
#define RT_TASK_STACK 4096 // in words
#define RT_TASK_PRIORITY (configMAX_PRIORITIES - 4) // highest priority after wifi tasks
struct isrParams
{

View File

@@ -5,11 +5,15 @@
#include <DebugLog.h>
#include <DebugLogEnable.h>
#include <SPI.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
// Definitions
#include <tasks.h>
#include <channels.h>
#include <devices.h>
#include <datasave.h>
#include <ui.h>
// FreeRTOS directives
@@ -19,10 +23,21 @@
// #define CH_B_ENABLE
#define TEST
void printTaskStats() {
char buffer[1024];
vTaskGetRunTimeStats(buffer);
Serial.println(buffer);
#define WIFI_SSID "AstroRotaxMonitor"
#define WIFI_PASSWORD "maledettirotax"
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len)
{
switch (type)
{
case WS_EVT_CONNECT:
Serial.printf("WS client IP[%s]-ID[%u] CONNECTED\r\n", client->remoteIP().toString().c_str(), client->id());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WS client ID[%u] DISCONNECTED\r\n", client->remoteIP().toString().c_str(), client->id());
break;
}
}
void setup()
@@ -35,35 +50,64 @@ void setup()
LOG_SET_LEVEL(DebugLogLevel::LVL_INFO);
// Print Processor Info
LOG_INFO("ESP32 Chip:", ESP.getChipModel());
LOG_DEBUG("ESP32 Chip:", ESP.getChipModel());
if (psramFound())
{
LOG_INFO("ESP32 PSram Found");
LOG_INFO("ESP32 PSram:", ESP.getPsramSize());
LOG_DEBUG("ESP32 PSram Found");
LOG_DEBUG("ESP32 PSram:", ESP.getPsramSize());
psramInit();
}
LOG_INFO("ESP32 Flash:", ESP.getFlashChipSize());
LOG_INFO("ESP32 Heap:", ESP.getHeapSize());
LOG_INFO("ESP32 Sketch:", ESP.getFreeSketchSpace());
LOG_DEBUG("ESP32 Flash:", ESP.getFlashChipSize());
LOG_DEBUG("ESP32 Heap:", ESP.getHeapSize());
LOG_DEBUG("ESP32 Sketch:", ESP.getFreeSketchSpace());
// Initialize Interrupt pins on PICKUP detectors
initTriggerPinsInputs();
// Initialize Interrupt pins on SPARK detectors
initSparkPinInputs();
// Init Wifi station
LOG_INFO("Initializing WiFi...");
WiFi.mode(WIFI_AP);
IPAddress local_IP(10, 11, 12, 1);
IPAddress gateway(10, 11, 12, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(local_IP, gateway, subnet);
if (WiFi.softAP(WIFI_SSID, WIFI_PASSWORD))
{
LOG_INFO("WiFi AP Mode Started");
LOG_INFO("Wifi SSID:", WIFI_SSID);
LOG_INFO("Wifi Password:", WIFI_PASSWORD);
LOG_INFO("WiFi IP:" + WiFi.softAPIP().toString());
}
else
{
LOG_ERROR("Failed to start WiFi AP Mode");
LOG_ERROR("5 seconds to restart...");
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart();
}
}
void loop()
{
// global variables
bool running = true;
static Devices dev;
const uint32_t max_queue = 128;
const uint32_t filter_k = 10;
PSRAMVector<ignitionBoxStatus> ignA_history_0(max_history);
PSRAMVector<ignitionBoxStatus> ignA_history_1(max_history);
auto *active_history = &ignA_history_0;
auto *writable_history = &ignA_history_1;
// Resources Initialization
static Devices dev;
// Task handle
static TaskHandle_t trigA_TaskHandle = NULL;
static TaskHandle_t trigB_TaskHandle = NULL;
static QueueHandle_t rt_taskA_queue = xQueueCreate(10, sizeof(ignitionBoxStatus));
static QueueHandle_t rt_taskB_queue = xQueueCreate(10, sizeof(ignitionBoxStatus));
// Data Queue for real time task to main loop communication
static QueueHandle_t rt_taskA_queue = xQueueCreate(max_queue, sizeof(ignitionBoxStatus));
static QueueHandle_t rt_taskB_queue = xQueueCreate(max_queue, sizeof(ignitionBoxStatus));
static rtTaskParams taskA_params{
.rt_running = true,
.dev = &dev,
@@ -79,10 +123,10 @@ void loop()
.spark_pin_34 = SPARK_PIN_A34},
.rt_resets = rtTaskResets{.rst_io_12p = RST_EXT_A12P, .rst_io_12n = RST_EXT_A12N, .rst_io_34p = RST_EXT_A34P, .rst_io_34n = RST_EXT_A34N}};
LOG_INFO("Task Variables OK");
LOG_DEBUG("Task Variables OK");
#ifdef CH_B_ENABLE
QueueHandle_t rt_taskB_queue = xQueueCreate(10, sizeof(ignitionBoxStatus));
QueueHandle_t rt_taskB_queue = xQueueCreate(max_queue, sizeof(ignitionBoxStatus));
rtTaskParams taskB_params{
.rt_running = true,
.dev = &dev,
@@ -99,17 +143,18 @@ void loop()
.rt_resets = rtTaskResets{.rst_io_12p = RST_EXT_B12P, .rst_io_12n = RST_EXT_B12N, .rst_io_34p = RST_EXT_B34P, .rst_io_34n = RST_EXT_B34N}};
#endif
// Spi ok flags
bool spiA_ok = true;
bool spiB_ok = true;
#ifndef TEST
// Init 2 SPI interfaces
SPIClass SPI_A(FSPI);
spiA_ok = SPI_A.begin(SPI_A_SCK, SPI_A_MISO, SPI_A_MOSI);
SPI_A.setDataMode(SPI_MODE1); // ADS1256 requires SPI mode 1
#ifndef TEST
SPIClass SPI_B(HSPI);
spiB_ok = SPI_B.begin(SPI_B_SCK, SPI_B_MISO, SPI_B_MOSI);
SPI_B.setDataMode(SPI_MODE1); // ADS1256 requires SPI mode 1
#endif
if (!spiA_ok || !spiB_ok)
{
LOG_ERROR("Unable to Initialize SPI Busses");
@@ -117,15 +162,15 @@ void loop()
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart();
}
LOG_INFO("Init SPI OK");
LOG_DEBUG("Init SPI OK");
#ifndef TEST
// Init ADC_A
dev.adc_a = new ADS1256(ADC_A_DRDY, ADC_A_RST, ADC_A_SYNC, ADC_A_CS, 2.5, &SPI_A);
dev.adc_a = new ADS1256(ADC_A_DRDY, ADS1256::PIN_UNUSED, ADC_A_SYNC, ADC_A_CS, 2.5, &SPI_A);
dev.adc_a->InitializeADC();
dev.adc_a->setPGA(PGA_1);
dev.adc_a->setDRATE(DRATE_1000SPS);
dev.adc_a->setDRATE(DRATE_7500SPS);
#ifndef TEST
// Init ADC_B
dev.adc_a = new ADS1256(ADC_B_DRDY, ADC_B_RST, ADC_B_SYNC, ADC_B_CS, 2.5, &SPI_B);
dev.adc_a->InitializeADC();
@@ -133,16 +178,16 @@ void loop()
dev.adc_a->setDRATE(DRATE_1000SPS);
#endif
LOG_INFO("Init ADC OK");
LOG_DEBUG("Init ADC OK");
// Ignition A on Core 0
auto ignA_task_success = pdPASS;
ignA_task_success = xTaskCreatePinnedToCore(
rtIgnitionTask,
"rtIgnitionTask_boxA",
TASK_STACK,
RT_TASK_STACK,
(void *)&taskA_params,
TASK_PRIORITY,
RT_TASK_PRIORITY,
&trigA_TaskHandle,
CORE_0);
@@ -152,75 +197,89 @@ void loop()
ignB_task_success = xTaskCreatePinnedToCore(
rtIgnitionTask,
"rtIgnitionTask_boxB",
TASK_STACK,
RT_TASK_STACK,
(void *)&taskB_params,
TASK_PRIORITY, // priorità leggermente più alta
RT_TASK_PRIORITY, // priorità leggermente più alta
&trigB_TaskHandle,
CORE_1);
#endif
if ((ignA_task_success && ignB_task_success) != pdPASS)
{
LOG_ERROR("Unble to initialize ISR task");
LOG_ERROR("Una ble to initialize ISR task");
LOG_ERROR("5 seconds to restart...");
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart();
}
LOG_INFO("Real Time Tasks A & B initialized");
LOG_DEBUG("Real Time Tasks A & B initialized");
////////////////////// MAIN LOOP //////////////////////
clearScreen();
setCursor(0, 0);
ignitionBoxStatus ignA;
bool partial_save = false; // flag to indicate if a partial save has been done after a timeout
uint32_t counter = 0;
uint32_t wait_count = 0;
ignitionBoxStatus ign_info;
int64_t last = esp_timer_get_time();
uint32_t missed_firings12 = 0;
uint32_t missed_firings34 = 0;
uint32_t counter = 0;
ignitionBoxStatusAverage ignA_avg(filter_k); // moving average calculator for ignition box A with a window of 100 samples
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
ws.onEvent(onWsEvent);
server.addHandler(&ws);
auto spiffs_guard = LITTLEFSGuard(); // use RAII guard to ensure SPIFFS is properly mounted and unmounted
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
server.begin();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/html", htmlTest.c_str()); });
while (running)
{
if (xQueueReceive(rt_taskA_queue, &ignA, pdMS_TO_TICKS(1000)) == pdTRUE)
if (counter >= active_history->size()) // not concurrent with write task
{
if (ignA.coils12.spark_status == sparkStatus::SPARK_NEG_FAIL || ignA.coils12.spark_status == sparkStatus::SPARK_POS_FAIL)
missed_firings12++;
if (ignA.coils34.spark_status == sparkStatus::SPARK_POS_FAIL || ignA.coils34.spark_status == sparkStatus::SPARK_NEG_FAIL)
missed_firings34++;
clearScreen();
setCursor(0, 0);
printField("++ Timestamp", (uint32_t)ignA.timestamp, 0, 0);
Serial.println("========== Coils 12 =============");
printField("Events", (uint32_t)ignA.coils12.n_events, 0, 1);
printField("Missed Firing", missed_firings12, 0, 2);
printField("Spark Dly", (uint32_t)ignA.coils12.spark_delay, 0, 3);
printField("Spark Sts", sparkStatusNames.at(ignA.coils12.spark_status), 0, 4);
// printField("Peak P_IN", ignA.coils12.peak_p_in, 0, 5);
// printField("Peak P_OUT", ignA.coils12.peak_p_out, 0, 6);
// printField("Peak N_IN", ignA.coils12.peak_n_in, 0, 7);
// printField("Peak N_OUT", ignA.coils12.peak_n_out, 0, 8);
printField("Soft Start ", softStartStatusNames.at(ignA.coils12.sstart_status), 0, 9);
counter = 0;
partial_save = false; // reset partial save flag on new data cycle
auto *temp = active_history;
active_history = writable_history; // switch active and writable buffers
writable_history = temp; // ensure writable_history points to the buffer we just filled
dataSaveParams save_params{
.history = writable_history,
.file_path = "ignition_history.csv"};
save_history(*writable_history, "ignition_history.csv"); // directly call the save task function to save without delay
}
Serial.println("========== Coils 34 =============");
printField("Events", (uint32_t)ignA.coils34.n_events, 0, 11);
printField("Missed Firing", missed_firings34, 0, 12);
printField("Spark Dly", (uint32_t)ignA.coils34.spark_delay, 0, 13);
printField("Spark Sts", sparkStatusNames.at(ignA.coils34.spark_status), 0, 14);
// printField("Peak P_IN", ignA.coils34.peak_p_in, 0, 15);
// printField("Peak P_OUT", ignA.coils34.peak_p_out, 0, 16);
// printField("Peak N_IN", ignA.coils34.peak_n_in, 0, 17);
// printField("Peak N_OUT", ignA.coils34.peak_n_out, 0, 18);
printField("Soft Start ", softStartStatusNames.at(ignA.coils34.sstart_status), 0, 19);
Serial.println("========== END =============");
Serial.println();
auto delta = (esp_timer_get_time() - last) / 1000000.0f; //in seconds
delta = delta > 0 ? 1.0f / delta : 0; // Calculate frequency (Hz)
printField("Frequency (Hz)", delta, 0, 21);
printField("Queue Errors", (uint32_t)ignA.n_queue_errors, 0, 22);
last = esp_timer_get_time();
} else
if (xQueueReceive(rt_taskA_queue, &ign_info, pdMS_TO_TICKS(1000)) == pdTRUE)
{
Serial.println("Waiting for data... ");;
// printInfo(ign_info);
auto &hist = *active_history;
hist[counter++ % active_history->size()] = ign_info;
ignA_avg.update(ign_info); // update moving average with latest ignition status
Serial.print("Data Received: " + String(counter) + "/" + String(hist.size()) + '\r');
if (ws.count() > 0 && counter % 10 == 0) // send data every 10 samples
{
Serial.println();
LOG_INFO("Sending average ignition status to websocket clients...");
auto msg = ignA_avg.toJson().as<String>();
ws.textAll(msg);
}
}
else
{
Serial.printf("[%d] Waiting for data...\r", wait_count++);
if (!partial_save && counter > 0) // if timeout occurs but we have unsaved data, save it before next timeout
{
active_history->resize(counter); // resize active history to actual number of records received to avoid saving empty records
save_history(*active_history, "ignition_history.csv");
active_history->resize(max_history); // resize back to max history size for next data cycle
counter = 0; // reset counter after saving
partial_save = true;
first_save = true;
}
delay(500);
}
}

View File

@@ -48,19 +48,17 @@
// =====================
#define ADC_A_CS 4
#define ADC_A_DRDY 5
#define ADC_A_RST 6
#define ADC_A_SYNC 7
#define ADC_A_SYNC 6
#define ADC_B_CS 14
#define ADC_B_DRDY 15
#define ADC_B_RST 16
#define ADC_B_SYNC 17
#define ADC_B_SYNC 16
// =====================
// DIGITAL POT
// =====================
//#define POT_A_CS 1
//#define POT_B_CS 2
#define POT_A_CS 7
#define POT_B_CS 17
// =====================
// TRIGGER INPUT INTERRUPTS

View File

@@ -0,0 +1,30 @@
#pragma once
#include <vector>
#include "esp_heap_caps.h"
// Allocator custom per PSRAM
template <typename T>
struct PSRAMAllocator {
using value_type = T;
PSRAMAllocator() noexcept {}
template <typename U>
PSRAMAllocator(const PSRAMAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
void* ptr = heap_caps_malloc(n * sizeof(T), MALLOC_CAP_SPIRAM);
if (!ptr) {
throw std::bad_alloc();
}
return static_cast<T*>(ptr);
}
void deallocate(T* p, std::size_t) noexcept {
heap_caps_free(p);
}
};
template <typename T>
using PSRAMVector = std::vector<T, PSRAMAllocator<T>>;

View File

@@ -2,7 +2,8 @@
#include <esp_timer.h>
// Timeout callback for microsecond precision
void spark_timeout_callback(void* arg) {
void spark_timeout_callback(void *arg)
{
TaskHandle_t handle = (TaskHandle_t)arg;
xTaskNotify(handle, SPARK_FLAG_TIMEOUT, eSetValueWithOverwrite);
}
@@ -55,17 +56,16 @@ void rtIgnitionTask(void *pvParameters)
.flag = SPARK_FLAG_34,
.ign_stat = &ign_box_sts,
.rt_handle_ptr = rt_handle_ptr};
LOG_INFO("rtTask ISR Params OK");
// Create esp_timer for microsecond precision timeout
esp_timer_handle_t timeout_timer;
esp_timer_create_args_t timer_args = {
.callback = spark_timeout_callback,
.arg = (void*)rt_handle_ptr,
.arg = (void *)rt_handle_ptr,
.dispatch_method = ESP_TIMER_TASK,
.name = "spark_timeout"
};
.name = "spark_timeout"};
esp_timer_create(&timer_args, &timeout_timer);
// Attach Pin Interrupts
@@ -89,6 +89,8 @@ void rtIgnitionTask(void *pvParameters)
bool first_cycle = true;
bool cycle12 = false;
bool cycle34 = false;
int64_t last_cycle_time = 0;
uint32_t n_errors = 0;
while (params->rt_running)
{
@@ -101,11 +103,9 @@ void rtIgnitionTask(void *pvParameters)
ULONG_MAX, // pulisci i primi 8 bit
&pickup_flag, // valore ricevuto
portMAX_DELAY);
if (first_cycle && pickup_flag != TRIG_FLAG_12P) // skip first cycle because of possible initial noise on pickup signals at startu
{
continue;
}
#ifdef DEBUG
Serial.print("\033[2J"); // clear screen
@@ -128,45 +128,37 @@ void rtIgnitionTask(void *pvParameters)
esp_timer_stop(timeout_timer); // stop timer in case it was running from previous cycle
esp_timer_start_once(timeout_timer, spark_timeout_max);
spark_flag = SPARK_FLAG_NIL; // default value in case of timeout, to be set by ISR if spark event occours
// WAIT FOR SPARK TO HAPPEN OR TIMEOUT
BaseType_t sp = pdFALSE;
sp = xTaskNotifyWait(
0x00, // non pulire all'ingresso
ULONG_MAX, // pulisci i primi 8 bit
&spark_flag, // valore ricevuto
xTaskNotifyWait(
0x00, // non pulire all'ingresso
ULONG_MAX, // pulisci i primi 8 bit
&spark_flag, // valore ricevuto
portMAX_DELAY); // wait indefinitely, timeout handled by esp_timer
// Handle timeout or spark event
if (spark_flag == SPARK_FLAG_TIMEOUT) {
spark_flag = SPARK_FLAG_NIL;
} else {
// Spark occurred, stop the timer
if (spark_flag != SPARK_FLAG_TIMEOUT)
esp_timer_stop(timeout_timer);
}
#ifdef DEBUG
// LOG_INFO("Spark Flags: ", printBits(spark_flag).c_str());
LOG_INFO("Spark12:", ign_box_sts.coils12.spark_ok ? "TRUE" : "FALSE");
LOG_INFO("Spark34:", ign_box_sts.coils34.spark_ok ? "TRUE" : "FALSE");
if (names.contains(spark_flag))
LOG_INFO("Spark Trigger:", names.at(spark_flag));
#endif
xTaskNotifyStateClear(NULL);
ulTaskNotifyValueClear(NULL, 0xFFFFFFFF);
// A trigger from pickup 12 is followed by a spark event on 34 or vice versa pickup 34 triggers spark on 12
if ((pickup_flag == TRIG_FLAG_12P || pickup_flag == TRIG_FLAG_12N) && (spark_flag != SPARK_FLAG_12 && spark_flag != SPARK_FLAG_NIL))
if ((pickup_flag == TRIG_FLAG_12P || pickup_flag == TRIG_FLAG_12N) && (spark_flag != SPARK_FLAG_12 && spark_flag != SPARK_FLAG_TIMEOUT))
{
ign_box_sts.coils12.spark_status = ign_box_sts.coils34.spark_status = sparkStatus::SPARK_SYNC_FAIL;
continue;
}
// Select coil status reference based on pickup_flag
coilsStatus *coils;
switch (pickup_flag)
{
case TRIG_FLAG_12P:
{
first_cycle = false;
// compute engine rpm from cycle time
auto current_time = esp_timer_get_time();
auto cycle_time = current_time - last_cycle_time;
last_cycle_time = current_time;
ign_box_sts.eng_rpm = (int32_t)(60.0f / (cycle_time / 1000000.0f));
}
case TRIG_FLAG_12N:
coils = &ign_box_sts.coils12;
break;
@@ -176,24 +168,21 @@ void rtIgnitionTask(void *pvParameters)
break;
}
// Select logic based on pickup and spark flags
switch (pickup_flag)
{
case TRIG_FLAG_12P:
case TRIG_FLAG_34P:
{
// Timeout not occourred, expected POSITIVE edge spark OCCOURRED
if (spark_flag != SPARK_FLAG_NIL)
if (spark_flag != SPARK_FLAG_TIMEOUT)
{
coils->spark_delay = coils->spark_time - coils->trig_time;
coils->spark_delay = (int32_t)(coils->spark_time - coils->trig_time);
coils->sstart_status = softStartStatus::NORMAL; // because spark on positive edge
coils->spark_status = sparkStatus::SPARK_POS_OK; // do not wait for spark on negative edge
#ifdef DEBUG
LOG_INFO("Spark on POSITIVE pulse");
LOG_INFO("Spark Delay Time: ", (int32_t)coils->spark_delay);
#endif
}
// Timeout occourred, expected POSITIVE edge spark NOT OCCOURRED
else if (spark_flag == SPARK_FLAG_NIL)
else if (spark_flag == SPARK_FLAG_TIMEOUT)
{
coils->spark_status = sparkStatus::SPARK_NEG_WAIT;
coils->sstart_status = softStartStatus::NORMAL;
@@ -206,24 +195,20 @@ void rtIgnitionTask(void *pvParameters)
{
const bool expected_negative = coils->spark_status == sparkStatus::SPARK_NEG_WAIT;
// Timeout not occourred, expected NEGATIVE edge spark OCCOURRED
if (spark_flag != SPARK_FLAG_NIL && expected_negative)
if (spark_flag != SPARK_FLAG_TIMEOUT && expected_negative)
{
coils->spark_delay = coils->spark_time - coils->trig_time;
coils->spark_delay = (int32_t)(coils->spark_time - coils->trig_time);
coils->sstart_status = softStartStatus::SOFT_START;
coils->spark_status = sparkStatus::SPARK_NEG_OK;
#ifdef DEBUG
LOG_INFO("Spark on NEGATIVE pulse");
LOG_INFO("Spark Delay Time: ", (int32_t)coils->spark_delay);
#endif
}
// Timeout occourred, expected POSITIVE edge spark NOT OCCOURRED
else if (spark_flag == SPARK_FLAG_NIL && expected_negative)
else if (spark_flag == SPARK_FLAG_TIMEOUT && expected_negative)
{
coils->sstart_status = softStartStatus::ERROR;
coils->spark_status = sparkStatus::SPARK_NEG_FAIL;
}
// Timeout not occouured, unexpected negative edge spark
else if (spark_flag != SPARK_FLAG_NIL && !expected_negative)
else if (spark_flag != SPARK_FLAG_TIMEOUT && !expected_negative)
{
coils->sstart_status = softStartStatus::SOFT_START;
coils->spark_status = sparkStatus::SPARK_NEG_UNEXPECTED;
@@ -237,11 +222,6 @@ void rtIgnitionTask(void *pvParameters)
break;
}
default:
#ifdef DEUG
LOG_ERROR("Invalid Interrupt");
LOG_ERROR("Pickup Flags: ", printBits(pickup_flag).c_str());
LOG_ERROR("Spark Flags: ", printBits(spark_flag).c_str());
#endif
break;
}
@@ -249,10 +229,16 @@ void rtIgnitionTask(void *pvParameters)
{
cycle12 = false;
cycle34 = false;
// vTaskDelay(pdMS_TO_TICKS(1)); // delay 1ms to allow peak detectors to charge for negative cycle
if (ign_box_sts.coils12.spark_status == sparkStatus::SPARK_POS_FAIL || ign_box_sts.coils12.spark_status == sparkStatus::SPARK_NEG_FAIL)
ign_box_sts.coils12.n_missed_firing++;
if (ign_box_sts.coils34.spark_status == sparkStatus::SPARK_POS_FAIL || ign_box_sts.coils34.spark_status == sparkStatus::SPARK_NEG_FAIL)
ign_box_sts.coils34.n_missed_firing++;
// read adc channels: pickup12, out12 [ pos + neg ]
if (adc) // read only if adc initialized
{
uint32_t start_adc_read = esp_timer_get_time();
// from peak detector circuits
ign_box_sts.coils12.peak_p_in = adcReadChannel(adc, ADC_CH_PEAK_12P_IN);
ign_box_sts.coils12.peak_n_in = adcReadChannel(adc, ADC_CH_PEAK_12N_IN);
@@ -262,6 +248,7 @@ void rtIgnitionTask(void *pvParameters)
ign_box_sts.coils12.peak_n_out = adcReadChannel(adc, ADC_CH_PEAK_12N_OUT);
ign_box_sts.coils34.peak_p_out = adcReadChannel(adc, ADC_CH_PEAK_34P_OUT);
ign_box_sts.coils34.peak_n_out = adcReadChannel(adc, ADC_CH_PEAK_34N_OUT);
ign_box_sts.adc_read_time = (int32_t)(esp_timer_get_time() - start_adc_read);
}
else // simulate adc read timig
vTaskDelay(pdMS_TO_TICKS(1));
@@ -282,10 +269,7 @@ void rtIgnitionTask(void *pvParameters)
if (rt_queue)
ign_box_sts.timestamp = esp_timer_get_time(); // update data timestamp
if (xQueueSendToBack(rt_queue, (void *)&ign_box_sts, 0) != pdPASS)
{
ign_box_sts.n_queue_errors++;
LOG_ERROR("Failed to send to rt_queue");
}
ign_box_sts.n_queue_errors = ++n_errors;
}
}
// Delete the timeout timer

73
RotaxMonitor/src/ui.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include <ui.h>
void clearScreen()
{
Serial.print("\033[2J"); // clear screen
Serial.print("\033[H"); // cursor home
Serial.flush();
}
void setCursor(const uint8_t x, const uint8_t y)
{
Serial.printf("\033[%d;%d", y, x + 1);
Serial.flush();
}
void printField(const char name[], const uint32_t val)
{
Serial.printf("%15s: %06u\n", name, val);
}
void printField(const char name[], const int32_t val)
{
Serial.printf("%15s: %06d\n", name, val);
}
void printField(const char name[], const int64_t val)
{
Serial.printf("%15s: %06u\n", name, (uint64_t)val);
}
void printField(const char name[], const float val)
{
Serial.printf("%15s: %4.2f\n", name, val);
}
void printField(const char name[], const char *val)
{
Serial.printf("%15s: %s\n", name, val);
}
void printInfo(const ignitionBoxStatus &info)
{
clearScreen();
setCursor(0, 0);
printField("++ Timestamp ++", (uint32_t)info.timestamp);
Serial.println("========== Coils 12 =============");
printField("Events", info.coils12.n_events);
printField("Events Missed", info.coils12.n_missed_firing);
printField("Spark Dly", (uint32_t)info.coils12.spark_delay);
printField("Spark Sts", sparkStatusNames.at(info.coils12.spark_status));
printField("Peak P_IN", info.coils12.peak_p_in);
printField("Peak N_IN", info.coils12.peak_n_in);
printField("Peak P_OUT", info.coils12.peak_p_out);
printField("Peak N_OUT", info.coils12.peak_n_out);
printField("Soft Start ", softStartStatusNames.at(info.coils12.sstart_status));
Serial.println("========== Coils 34 =============");
printField("Events", info.coils34.n_events);
printField("Events Missed", info.coils34.n_missed_firing);
printField("Spark Dly", (uint32_t)info.coils34.spark_delay);
printField("Spark Sts", sparkStatusNames.at(info.coils34.spark_status));
printField("Peak P_IN", info.coils34.peak_p_in);
printField("Peak N_IN", info.coils34.peak_n_in);
printField("Peak P_OUT", info.coils34.peak_p_out);
printField("Peak N_OUT", info.coils34.peak_n_out);
printField("Soft Start ", softStartStatusNames.at(info.coils34.sstart_status));
Serial.println("============ END ===============");
Serial.println();
printField("Engine RPM", info.eng_rpm);
printField("ADC Read Time", info.adc_read_time);
printField("Queue Errors", info.n_queue_errors);
}

View File

@@ -1,59 +1,204 @@
#pragma once
#include <Arduino.h>
#include <datastruct.h>
#include <string>
static bool firstRun = true;
void clearScreen();
void setCursor(const uint8_t x, const uint8_t y);
void printField(const char name[], const uint32_t val);
void printField(const char name[], const int32_t val);
void printField(const char name[], const int64_t val);
void printField(const char name[], const float val);
void printField(const char name[], const char *val);
void clearScreen(){
Serial.print("\033[2J"); // clear screen
Serial.print("\033[H"); // cursor home
Serial.flush();
}
void printInfo(const ignitionBoxStatus &info);
void setCursor(const uint8_t x, const uint8_t y) {
Serial.printf("\033[%d;%d", y, x+1);
Serial.flush();
}
static const std::string htmlTest = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32 Dashboard</title>
<style>
body {
font-family: Arial;
text-align: center;
margin-top: 40px;
}
void printField(const char name[], const uint32_t val, const uint8_t x, const uint8_t y) {
if (firstRun) {
setCursor(x,y);
Serial.printf("%15s: %06d\n", name, val);
return;
}
setCursor(x+16, y);
Serial.print(val);
}
table {
margin: auto;
border-collapse: collapse;
width: 100%;
max-width: 900px;
}
void printField(const char name[], const int64_t val, const uint8_t x, const uint8_t y) {
if (firstRun) {
setCursor(x,y);
Serial.printf("%15s: %06u\n", name, (uint64_t)val);
return;
}
setCursor(x+16, y);
Serial.print((uint64_t)val);
Serial.flush();
}
th, td {
border: 1px solid #ccc;
padding: 10px;
font-size: 16px;
text-align: left;
}
void printField(const char name[], const float val, const uint8_t x, const uint8_t y) {
if (firstRun) {
setCursor(x,y);
Serial.printf("%15s: %4.2f\n", name, val);
return;
}
setCursor(x+16, y);
Serial.print(val);
Serial.flush();
}
th {
background-color: #f4f4f4;
}
void printField(const char name[], const char *val, const uint8_t x, const uint8_t y) {
if (firstRun) {
setCursor(x,y);
Serial.printf("%15s: %s\n", name, val);
return;
}
setCursor(x+16, y);
Serial.print(val);
Serial.flush();
}
button {
margin: 10px;
padding: 10px 20px;
font-size: 16px;
}
</style>
</head>
<body>
<h2>RotaxMonitor realtime data</h2>
<button onclick="start()">Start</button>
<button onclick="stop()">Stop</button>
<div style="max-width: 900px; margin: 0 auto; text-align: left;">
<p><strong>Timestamp:</strong> <span id="timestamp">-</span></p>
<p><strong>Data Valid:</strong> <span id="datavalid">-</span></p>
<p><strong>Generator voltage:</strong> <span id="volts_gen">-</span></p>
<p><strong>Engine RPM:</strong> <span id="eng_rpm">-</span></p>
<p><strong>ADC read time:</strong> <span id="adc_read_time">-</span></p>
<p><strong>Queue errors:</strong> <span id="n_queue_errors">-</span></p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Coils 12</th>
<th>Coils 34</th>
</tr>
</thead>
<tbody>
<tr>
<td>Spark delay</td>
<td id="coils12_spark_delay">-</td>
<td id="coils34_spark_delay">-</td>
</tr>
<tr>
<td>Spark status</td>
<td id="coils12_spark_status">-</td>
<td id="coils34_spark_status">-</td>
</tr>
<tr>
<td>Soft start status</td>
<td id="coils12_sstart_status">-</td>
<td id="coils34_sstart_status">-</td>
</tr>
<tr>
<td>Peak P in</td>
<td id="coils12_peak_p_in">-</td>
<td id="coils34_peak_p_in">-</td>
</tr>
<tr>
<td>Peak N in</td>
<td id="coils12_peak_n_in">-</td>
<td id="coils34_peak_n_in">-</td>
</tr>
<tr>
<td>Peak P out</td>
<td id="coils12_peak_p_out">-</td>
<td id="coils34_peak_p_out">-</td>
</tr>
<tr>
<td>Peak N out</td>
<td id="coils12_peak_n_out">-</td>
<td id="coils34_peak_n_out">-</td>
</tr>
<tr>
<td>Level spark</td>
<td id="coils12_level_spark">-</td>
<td id="coils34_level_spark">-</td>
</tr>
<tr>
<td>Events</td>
<td id="coils12_n_events">-</td>
<td id="coils34_n_events">-</td>
</tr>
<tr>
<td>Missed firings</td>
<td id="coils12_n_missed_firing">-</td>
<td id="coils34_n_missed_firing">-</td>
</tr>
</tbody>
</table>
</div>
<script>
let ws;
function connectWS() {
ws = new WebSocket("ws://" + location.host + "/ws");
ws.onopen = () => {
console.log("WebSocket connesso");
};
ws.onclose = () => {
console.log("WebSocket disconnesso, retry...");
setTimeout(connectWS, 5000);
};
ws.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
console.error("Invalid JSON received", e);
return;
}
document.getElementById("datavalid").textContent = data.datavalid ?? "-";
document.getElementById("timestamp").textContent = data.timestamp ?? "-";
document.getElementById("volts_gen").textContent = data.volts_gen ?? "-";
document.getElementById("eng_rpm").textContent = data.eng_rpm ?? "-";
document.getElementById("adc_read_time").textContent = data.adc_read_time ?? "-";
document.getElementById("n_queue_errors").textContent = data.n_queue_errors ?? "-";
const coils12 = data.coils12 || {};
const coils34 = data.coils34 || {};
document.getElementById("coils12_spark_delay").textContent = coils12.spark_delay ?? "-";
document.getElementById("coils34_spark_delay").textContent = coils34.spark_delay ?? "-";
document.getElementById("coils12_spark_status").textContent = coils12.spark_status ?? "-";
document.getElementById("coils34_spark_status").textContent = coils34.spark_status ?? "-";
document.getElementById("coils12_sstart_status").textContent = coils12.sstart_status ?? "-";
document.getElementById("coils34_sstart_status").textContent = coils34.sstart_status ?? "-";
document.getElementById("coils12_peak_p_in").textContent = coils12.peak_p_in ?? "-";
document.getElementById("coils34_peak_p_in").textContent = coils34.peak_p_in ?? "-";
document.getElementById("coils12_peak_n_in").textContent = coils12.peak_n_in ?? "-";
document.getElementById("coils34_peak_n_in").textContent = coils34.peak_n_in ?? "-";
document.getElementById("coils12_peak_p_out").textContent = coils12.peak_p_out ?? "-";
document.getElementById("coils34_peak_p_out").textContent = coils34.peak_p_out ?? "-";
document.getElementById("coils12_peak_n_out").textContent = coils12.peak_n_out ?? "-";
document.getElementById("coils34_peak_n_out").textContent = coils34.peak_n_out ?? "-";
document.getElementById("coils12_level_spark").textContent = coils12.level_spark ?? "-";
document.getElementById("coils34_level_spark").textContent = coils34.level_spark ?? "-";
document.getElementById("coils12_n_events").textContent = coils12.n_events ?? "-";
document.getElementById("coils34_n_events").textContent = coils34.n_events ?? "-";
document.getElementById("coils12_n_missed_firing").textContent = coils12.n_missed_firing ?? "-";
document.getElementById("coils34_n_missed_firing").textContent = coils34.n_missed_firing ?? "-";
};
}
function start() {
fetch("/start");
}
function stop() {
fetch("/stop");
}
connectWS();
</script>
</body>
</html>
)rawliteral";

View File

@@ -16,7 +16,7 @@ lib_deps =
hideakitai/DebugLog@^0.8.4
board_build.flash_size = 4MB
board_build.partitions = default.csv
monitor_speed = 115200
monitor_speed = 921600
build_type = release
[env:esp32-devtest-debug]
@@ -27,7 +27,7 @@ lib_deps =
hideakitai/DebugLog@^0.8.4
board_build.flash_size = 4MB
board_build.partitions = default.csv
monitor_speed = 115200
monitor_speed = 921600
build_type = debug
build_flags =
-O0

View File

@@ -18,13 +18,16 @@ static uint32_t count = 0;
#define PAUSE_LONG_MIN 5000
#define PAUSE_LONG_MAX PAUSE_LONG_MIN*100
#define RPM_MIN 800
#define RPM_MAX 5500
void clearScreen(){
Serial.print("\033[2J"); // clear screen
Serial.print("\033[H"); // cursor home
Serial.flush();
}
static double filtered = 0;
static double filtered_rpm = 0;
static const std::map<const uint32_t, const char *> pin2Name = {
{PIN_TRIG_A12P, "HIGH_PIN_TRIG_A12P"},
@@ -51,10 +54,12 @@ static timerStatus stsA = {
.spark_delay_us = 50,
.main_task = NULL};
static bool isEnabled = false;
void setup()
{
Serial.begin(115200);
Serial.begin(921600);
delay(1000);
LOG_ATTACH_SERIAL(Serial);
@@ -73,9 +78,14 @@ void setup()
pinMode(SPARK_B34, OUTPUT);
pinMode(SPARK_DELAY_POT, ANALOG);
pinMode(FREQ_POT, ANALOG);
pinMode(ENABLE_PIN, INPUT_PULLUP);
stsA.main_task = xTaskGetCurrentTaskHandleForCore(1);
timerA = timerBegin(FREQUENCY);
timerStop(timerA);
timerAttachInterruptArg(timerA, &onTimer, (void *)&stsA);
timerAlarm(timerA, 1, true, 0);
@@ -94,14 +104,28 @@ void loop()
stsA.soft_start = false;
}
double new_val = (float)(map(analogRead(FREQ_POT), 0, 4096, PAUSE_LONG_MIN, PAUSE_LONG_MAX));
filtered = filtered + 0.1 * (new_val - filtered);
stsA.pause_long_us = (uint32_t)filtered;
double new_rpm = (double)(map(analogRead(FREQ_POT), 0, 4096, RPM_MIN, RPM_MAX));
filtered_rpm = filtered_rpm + 0.1 * (new_rpm - filtered_rpm);
stsA.pause_long_us = (uint32_t)(60000000.0f / filtered_rpm / 2.0f);
if (isEnabled) {
LOG_INFO("==== System is ENABLED ====");
} else {
LOG_INFO("==== System is DISABLED ====");
}
LOG_INFO("Spark Delay uS: ", stsA.spark_delay_us, "\tSoft Start: ", stsA.soft_start ? "TRUE" : "FALSE");
LOG_INFO("Pause: ", (uint32_t)(stsA.pause_long_us / 1000), "ms");
LOG_INFO("Engine Rpm: ", (uint32_t)(filtered_rpm));
LOG_INFO("Coil Pulse: ", stsA.coil_pulse_us, "us");
LOG_INFO("Spark Pulse: ", stsA.spark_pulse_us, "us");
if (digitalRead(ENABLE_PIN) == LOW && !isEnabled) {
timerStart(timerA);
isEnabled = true;
} else if (digitalRead(ENABLE_PIN) == HIGH && isEnabled) {
timerStop(timerA);
isEnabled = false;
}
delay(100);
clearScreen();

View File

@@ -1,5 +1,8 @@
#pragma once
// Enable Pin
#define ENABLE_PIN 16
///// Ignition Box A /////
#define PIN_TRIG_A12P 18
#define PIN_TRIG_A12N 19