17 Commits

22 changed files with 866 additions and 552 deletions

View File

@@ -11738,7 +11738,7 @@
(layer "Edge.Cuts") (layer "Edge.Cuts")
(uuid "da6f4122-0ecc-496f-b0fd-e4abef534976") (uuid "da6f4122-0ecc-496f-b0fd-e4abef534976")
) )
(gr_text "KTM CAN ChainLube Mk1\nPCB Rev.: 1.4\n(c) 2025 - Marcel Peterkau" (gr_text "KTM CAN ChainLube Mk1\nPCB Rev.: 1.5\n(c) 2025 - Marcel Peterkau"
(at 156.464 85.344 90) (at 156.464 85.344 90)
(layer "B.SilkS") (layer "B.SilkS")
(uuid "00000000-0000-0000-0000-000061de6936") (uuid "00000000-0000-0000-0000-000061de6936")

119
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,119 @@
pipeline {
agent any
environment {
BUILD_ENV = "pcb_rev_1-4_serial"
PIO_HOME_DIR = "${WORKSPACE}/.pio"
VENV_PATH = "${WORKSPACE}/Software/.venv"
}
parameters {
choice(
name: 'BUILD_ENV',
choices: ['pcb_rev_1-2_serial', 'pcb_rev_1-3_serial', 'pcb_rev_1-4_serial'],
description: 'Firmware-Umgebung auswählen'
)
}
stages {
stage('🧰 Setup PlatformIO') {
steps {
dir('Software') {
sh """
[ -d .venv ] || python3 -m venv .venv
${env.VENV_PATH}/bin/pip install --upgrade pip platformio
"""
}
}
}
stage('📄 Dummy WiFi-Creds') {
steps {
dir('Software') {
writeFile file: 'wifi_credentials.ini', text: '''
[wifi_cred]
wifi_ssid_client = DummySSID
wifi_password_client = DummyPass
admin_password = Admin123
wifi_ap_password = DummyAP
'''.stripIndent()
}
}
}
stage('🧪 Build Firmware') {
steps {
dir('Software') {
sh """
${env.VENV_PATH}/bin/platformio run -e ${params.BUILD_ENV}
"""
}
}
}
stage('📁 Build Filesystem (LittleFS)') {
steps {
dir('Software') {
sh """
${env.VENV_PATH}/bin/platformio run -t buildfs -e ${params.BUILD_ENV}
"""
}
}
}
stage('📦 Find & Archive Firmware') {
steps {
dir('Software') {
script {
echo "🔍 Suche nach Firmware (.fw.bin) und Filesystem (.fs.gz) Artefakten..."
def firmwareFiles = findFiles(glob: '.pio/build/**/*.fw.bin')
def fsFiles = findFiles(glob: '.pio/build/**/*.fs.gz')
if (firmwareFiles.length == 0 && fsFiles.length == 0) {
echo "⚠️ Keine passenden Artefakte (.fw.bin / .fs.gz) gefunden nichts zu archivieren."
} else {
firmwareFiles.each { echo "📦 Firmware: ${it.path}" }
fsFiles.each { echo "📦 Filesystem: ${it.path}" }
def allArtifacts = (firmwareFiles + fsFiles).collect { it.path }
archiveArtifacts artifacts: allArtifacts.join(', ')
}
}
}
}
}
stage('🔌 Flash Hardware (Dummy)') {
steps {
echo "TODO: Flash-Script aufrufen, z.B.: python3 Hardware/flash.py /dev/ttyUSB0"
}
}
stage('🧠 Run Tests (Dummy)') {
steps {
dir('Testing') {
echo "TODO: Testing mit z.B.: python3 test_runner.py oder pytest starten"
}
}
}
stage('🧹 Cleanup Build Output') {
steps {
dir('Software') {
sh "rm -rf .pio"
}
}
}
}
post {
success {
echo "✅ CI abgeschlossen Firmware gebaut, Dummy-Stages bereit"
}
failure {
echo "❌ Fehler im Build Logs checken, Commander Seraphon"
}
}
}

3
Software/.gitignore vendored
View File

@@ -3,3 +3,6 @@ data/
.vscode .vscode
wifi_credentials.ini wifi_credentials.ini
__pycache__ __pycache__
# Node-Tools für Build-Scripts
/tools_node/
/data_stripped/

View File

@@ -11,23 +11,35 @@ import platform
Import("env") Import("env")
Import("projenv") Import("projenv")
# Überprüfe die Betriebssystemplattform def ensure_node_tool(package_name, binary_name=None):
if platform.system() == "Windows": """Installiert das Tool lokal, wenn es fehlt mit npm init bei Bedarf"""
# Setze die Pfade zu den Tools für Windows if binary_name is None:
html_minifier_path = os.path.join(os.getenv("APPDATA"), "npm", "html-minifier.cmd") binary_name = package_name
uglifyjs_path = os.path.join(os.getenv("APPDATA"), "npm", "uglifyjs.cmd")
terser_path = os.path.join(os.getenv("APPDATA"), "npm", "terser.cmd")
cssnano_path = os.path.join(os.getenv("APPDATA"), "npm", "cssnano.cmd")
elif platform.system() == "Linux":
# Setze die Namen der Tools für Linux
html_minifier_path = "html-minifier"
uglifyjs_path = "uglifyjs"
terser_path = "terser"
cssnano_path = "cssnano"
else:
# Hier könntest du weitere Bedingungen für andere Betriebssysteme hinzufügen
raise Exception("Unterstütztes Betriebssystem nicht erkannt")
project_dir = env.subst('$PROJECT_DIR')
tools_dir = os.path.join(project_dir, 'tools_node')
local_bin = os.path.join(tools_dir, 'node_modules', '.bin', binary_name)
os.makedirs(tools_dir, exist_ok=True)
# Initialisiere npm, falls noch nicht geschehen
if not os.path.isfile(os.path.join(tools_dir, 'package.json')):
print("🛠️ Initializing local npm project in tools_node...")
subprocess.run(['npm', 'init', '-y'], cwd=tools_dir, check=True)
# Installiere Tool (idempotent)
try:
subprocess.run(['npm', 'install', package_name], cwd=tools_dir, check=True)
except Exception as e:
print(f"❌ Fehler beim Installieren von {package_name}: {e}")
return local_bin if os.path.isfile(local_bin) else binary_name
# Tools sicherstellen
# Tools sicherstellen Package-Name und CLI-Binary ggf. unterschiedlich
html_minifier_path = ensure_node_tool("html-minifier")
terser_path = ensure_node_tool("terser")
cssnano_path = ensure_node_tool("cssnano-cli", "cssnano")
def minify_html(input_path, output_path): def minify_html(input_path, output_path):
subprocess.run([html_minifier_path, '--collapse-whitespace', '--remove-comments', input_path, '-o', output_path]) subprocess.run([html_minifier_path, '--collapse-whitespace', '--remove-comments', input_path, '-o', output_path])
@@ -40,13 +52,7 @@ def minify_css(input_path, output_path):
def process_file(src_path, dest_path): def process_file(src_path, dest_path):
_, file_extension = os.path.splitext(src_path) _, file_extension = os.path.splitext(src_path)
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
# Extrahiere den Ordnerpfad im Zielverzeichnis
dest_dir = os.path.dirname(dest_path)
# Erstelle den Ordner und alle dazugehörigen Unterordner, falls sie nicht existieren
os.makedirs(dest_dir, exist_ok=True)
if file_extension.lower() == '.js': if file_extension.lower() == '.js':
minify_js(src_path, dest_path) minify_js(src_path, dest_path)
elif file_extension.lower() == '.css': elif file_extension.lower() == '.css':
@@ -54,128 +60,89 @@ def process_file(src_path, dest_path):
elif file_extension.lower() in ['.html', '.htm']: elif file_extension.lower() in ['.html', '.htm']:
minify_html(src_path, dest_path) minify_html(src_path, dest_path)
else: else:
# Kopiere nicht bearbeitbare Dateien direkt in den Zielordner
shutil.copy2(src_path, dest_path) shutil.copy2(src_path, dest_path)
def strip_files(src_dir, dest_dir): def strip_files(src_dir, dest_dir):
# Erstelle den Zielordner und alle dazugehörigen Unterordner, falls sie nicht existieren
os.makedirs(dest_dir, exist_ok=True) os.makedirs(dest_dir, exist_ok=True)
# Durchlaufe alle Dateien und Unterverzeichnisse im Quellordner
for root, _, files in os.walk(src_dir): for root, _, files in os.walk(src_dir):
for filename in files: for filename in files:
src_path = os.path.join(root, filename) src_path = os.path.join(root, filename)
dest_path = os.path.relpath(src_path, src_dir) rel_path = os.path.relpath(src_path, src_dir)
dest_path = os.path.join(dest_dir, dest_path) dest_path = os.path.join(dest_dir, rel_path)
# Verarbeite nur Dateien (keine Unterverzeichnisse)
process_file(src_path, dest_path) process_file(src_path, dest_path)
def gzip_file(src_path, dst_path): def gzip_file(src_path, dst_path):
with open(src_path, 'rb') as src, gzip.open(dst_path, 'wb') as dst: with open(src_path, 'rb') as src, gzip.open(dst_path, 'wb') as dst:
for chunk in iter(lambda: src.read(4096), b""): for chunk in iter(lambda: src.read(4096), b""):
dst.write(chunk) dst.write(chunk)
def getListOfFiles(dirName): def getListOfFiles(dirName):
# create a list of file and sub directories entries = os.listdir(dirName)
# names in the given directory allFiles = []
listOfFile = os.listdir(dirName) for entry in entries:
allFiles = list()
# Iterate over all the entries
for entry in listOfFile:
# Create full path
fullPath = os.path.join(dirName, entry) fullPath = os.path.join(dirName, entry)
# If entry is a directory then get the list of files in this directory
if os.path.isdir(fullPath): if os.path.isdir(fullPath):
allFiles = allFiles + getListOfFiles(fullPath) allFiles += getListOfFiles(fullPath)
else: else:
allFiles.append(fullPath) allFiles.append(fullPath)
return allFiles return allFiles
def remove_prefix(text, prefix): def safe_relpath(path, start):
if text.startswith(prefix): return os.path.relpath(path, start).replace("\\", "/")
return text[len(prefix):]
return text # or whatever
# Compress files from 'data_src/' to 'data/'
def gzip_webfiles(source, target, env): def gzip_webfiles(source, target, env):
# Filetypes to compress
filetypes_to_gzip = ['.css', '.png', '.js', '.ico', '.woff2', '.json'] filetypes_to_gzip = ['.css', '.png', '.js', '.ico', '.woff2', '.json']
print('\nGZIP: Starting gzip-Process for LittleFS-Image...\n') print('\nGZIP: Starting gzip-Process for LittleFS-Image...\n')
data_src_dir_path = os.path.join(env.get('PROJECT_DIR'), 'data_src') src_dir = os.path.join(env.get('PROJECT_DIR'), 'data_src')
data_temp_dir_path = os.path.join(env.get('PROJECT_DIR'), 'data_stripped') temp_dir = os.path.join(env.get('PROJECT_DIR'), 'data_stripped')
strip_files(data_src_dir_path, data_temp_dir_path) dst_dir = env.get('PROJECT_DATA_DIR')
data_dir_path = env.get('PROJECT_DATA_DIR')
# check if data and datasrc exist. If the first exists and not the second, it renames it strip_files(src_dir, temp_dir)
if(os.path.exists(data_dir_path) and not os.path.exists(data_temp_dir_path)):
print('GZIP: Directory "'+data_dir_path + if os.path.exists(dst_dir):
'" exists, "'+data_temp_dir_path+'" is not found.') shutil.rmtree(dst_dir)
print('GZIP: Renaming "' + data_dir_path + os.mkdir(dst_dir)
'" to "' + data_temp_dir_path + '"')
os.rename(data_dir_path, data_temp_dir_path)
# Delete the 'data' directory
if(os.path.exists(data_dir_path)):
print('GZIP: Deleting the "data" directory ' + data_dir_path)
shutil.rmtree(data_dir_path)
# Recreate empty 'data' directory
print('GZIP: Re-creating an empty data directory ' + data_dir_path)
os.mkdir(data_dir_path)
# Determine the files to compress
files_to_copy = [] files_to_copy = []
files_to_gzip = [] files_to_gzip = []
all_data_src = getListOfFiles(data_temp_dir_path) for file in getListOfFiles(temp_dir):
for file in all_data_src: _, ext = os.path.splitext(file)
file_name, file_extension = os.path.splitext(file) if ext in filetypes_to_gzip:
print(file_name + " has filetype " + file_extension)
if file_extension in filetypes_to_gzip:
files_to_gzip.append(file) files_to_gzip.append(file)
else: else:
filename_subdir = remove_prefix(file, data_temp_dir_path) files_to_copy.append(safe_relpath(file, temp_dir))
files_to_copy.append(filename_subdir)
for file in files_to_copy: for file in files_to_copy:
print('GZIP: Copying file from: ' + data_temp_dir_path + file + ' to: ' + data_dir_path + file) full_dst = os.path.join(dst_dir, file)
os.makedirs(os.path.dirname(data_dir_path + file), exist_ok=True) os.makedirs(os.path.dirname(full_dst), exist_ok=True)
shutil.copy(data_temp_dir_path + file, data_dir_path + file) shutil.copy(os.path.join(temp_dir, file), full_dst)
# Compress and move files
was_error = False was_error = False
try: try:
for source_file_path in files_to_gzip: for src in files_to_gzip:
print('GZIP: compressing... ' + source_file_path) rel_path = safe_relpath(src, temp_dir)
filename_subdir = remove_prefix(source_file_path, data_temp_dir_path) dst_path = os.path.join(dst_dir, rel_path + '.gz')
target_file_path = data_dir_path + filename_subdir os.makedirs(os.path.dirname(dst_path), exist_ok=True)
os.makedirs(os.path.dirname(target_file_path), exist_ok=True) print('GZIP: compressing... ' + rel_path)
print('GZIP: Compressed... ' + target_file_path) gzip_file(src, dst_path)
gzip_file(source_file_path, target_file_path + ".gz")
except IOError as e: except IOError as e:
was_error = True was_error = True
print('GZIP: Failed to compress file: ' + source_file_path) print('GZIP: Fehler beim Komprimieren:', e)
# print( 'GZIP: EXCEPTION... {}'.format( e ) )
if was_error:
print('GZIP: Failure/Incomplete.\n')
else:
print('GZIP: Compressed correctly.\n')
shutil.rmtree(data_temp_dir_path)
return if was_error:
print('⚠️ GZIP: Nicht alle Dateien konnten verarbeitet werden.\n')
else:
print('✅ GZIP: Komprimierung abgeschlossen.\n')
shutil.rmtree(temp_dir)
def gzip_binffiles(source, target, env): def gzip_binffiles(source, target, env):
littlefsbin = target[0].get_abspath() littlefsbin = target[0].get_abspath()
targetbin = os.path.join(os.path.dirname(littlefsbin), 'filesystem.fs') tmpbin = os.path.join(os.path.dirname(littlefsbin), 'filesystem.fs')
shutil.copyfile(littlefsbin, targetbin) shutil.copyfile(littlefsbin, tmpbin)
gzip_file(targetbin, os.path.join(str(targetbin) + '.gz')) gzip_file(tmpbin, tmpbin + '.gz')
os.remove(targetbin) os.remove(tmpbin)
return
# IMPORTANT, this needs to be added to call the routine # Hooks setzen
env.AddPreAction('$BUILD_DIR/littlefs.bin', gzip_webfiles) env.AddPreAction('$BUILD_DIR/littlefs.bin', gzip_webfiles)
env.AddPostAction('$BUILD_DIR/littlefs.bin', gzip_binffiles) env.AddPostAction('$BUILD_DIR/littlefs.bin', gzip_binffiles)

View File

@@ -349,6 +349,33 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="washdistance" class="control-label col-4">Waschmodus Distanz</label>
<div class="col-8">
<div class="input-group">
<input id="washdistance" type="text"
class="set-wsevent data-washdistance form-control" required="required">
<div class="input-group-append">
<span class="input-group-text">m</span>
</div>
</div>
</div>
</div>
<div class="form-group row">
<label for="washinterval" class="control-label col-4">Waschmodus Interval</label>
<div class="col-8">
<div class="input-group">
<input id="washinterval" type="text"
class="set-wsevent data-washinterval form-control" required="required">
<div class="input-group-append">
<span class="input-group-text">m</span>
</div>
</div>
</div>
</div>
</p> </p>
<!-- Div Group Lube Settings--> <!-- Div Group Lube Settings-->
<!-- Div Group Oiltank Settings --> <!-- Div Group Oiltank Settings -->

View File

@@ -0,0 +1,18 @@
// === button_actions.h ===
#ifndef _BUTTON_ACTIONS_H_
#define _BUTTON_ACTIONS_H_
#include <Arduino.h>
#include "buttoncontrol.h"
// Deklarationen der Button-Callbacks
void ButtonAction_ToggleMode();
void ButtonAction_StartPurge();
void ButtonAction_ToggleWiFi();
void ButtonAction_WashMode();
// Bereitstellung der Aktionsliste
extern const ButtonActionEntry buttonActions[];
extern const uint8_t buttonActionCount;
#endif

View File

@@ -0,0 +1,30 @@
// === buttoncontrol.h ===
#ifndef _BUTTONCONTROL_H_
#define _BUTTONCONTROL_H_
#include <Arduino.h>
// Aktionen, die vom Button ausgelöst werden können
enum ButtonAction
{
BTN_NONE,
BTN_CUSTOM
};
// Callback-Funktionstyp
typedef void (*ButtonCallback)();
struct ButtonActionEntry
{
uint32_t holdTimeMs;
uint32_t ledColor;
ButtonCallback callback;
};
// Initialisierung des Buttonmoduls
void ButtonControl_Init(uint8_t pin, const ButtonActionEntry *actions, uint8_t actionCount);
// Muss regelmäßig in loop() aufgerufen werden
void ButtonControl_Update();
#endif

View File

@@ -41,9 +41,10 @@
#elif PCB_REV == 4 #elif PCB_REV == 4
#define GPIO_BUTTON D4 #define GPIO_BUTTON D4
#define GPIO_LED D3 #define GPIO_LED D3
#define GPIO_TRIGGER D6 #define GPIO_TRIGGER D8
#define GPIO_PUMP D0 #define GPIO_PUMP D0
#define GPIO_CS_CAN D8 #define GPIO_CS_CAN D8
#define GPIO_CE_KLINE D8
#endif #endif
#ifndef HOST_NAME #ifndef HOST_NAME
@@ -72,6 +73,7 @@ typedef enum eSystem_Status
sysStat_Startup, sysStat_Startup,
sysStat_Normal, sysStat_Normal,
sysStat_Rain, sysStat_Rain,
sysStat_Wash,
sysStat_Purge, sysStat_Purge,
sysStat_Error, sysStat_Error,
sysStat_Shutdown sysStat_Shutdown

View File

@@ -21,7 +21,7 @@
#include "dtc.h" #include "dtc.h"
#include "common.h" #include "common.h"
#define EEPROM_STRUCTURE_REVISION 3 // Increment this version when changing EEPROM structures #define EEPROM_STRUCTURE_REVISION 4 // Increment this version when changing EEPROM structures
#if PCB_REV == 1 || PCB_REV == 2 || PCB_REV == 3 #if PCB_REV == 1 || PCB_REV == 2 || PCB_REV == 3
#define EEPROM_SIZE_BYTES I2C_DEVICESIZE_24LC64 #define EEPROM_SIZE_BYTES I2C_DEVICESIZE_24LC64
@@ -69,6 +69,8 @@ typedef struct
uint32_t RimDiameter_Inch; uint32_t RimDiameter_Inch;
uint32_t DistancePerRevolution_mm; uint32_t DistancePerRevolution_mm;
uint16_t BleedingPulses; uint16_t BleedingPulses;
uint16_t WashMode_Distance;
uint16_t WashMode_Interval;
SpeedSource_t SpeedSource; SpeedSource_t SpeedSource;
GPSBaudRate_t GPSBaudRate; GPSBaudRate_t GPSBaudRate;
CANSource_t CANSource; CANSource_t CANSource;
@@ -85,7 +87,7 @@ typedef struct
// Default configuration settings // Default configuration settings
const LubeConfig_t LubeConfig_defaults = { const LubeConfig_t LubeConfig_defaults = {
0, 8000, 4000, 320, DEFAULT_PUMP_DOSE, 30, 1, 150, 70, 18, 2000, 25, SOURCE_IMPULSE, 0, 8000, 4000, 320, DEFAULT_PUMP_DOSE, 30, 1, 150, 70, 18, 2000, 25, 500, 10, SOURCE_IMPULSE,
BAUD_115200, BAUD_115200,
KTM_890_ADV_R_2021, KTM_890_ADV_R_2021,
false, false,

View File

@@ -16,19 +16,6 @@
#include <Arduino.h> #include <Arduino.h>
#include "webui.h" #include "webui.h"
const char PROGMEM helpCmd[] = "sysinfo - System Info\n"
"netinfo - WiFi Info\n"
"formatPDS - Format Persistence EEPROM Data\n"
"formatCFG - Format Configuration EEPROM Data\n"
"checkEE - Check EEPROM with checksum\n"
"dumpEE1k - dump the first 1kb of EEPROM to Serial\n"
"dumpEE - dump the whole EPPROM to Serial\n"
"resetPageEE - Reset the PersistenceData Page\n"
"dumpCFG - print Config struct\n"
"dumpPDS - print PersistanceStruct\n"
"saveEE - save EE-Data\n"
"showdtc - Show all DTCs\n"
"dumpGlobals - print globals\n";
typedef enum DebugStatus_e typedef enum DebugStatus_e
{ {
@@ -49,6 +36,13 @@ const char sDebugPorts[dbg_cntElements][7] = {
extern DebugStatus_t DebuggerStatus[dbg_cntElements]; extern DebugStatus_t DebuggerStatus[dbg_cntElements];
enum LogLevel
{
LOG_INFO,
LOG_WARN,
LOG_ERROR
};
void initDebugger(); void initDebugger();
void pushCANDebug(uint32_t id, uint8_t dlc, uint8_t *data); void pushCANDebug(uint32_t id, uint8_t dlc, uint8_t *data);
void Debug_pushMessage(const char *format, ...); void Debug_pushMessage(const char *format, ...);

View File

@@ -37,9 +37,10 @@
#define LED_STARTUP_TANKWARN COLOR_AMBER #define LED_STARTUP_TANKWARN COLOR_AMBER
#define LED_NORMAL_COLOR COLOR_GREEN #define LED_NORMAL_COLOR COLOR_GREEN
#define LED_RAIN_COLOR COLOR_BLUE #define LED_RAIN_COLOR COLOR_BLUE
#define LED_WIFI_BLINK COLOR_YELLOW #define LED_WASH_COLOR COLOR_JADE
#define LED_WIFI_COLOR COLOR_YELLOW
#define LED_PURGE_COLOR COLOR_MAGENTA #define LED_PURGE_COLOR COLOR_MAGENTA
#define LED_ERROR_BLINK COLOR_RED #define LED_ERROR_COLOR COLOR_RED
#define LED_SHUTDOWN_BLINK COLOR_CYAN #define LED_SHUTDOWN_COLOR COLOR_CYAN
#endif /* _LED_COLORS_H_ */ #endif /* _LED_COLORS_H_ */

View File

@@ -0,0 +1,35 @@
// === ledcontrol.h ===
#ifndef _LEDCONTROL_H_
#define _LEDCONTROL_H_
#include <Arduino.h>
#include "led_colors.h"
// LED-Muster
enum LedPattern
{
LED_PATTERN_ON,
LED_PATTERN_FLASH,
LED_PATTERN_FLASH_FAST,
LED_PATTERN_BLINK,
LED_PATTERN_BLINK_FAST,
LED_PATTERN_BREATH,
LED_PATTERN_BREATH_REVERSE
};
// Initialisiert die LED-Steuerung
void LEDControl_Init(uint8_t pin);
// Setzt den Basiszustand (Farbe + Pattern), wird verwendet wenn kein Override aktiv ist
void LEDControl_SetBasic(uint32_t color, LedPattern pattern);
// Setzt ein Override mit Timeout (0 = bis explizit gecleart)
void LEDControl_SetOverride(uint32_t color, LedPattern pattern, uint32_t durationMs);
// Hebt das Override wieder auf
void LEDControl_ClearOverride();
// Muss regelmäßig aus loop() aufgerufen werden
void LEDControl_Update();
#endif

View File

@@ -3,10 +3,10 @@
* *
* @brief Header file for converting structs to JSON objects. * @brief Header file for converting structs to JSON objects.
* *
* @note This file is auto-generated by a script on 2024-01-30 20:29:34. * @note This file is auto-generated by a script on 2025-06-15 11:37:51.
* *
* @author Marcel Peterkau * @author Marcel Peterkau
* @date 30.01.2024 * @date 15.06.2025
*/ */
#ifndef _STRUCT2JSON_H_ #ifndef _STRUCT2JSON_H_
@@ -23,4 +23,4 @@ void generateJsonObject_PersistenceData(JsonObject data);
#endif /* _STRUCT2JSON_H_ */ #endif /* _STRUCT2JSON_H_ */
// CODEGENERATOR_CHECKSUM: 59f35aadffd0bbef253210ea2fbaaf9a515553a2e3cc9bf4cfa2819b63c969ce // CODEGENERATOR_CHECKSUM: 4702cb49ea55617cbb34715164810bb58d3c3f46fb1653b6f47bd4fd9cb0031e

View File

@@ -11,7 +11,7 @@
[platformio] [platformio]
extra_configs = extra_configs =
wifi_credentials.ini wifi_credentials.ini
default_envs = pcb_rev_1-3_serial, pcb_rev_1-3_ota, pcb_rev_1-2_serial, pcb_rev_1-2_ota default_envs = pcb_rev_1-4_serial
[env] [env]
platform = espressif8266 platform = espressif8266
@@ -42,15 +42,36 @@ monitor_speed = 115200
lib_ldf_mode = deep lib_ldf_mode = deep
lib_deps = lib_deps =
olikraus/U8g2 @ ^2.35.9 olikraus/U8g2 @ ^2.36.5
adafruit/Adafruit NeoPixel @ ^1.12.0 adafruit/Adafruit NeoPixel @ ^1.15.1
sstaub/Ticker @ ^4.4.0 sstaub/Ticker @ ^4.4.0
robtillaart/I2C_EEPROM @ ^1.8.2 robtillaart/I2C_EEPROM @ ^1.9.2
esphome/ESPAsyncWebServer-esphome @ 3.1.0 esphome/ESPAsyncWebServer-esphome @ 3.3.0
bblanchon/ArduinoJson @ ^7.0.1 bblanchon/ArduinoJson @ ^7.4.1
coryjfowler/mcp_can @ ^1.5.0 coryjfowler/mcp_can @ ^1.5.1
mikalhart/TinyGPSPlus @ ^1.0.3 mikalhart/TinyGPSPlus @ ^1.1.0
[env:pcb_rev_1-4_serial]
extends = env
custom_pcb_revision = 4
upload_protocol = esptool
build_flags =
${env.build_flags}
-DPCB_REV=${this.custom_pcb_revision}
board_build.ldscript = eagle.flash.4m1m.ld
[env:pcb_rev_1-4_ota]
extends = env
custom_pcb_revision = 4
upload_protocol = espota
upload_port = 10.0.1.14
upload_flags =
--port=8266
--auth=${wifi_cred.admin_password}
build_flags =
${env.build_flags}
-DPCB_REV=${this.custom_pcb_revision}
board_build.ldscript = eagle.flash.4m1m.ld
[env:pcb_rev_1-3_serial] [env:pcb_rev_1-3_serial]
extends = env extends = env

View File

@@ -0,0 +1,47 @@
// === button_actions.cpp ===
#include "button_actions.h"
#include "globals.h"
#include "debugger.h"
#include "led_colors.h"
void ButtonAction_ToggleMode()
{
if (globals.systemStatus == sysStat_Normal)
{
globals.systemStatus = sysStat_Rain;
}
else if (globals.systemStatus == sysStat_Rain)
{
globals.systemStatus = sysStat_Normal;
}
Debug_pushMessage("Toggling Mode\n");
}
void ButtonAction_StartPurge()
{
globals.systemStatus = sysStat_Purge;
Debug_pushMessage("Starting Purge\n");
}
void ButtonAction_ToggleWiFi()
{
extern void toggleWiFiAP(bool shutdown = false);
toggleWiFiAP();
Debug_pushMessage("Toggling WiFi\n");
}
void ButtonAction_WashMode()
{
globals.systemStatus = sysStat_Wash;
Debug_pushMessage("Setting WashMode\n");
}
// Liste der Aktionen, sortiert nach Mindest-Haltezeit (ms)
const ButtonActionEntry buttonActions[] = {
{500, LED_RAIN_COLOR, ButtonAction_ToggleMode},
{3500, LED_PURGE_COLOR, ButtonAction_StartPurge},
{6500, LED_WIFI_COLOR, ButtonAction_ToggleWiFi},
{9500, LED_WASH_COLOR, ButtonAction_WashMode}};
const uint8_t buttonActionCount = sizeof(buttonActions) / sizeof(ButtonActionEntry);

View File

@@ -0,0 +1,64 @@
// === buttoncontrol.cpp ===
#include "buttoncontrol.h"
#include "ledcontrol.h"
static uint8_t btnPin;
static uint32_t pressStart = 0;
static bool pressed = false;
static const ButtonActionEntry *btnActions = nullptr;
static uint8_t btnActionCount = 0;
static uint8_t currentActionIndex = 0xFF;
static uint32_t lastColor = 0;
void ButtonControl_Init(uint8_t pin, const ButtonActionEntry *actions, uint8_t actionCount)
{
btnPin = pin;
pinMode(btnPin, INPUT_PULLUP);
btnActions = actions;
btnActionCount = actionCount;
}
void ButtonControl_Update()
{
bool currentState = digitalRead(btnPin) == LOW;
uint32_t now = millis();
if (currentState && !pressed)
{
pressStart = now;
pressed = true;
currentActionIndex = 0xFF;
lastColor = 0;
}
else if (currentState && pressed)
{
uint32_t duration = now - pressStart;
// Finde passende Aktion basierend auf Zeit
for (uint8_t i = 0; i < btnActionCount; i++)
{
if (duration >= btnActions[i].holdTimeMs)
{
if (currentActionIndex != i)
{
currentActionIndex = i;
lastColor = btnActions[i].ledColor;
// Farbe + Pattern setzen
LEDControl_SetOverride(lastColor, LED_PATTERN_BREATH, 0); // Kein Timeout, wird bei Release beendet
}
}
}
}
else if (!currentState && pressed)
{
pressed = false;
if (currentActionIndex != 0xFF && currentActionIndex < btnActionCount)
{
if (btnActions[currentActionIndex].callback)
{
btnActions[currentActionIndex].callback();
}
}
LEDControl_ClearOverride(); // Override-Modus zurücksetzen
}
}

View File

@@ -11,7 +11,10 @@
* @date 09.04.2024 * @date 09.04.2024
*/ */
#include "debugger.h" #include "debugger.h"
#include <map>
#include <functional>
#include <vector>
DebugStatus_t DebuggerStatus[dbg_cntElements]; DebugStatus_t DebuggerStatus[dbg_cntElements];
@@ -27,6 +30,7 @@ void Debug_ShowDTCs();
void Debug_dumpGlobals(); void Debug_dumpGlobals();
void Debug_printHelp(); void Debug_printHelp();
void Debug_Purge(); void Debug_Purge();
void Debug_refillTank();
const char *uint32_to_binary_string(uint32_t num); const char *uint32_to_binary_string(uint32_t num);
/** /**
@@ -94,10 +98,9 @@ void Debug_Process()
} }
// Check for input buffer overflow // Check for input buffer overflow
if (inputCnt > sizeof(inputBuffer)) if (inputCnt >= sizeof(inputBuffer) - 1) {
{ inputBuffer[sizeof(inputBuffer) - 1] = '\0';
inputCnt = 0; inputCnt = 0;
inputBuffer[sizeof(inputBuffer) - 1] = 0; // terminate the String
InputProcessed = CMD_OVERFLOW; InputProcessed = CMD_OVERFLOW;
} }
} }
@@ -147,6 +150,29 @@ void SetDebugportStatus(DebugPorts_t port, DebugStatus_t status)
Debug_pushMessage("Enabled DebugPort %s\n", sDebugPorts[port]); Debug_pushMessage("Enabled DebugPort %s\n", sDebugPorts[port]);
} }
void Debug_log(LogLevel level, const char *format, ...)
{
if ((DebuggerStatus[dbg_Serial] == enabled) || (DebuggerStatus[dbg_Webui] == enabled))
{
char buff[128];
va_list arg;
va_start(arg, format);
vsnprintf(buff, sizeof(buff), format, arg);
va_end(arg);
if (DebuggerStatus[dbg_Serial] == enabled)
{
Serial.print(buff);
}
if (DebuggerStatus[dbg_Webui] == enabled)
{
Websocket_PushLiveDebug(String(buff));
}
}
}
/** /**
* @brief Pushes a formatted debug message to the enabled debug ports (Serial or WebUI). * @brief Pushes a formatted debug message to the enabled debug ports (Serial or WebUI).
* *
@@ -218,68 +244,100 @@ void pushCANDebug(uint32_t id, uint8_t dlc, uint8_t *data)
} }
} }
/** // === splitArgs Helper ===
* @brief Processes a debug command and performs corresponding actions. std::vector<String> splitArgs(const String &input)
*
* @param command The debug command to be processed.
*/
void processCmdDebug(String command)
{ {
// Check the received command and execute corresponding actions std::vector<String> tokens;
if (command == "help") int start = 0, end = 0;
Debug_printHelp(); while ((end = input.indexOf(' ', start)) != -1)
else if (command == "sysinfo") {
Debug_printSystemInfo(); tokens.push_back(input.substring(start, end));
else if (command == "netinfo") start = end + 1;
Debug_printWifiInfo(); }
else if (command == "formatCFG") if (start < input.length())
Debug_formatCFG(); tokens.push_back(input.substring(start));
else if (command == "formatPDS") return tokens;
Debug_formatPersistence(); }
else if (command == "checkEE")
Debug_CheckEEPOM(false); // === getArg helper ===
else if (command == "checkEEfix") String getArg(const std::vector<String> &args, size_t index, const String &defaultVal = "")
Debug_CheckEEPOM(true); {
else if (command == "dumpEE1k") if (index < args.size())
dumpEEPROM(0, 1024); return args[index];
else if (command == "dumpEE") return defaultVal;
dumpEEPROM(0, EEPROM_SIZE_BYTES); }
else if (command == "resetPageEE")
MovePersistencePage_EEPROM(true); // === Command Handler Map ===
else if (command == "dumpCFG") typedef std::function<void(const String &args)> DebugCmdHandler;
Debug_dumpConfig();
else if (command == "dumpPDS") static const std::map<String, DebugCmdHandler> &getCmdMap()
Debug_dumpPersistance(); {
else if (command == "saveEE") static const std::map<String, DebugCmdHandler> cmdMap = {
globals.requestEEAction = EE_ALL_SAVE; {"help", [](const String &) {
else if (command == "dumpGlobals") Debug_log(LOG_INFO, "Available commands:\n");
Debug_dumpGlobals(); for (const auto &entry : getCmdMap())
else if (command == "sdbg") Debug_log(LOG_INFO, " - %s\n", entry.first.c_str());
SetDebugportStatus(dbg_Serial, enabled); }},
else if (command == "dtc_show") {"sysinfo", [](const String &) { Debug_printSystemInfo(); }},
Debug_ShowDTCs(); {"netinfo", [](const String &) { Debug_printWifiInfo(); }},
else if (command == "dtc_clear") {"formatCFG", [](const String &) { Debug_formatCFG(); }},
ClearAllDTC(); {"formatPDS", [](const String &) { Debug_formatPersistence(); }},
else if (command == "dtc_crit") {"checkEE", [](const String &) { Debug_CheckEEPOM(false); }},
MaintainDTC(DTC_FAKE_DTC_CRIT, true, millis()); {"checkEEfix", [](const String &) { Debug_CheckEEPOM(true); }},
else if (command == "dtc_warn") {"dumpEE1k", [](const String &) { dumpEEPROM(0, 1024); }},
MaintainDTC(DTC_FAKE_DTC_WARN, true, millis()); {"dumpEE", [](const String &args) {
else if (command == "dtc_info") int start = 0, len = EEPROM_SIZE_BYTES;
MaintainDTC(DTC_FAKE_DTC_INFO, true, millis()); auto tokens = splitArgs(args);
else if (command == "notify_error") if (tokens.size() >= 2)
Websocket_PushNotification("Debug Error Notification", error); {
else if (command == "notify_warning") start = tokens[0].toInt();
Websocket_PushNotification("Debug Warning Notification", warning); len = tokens[1].toInt();
else if (command == "notify_success") }
Websocket_PushNotification("Debug Success Notification", success); dumpEEPROM(start, len);
else if (command == "notify_info") }},
Websocket_PushNotification("Debug Info Notification", info); {"resetPageEE", [](const String &) { MovePersistencePage_EEPROM(true); }},
else if (command == "purge") {"dumpCFG", [](const String &) { Debug_dumpConfig(); }},
Debug_Purge(); {"dumpPDS", [](const String &) { Debug_dumpPersistance(); }},
else if (command == "toggle_wifi") {"saveEE", [](const String &) { globals.requestEEAction = EE_ALL_SAVE; }},
globals.toggle_wifi = true; {"dumpGlobals", [](const String &) { Debug_dumpGlobals(); }},
{"sdbg", [](const String &) { SetDebugportStatus(dbg_Serial, enabled); }},
{"dtc_show", [](const String &) { Debug_ShowDTCs(); }},
{"dtc_clear", [](const String &) { ClearAllDTC(); }},
{"dtc_crit", [](const String &) { MaintainDTC(DTC_FAKE_DTC_CRIT, true, millis()); }},
{"dtc_warn", [](const String &) { MaintainDTC(DTC_FAKE_DTC_WARN, true, millis()); }},
{"dtc_info", [](const String &) { MaintainDTC(DTC_FAKE_DTC_INFO, true, millis()); }},
{"notify_error", [](const String &) { Websocket_PushNotification("Debug Error Notification", error); }},
{"notify_warning", [](const String &) { Websocket_PushNotification("Debug Warning Notification", warning); }},
{"notify_success", [](const String &) { Websocket_PushNotification("Debug Success Notification", success); }},
{"notify_info", [](const String &) { Websocket_PushNotification("Debug Info Notification", info); }},
{"purge", [](const String &) { Debug_Purge(); }},
{"toggle_wifi", [](const String &) { globals.toggle_wifi = true; }},
{"dtc_add", [](const String &args) {
auto tokens = splitArgs(args);
if (!tokens.empty())
{
int code = tokens[0].toInt();
MaintainDTC((DTCNum_t)code, true, millis());
}
}},
{"tank_refill", [](const String &) { Debug_refillTank(); }},
};
return cmdMap;
}
void processCmdDebug(String input)
{
input.trim();
int splitIndex = input.indexOf(' ');
String command = splitIndex == -1 ? input : input.substring(0, splitIndex);
String args = splitIndex == -1 ? "" : input.substring(splitIndex + 1);
auto &cmdMap = getCmdMap();
auto it = cmdMap.find(command);
if (it != cmdMap.end())
it->second(args);
else else
Debug_pushMessage("unknown Command\n"); Debug_log(LOG_WARN, "Unknown command: '%s'\n", command.c_str());
} }
/** /**
@@ -473,25 +531,6 @@ void Debug_ShowDTCs()
} }
} }
/**
* @brief Displays the help commands for debugging through Serial or WebUI.
* Each command is printed individually in a formatted manner.
*/
void Debug_printHelp()
{
char buff[64];
// Iterate through helpCmd and display each command
for (unsigned int i = 0; i < sizeof(helpCmd) / 63; i++)
{
// Copy a portion of helpCmd to buff for display
memcpy_P(buff, (helpCmd + (i * 63)), 63);
buff[63] = 0;
// Display the help command
Debug_pushMessage(buff);
}
}
/** /**
* @brief Start purging for 10 pulses. * @brief Start purging for 10 pulses.
@@ -505,6 +544,13 @@ void Debug_Purge()
Debug_pushMessage("Purging 10 pulses\n"); Debug_pushMessage("Purging 10 pulses\n");
} }
void Debug_refillTank()
{
PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000;
globals.requestEEAction = EE_PDS_SAVE;
Debug_pushMessage("Setting Tank to 100%\n");
}
/** /**
* @brief Convert a uint32_t value to a binary string with nibbles separated by a space. * @brief Convert a uint32_t value to a binary string with nibbles separated by a space.
* *

131
Software/src/ledcontrol.cpp Normal file
View File

@@ -0,0 +1,131 @@
// === ledcontrol.cpp ===
#include "ledcontrol.h"
#include <Adafruit_NeoPixel.h>
#include "globals.h"
static Adafruit_NeoPixel leds(1, GPIO_LED, NEO_RGB + NEO_KHZ800);
static uint32_t basicColor = 0x000000;
static LedPattern basicPattern = LED_PATTERN_ON;
static uint32_t overrideColor = 0;
static LedPattern overridePattern = LED_PATTERN_ON;
static uint32_t overrideEndTime = 0;
static bool overrideActive = false;
void LEDControl_Init(uint8_t pin)
{
leds.begin();
leds.setBrightness(LubeConfig.LED_Max_Brightness);
leds.setPixelColor(0, 0);
leds.show();
}
void LEDControl_SetBasic(uint32_t color, LedPattern pattern)
{
basicColor = color;
basicPattern = pattern;
}
void LEDControl_SetOverride(uint32_t color, LedPattern pattern, uint32_t durationMs)
{
overrideColor = color;
overridePattern = pattern;
overrideEndTime = millis() + durationMs;
overrideActive = true;
if (durationMs == 0)
overrideEndTime = 0xFFFFFFFF; // Kein Timeout
}
void LEDControl_ClearOverride()
{
overrideActive = false;
overrideEndTime = 0;
}
void LEDControl_Update()
{
uint32_t now = millis();
uint32_t color = basicColor;
LedPattern pattern = basicPattern;
// Check override
if (overrideActive)
{
if (overrideEndTime != 0xFFFFFFFF && now >= overrideEndTime)
{
LEDControl_ClearOverride();
}
else
{
color = overrideColor;
pattern = overridePattern;
}
}
uint8_t brightness = LubeConfig.LED_Min_Brightness;
bool on = true;
switch (pattern)
{
case LED_PATTERN_ON:
brightness = LubeConfig.LED_Max_Brightness;
break;
case LED_PATTERN_FLASH:
on = (now % 1000) < 100;
brightness = LubeConfig.LED_Max_Brightness;
break;
case LED_PATTERN_FLASH_FAST:
on = (now % 500) < 50;
brightness = LubeConfig.LED_Max_Brightness;
break;
case LED_PATTERN_BLINK:
on = (now % 1000) < 500;
brightness = on ? LubeConfig.LED_Max_Brightness : 0;
break;
case LED_PATTERN_BLINK_FAST:
on = (now % 400) < 200;
brightness = on ? LubeConfig.LED_Max_Brightness : 0;
break;
case LED_PATTERN_BREATH:
{
uint32_t t = now % 2000;
if (t < 600)
{
// Schnell hochdimmen (600 ms)
brightness = map(t, 0, 600, LubeConfig.LED_Min_Brightness, LubeConfig.LED_Max_Brightness);
}
else
{
// Langsam runterdimmen (1400 ms)
brightness = map(t, 600, 2000, LubeConfig.LED_Max_Brightness, LubeConfig.LED_Min_Brightness);
}
break;
}
case LED_PATTERN_BREATH_REVERSE:
{
uint32_t t = now % 2000;
if (t < 1400)
{
// Langsam hochdimmen (1400 ms)
brightness = map(t, 0, 1400, LubeConfig.LED_Min_Brightness, LubeConfig.LED_Max_Brightness);
}
else
{
// Schnell runterdimmen (600 ms)
brightness = map(t, 1400, 2000, LubeConfig.LED_Max_Brightness, LubeConfig.LED_Min_Brightness);
}
break;
}
}
leds.setBrightness(brightness);
leds.setPixelColor(0, on ? color : 0);
leds.show();
}

View File

@@ -12,6 +12,7 @@
*/ */
#include "lubeapp.h" #include "lubeapp.h"
#include "ledcontrol.h"
uint32_t lubePulseTimestamp = 0; uint32_t lubePulseTimestamp = 0;
@@ -27,6 +28,9 @@ uint32_t lubePulseTimestamp = 0;
*/ */
void RunLubeApp(uint32_t add_milimeters) void RunLubeApp(uint32_t add_milimeters)
{ {
static tSystem_Status lastSystemStatus = sysStat_Startup;
static uint16_t washModeDistance = 0;
// Calculate and update tank percentage // Calculate and update tank percentage
globals.TankPercentage = PersistenceData.tankRemain_microL / (LubeConfig.tankCapacity_ml * 10); globals.TankPercentage = PersistenceData.tankRemain_microL / (LubeConfig.tankCapacity_ml * 10);
@@ -49,17 +53,31 @@ void RunLubeApp(uint32_t add_milimeters)
switch (globals.systemStatus) switch (globals.systemStatus)
{ {
case sysStat_Startup: case sysStat_Startup:
if (lastSystemStatus != globals.systemStatus)
{
strcpy_P(globals.systemStatustxt, PSTR("Startup")); strcpy_P(globals.systemStatustxt, PSTR("Startup"));
LEDControl_SetBasic(LED_STARTUP_NORMAL, LED_PATTERN_BLINK);
lastSystemStatus = globals.systemStatus;
globals.resumeStatus = sysStat_Startup;
}
// Transition to Normal status after startup delay // Transition to Normal status after startup delay
if (millis() > STARTUP_DELAY) if (millis() > STARTUP_DELAY)
{ {
globals.systemStatus = sysStat_Normal; globals.systemStatus = sysStat_Normal;
globals.resumeStatus = sysStat_Normal;
} }
break; break;
case sysStat_Normal: case sysStat_Normal:
if (lastSystemStatus != globals.systemStatus)
{
strcpy_P(globals.systemStatustxt, PSTR("Normal")); strcpy_P(globals.systemStatustxt, PSTR("Normal"));
LEDControl_SetBasic(LED_NORMAL_COLOR, LED_PATTERN_ON);
lastSystemStatus = globals.systemStatus;
globals.resumeStatus = sysStat_Normal;
}
// Trigger lube pulse if traveled distance exceeds the configured limit // Trigger lube pulse if traveled distance exceeds the configured limit
if (PersistenceData.TravelDistance_highRes_mm / 1000 > LubeConfig.DistancePerLube_Default) if (PersistenceData.TravelDistance_highRes_mm / 1000 > LubeConfig.DistancePerLube_Default)
{ {
@@ -69,7 +87,14 @@ void RunLubeApp(uint32_t add_milimeters)
break; break;
case sysStat_Rain: case sysStat_Rain:
if (lastSystemStatus != globals.systemStatus)
{
strcpy_P(globals.systemStatustxt, PSTR("Rain")); strcpy_P(globals.systemStatustxt, PSTR("Rain"));
LEDControl_SetBasic(LED_RAIN_COLOR, LED_PATTERN_ON);
lastSystemStatus = globals.systemStatus;
globals.resumeStatus = sysStat_Rain;
}
// Trigger lube pulse if traveled distance exceeds the configured limit in Rain mode // Trigger lube pulse if traveled distance exceeds the configured limit in Rain mode
if (PersistenceData.TravelDistance_highRes_mm / 1000 > LubeConfig.DistancePerLube_Rain) if (PersistenceData.TravelDistance_highRes_mm / 1000 > LubeConfig.DistancePerLube_Rain)
{ {
@@ -78,8 +103,41 @@ void RunLubeApp(uint32_t add_milimeters)
} }
break; break;
case sysStat_Wash:
if (lastSystemStatus != globals.systemStatus)
{
washModeDistance = LubeConfig.WashMode_Distance;
strcpy_P(globals.systemStatustxt, PSTR("Wash"));
LEDControl_SetBasic(LED_WASH_COLOR, LED_PATTERN_BREATH);
lastSystemStatus = globals.systemStatus;
}
// Trigger lube pulse if traveled distance exceeds the configured Interval in Wash mode
if (PersistenceData.TravelDistance_highRes_mm / 1000 > LubeConfig.WashMode_Interval)
{
LubePulse();
PersistenceData.TravelDistance_highRes_mm = 0;
if (washModeDistance >= LubeConfig.WashMode_Distance)
{
washModeDistance = washModeDistance - LubeConfig.WashMode_Interval;
}
else
{
globals.systemStatus = globals.resumeStatus;
}
}
break;
case sysStat_Purge: case sysStat_Purge:
if (lastSystemStatus != globals.systemStatus)
{
globals.purgePulses = LubeConfig.BleedingPulses;
strcpy_P(globals.systemStatustxt, PSTR("Purge")); strcpy_P(globals.systemStatustxt, PSTR("Purge"));
LEDControl_SetBasic(LED_PURGE_COLOR, LED_PATTERN_BLINK);
lastSystemStatus = globals.systemStatus;
}
// Execute lube pulses during the Purge status // Execute lube pulses during the Purge status
if (globals.purgePulses > 0) if (globals.purgePulses > 0)
{ {
@@ -99,12 +157,26 @@ void RunLubeApp(uint32_t add_milimeters)
break; break;
case sysStat_Error: case sysStat_Error:
if (lastSystemStatus != globals.systemStatus)
{
strcpy_P(globals.systemStatustxt, PSTR("Error")); strcpy_P(globals.systemStatustxt, PSTR("Error"));
LEDControl_SetBasic(LED_ERROR_COLOR, LED_PATTERN_BLINK_FAST);
lastSystemStatus = globals.systemStatus;
}
globals.purgePulses = 0; globals.purgePulses = 0;
break; break;
case sysStat_Shutdown: case sysStat_Shutdown:
if (lastSystemStatus != globals.systemStatus)
{
strcpy_P(globals.systemStatustxt, PSTR("Shutdown")); strcpy_P(globals.systemStatustxt, PSTR("Shutdown"));
LEDControl_SetBasic(LED_SHUTDOWN_COLOR, LED_PATTERN_BREATH_REVERSE);
lastSystemStatus = globals.systemStatus;
}
break; break;
default: default:

View File

@@ -39,6 +39,10 @@
#include "led_colors.h" #include "led_colors.h"
#include "obd2_kline.h" #include "obd2_kline.h"
#include "obd2_can.h" #include "obd2_can.h"
#include "buttoncontrol.h"
#include "button_actions.h"
#include "ledcontrol.h"
#ifdef FEATURE_ENABLE_WIFI_CLIENT #ifdef FEATURE_ENABLE_WIFI_CLIENT
#include <ESP8266WiFiMulti.h> #include <ESP8266WiFiMulti.h>
@@ -59,12 +63,10 @@ Adafruit_NeoPixel leds(1, GPIO_LED, NEO_RGB + NEO_KHZ800);
// Function-Prototypes // Function-Prototypes
void IRAM_ATTR trigger_ISR(); void IRAM_ATTR trigger_ISR();
void LED_Process(uint8_t override = false, uint32_t setColor = LED_DEFAULT_COLOR);
#ifdef FEATURE_ENABLE_OLED #ifdef FEATURE_ENABLE_OLED
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(-1); U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(-1);
void Display_Process(); void Display_Process();
#endif #endif
void Button_Process();
void toggleWiFiAP(bool shutdown = false); void toggleWiFiAP(bool shutdown = false);
void SystemShutdown(bool restart = false); void SystemShutdown(bool restart = false);
uint32_t Process_Impulse_WheelSpeed(); uint32_t Process_Impulse_WheelSpeed();
@@ -134,7 +136,7 @@ void setup()
Serial.print("\nEE-Init done"); Serial.print("\nEE-Init done");
// Initialize LEDs // Initialize LEDs
leds.begin(); LEDControl_Init(GPIO_LED);
Serial.print("\nLED-Init done"); Serial.print("\nLED-Init done");
// Initialize based on the chosen speed source (CAN, GPS, Impulse) // Initialize based on the chosen speed source (CAN, GPS, Impulse)
@@ -175,6 +177,8 @@ void setup()
pinMode(GPIO_BUTTON, INPUT_PULLUP); pinMode(GPIO_BUTTON, INPUT_PULLUP);
pinMode(GPIO_PUMP, OUTPUT); pinMode(GPIO_PUMP, OUTPUT);
ButtonControl_Init(GPIO_BUTTON, buttonActions, buttonActionCount);
// Set up OTA updates // Set up OTA updates
ArduinoOTA.setPort(8266); ArduinoOTA.setPort(8266);
ArduinoOTA.setHostname(globals.DeviceName); ArduinoOTA.setHostname(globals.DeviceName);
@@ -249,8 +253,8 @@ void loop()
// Process button input, manage LED behavior, perform EEPROM tasks, handle webserver operations, // Process button input, manage LED behavior, perform EEPROM tasks, handle webserver operations,
// process Diagnostic Trouble Codes (DTC), and manage debugging // process Diagnostic Trouble Codes (DTC), and manage debugging
Button_Process(); ButtonControl_Update();
LED_Process(); LEDControl_Update();
EEPROM_Process(); EEPROM_Process();
Webserver_Process(); Webserver_Process();
DTC_Process(); DTC_Process();
@@ -340,208 +344,6 @@ void trigger_ISR()
wheel_pulse++; wheel_pulse++;
} }
/**
* @brief Manages LED behavior based on the current system status and user overrides.
*
* This function handles LED behavior, including startup animations, confirmation animations for
* normal and rain modes, indication for purge, error, shutdown, and normal operation. It supports
* user overrides to set a specific LED color. The LED status is determined by the current system
* status, and specific LED patterns are displayed accordingly.
*
* @param override Flag indicating whether to override the LED behavior (0: No override, 1: Override, 2: Resume previous state).
* @param SetColor The color to set when overriding the LED behavior.
*/
void LED_Process(uint8_t override, uint32_t SetColor)
{
// Enumeration to represent LED status
typedef enum
{
LED_Startup,
LED_Normal,
LED_Confirm_Normal,
LED_Rain,
LED_Confirm_Rain,
LED_Purge,
LED_Error,
LED_Shutdown,
LED_Override
} tLED_Status;
// Static variables to track LED status, system status, override color, and previous LED status
static tSystem_Status oldSysStatus = sysStat_Startup;
static tLED_Status LED_Status = LED_Startup;
static uint32_t LED_override_color = 0;
static tLED_Status LED_ResumeOverrideStatus = LED_Startup;
// Variables for managing LED animation timing
uint8_t color = 0;
uint32_t timer = 0;
uint32_t animtimer = 0;
static uint32_t timestamp = 0;
timer = millis();
// Handle LED overrides
if (override == 1)
{
if (LED_Status != LED_Override)
{
LED_ResumeOverrideStatus = LED_Status;
Debug_pushMessage("Override LED_Status\n");
}
LED_Status = LED_Override;
LED_override_color = SetColor;
}
if (override == 2)
{
if (LED_Status == LED_Override)
{
LED_Status = LED_ResumeOverrideStatus;
Debug_pushMessage("Resume LED_Status\n");
}
}
// Update LED status when system status changes
if (oldSysStatus != globals.systemStatus)
{
switch (globals.systemStatus)
{
case sysStat_Startup:
LED_Status = LED_Startup;
Debug_pushMessage("sysStat: Startup\n");
break;
case sysStat_Normal:
timestamp = timer + 3500;
LED_Status = LED_Confirm_Normal;
Debug_pushMessage("sysStat: Normal\n");
break;
case sysStat_Rain:
timestamp = timer + 3500;
LED_Status = LED_Confirm_Rain;
Debug_pushMessage("sysStat: Rain\n");
break;
case sysStat_Purge:
LED_Status = LED_Purge;
Debug_pushMessage("sysStat: Purge\n");
break;
case sysStat_Error:
LED_Status = LED_Error;
Debug_pushMessage("sysStat: Error\n");
break;
case sysStat_Shutdown:
LED_Status = LED_Shutdown;
Debug_pushMessage("sysStat: Shutdown\n");
break;
default:
break;
}
oldSysStatus = globals.systemStatus;
}
// Handle different LED statuses
switch (LED_Status)
{
case LED_Startup:
leds.setBrightness(LubeConfig.LED_Max_Brightness);
if (globals.TankPercentage < LubeConfig.TankRemindAtPercentage)
leds.setPixelColor(0, LED_STARTUP_TANKWARN);
else
leds.setPixelColor(0, LED_STARTUP_NORMAL);
break;
case LED_Confirm_Normal:
animtimer = timer % 500;
color = map(animtimer / 2, 0, 250, 0, LubeConfig.LED_Max_Brightness);
leds.setPixelColor(0, LED_NORMAL_COLOR);
if (animtimer < 250)
leds.setBrightness(color);
else
leds.setBrightness(LubeConfig.LED_Max_Brightness - color);
if (timestamp < timer)
{
LED_Status = LED_Normal;
Debug_pushMessage("LED_Status: Confirm -> Normal\n");
}
break;
case LED_Normal:
leds.setBrightness(LubeConfig.LED_Min_Brightness);
leds.setPixelColor(0, LED_NORMAL_COLOR);
if (timer % 2000 > 1950 && LubeConfig.LED_Mode_Flash == true)
leds.setBrightness(LubeConfig.LED_Max_Brightness);
else if (timer % 2000 > 1500 && WiFi.getMode() != WIFI_OFF)
leds.setPixelColor(0, LED_WIFI_BLINK);
break;
case LED_Confirm_Rain:
animtimer = timer % 500;
color = map(animtimer / 2, 0, 250, 0, LubeConfig.LED_Max_Brightness);
leds.setPixelColor(0, LED_RAIN_COLOR);
if (animtimer < 250)
leds.setBrightness(color);
else
leds.setBrightness(LubeConfig.LED_Max_Brightness - color);
if (timestamp < timer)
{
LED_Status = LED_Rain;
Debug_pushMessage("LED_Status: Confirm -> Rain\n");
}
break;
case LED_Rain:
leds.setBrightness(LubeConfig.LED_Min_Brightness);
leds.setPixelColor(0, LED_RAIN_COLOR);
if (timer % 2000 > 1950 && LubeConfig.LED_Mode_Flash == true)
leds.setBrightness(LubeConfig.LED_Max_Brightness);
else if (timer % 2000 > 1500 && WiFi.getMode() != WIFI_OFF)
leds.setPixelColor(0, LED_WIFI_BLINK);
break;
case LED_Purge:
timer = timer % 500;
color = map(timer / 2, 0, 250, LubeConfig.LED_Min_Brightness, LubeConfig.LED_Max_Brightness);
leds.setPixelColor(0, LED_PURGE_COLOR);
if (timer < 250)
leds.setBrightness(color);
else
leds.setBrightness(LubeConfig.LED_Max_Brightness - color);
break;
case LED_Error:
leds.setBrightness(LubeConfig.LED_Max_Brightness);
leds.setPixelColor(0, timer % 500 > 250 ? LED_ERROR_BLINK : 0);
break;
case LED_Shutdown:
timer = timer % 600;
leds.setPixelColor(0, LED_SHUTDOWN_BLINK);
if (timer < 500)
{
color = map(timer, 0, 500, LubeConfig.LED_Max_Brightness, LubeConfig.LED_Min_Brightness);
leds.setBrightness(color);
}
else
{
leds.setBrightness(LubeConfig.LED_Min_Brightness);
}
break;
case LED_Override:
leds.setBrightness(LubeConfig.LED_Max_Brightness);
leds.setPixelColor(0, LED_override_color);
break;
default:
break;
}
leds.show();
}
#ifdef FEATURE_ENABLE_OLED #ifdef FEATURE_ENABLE_OLED
/** /**
@@ -597,119 +399,6 @@ void Display_Process()
} }
#endif #endif
/**
* @brief Processes the button input and performs corresponding actions based on button state and timing.
*
* This function handles the button input, detecting button presses and executing actions based on
* predefined time delays. Actions include toggling WiFi, starting purge, toggling operating modes,
* and displaying feedback through LEDs. The function utilizes an enumeration to track button actions
* and manages the timing for different actions.
*/
void Button_Process()
{
// Time delays for different button actions
#define BUTTON_ACTION_DELAY_TOGGLEMODE 500
#define BUTTON_ACTION_DELAY_PURGE 3500
#define BUTTON_ACTION_DELAY_WIFI 6500
#define BUTTON_ACTION_DELAY_NOTHING 9500
// Enumeration to represent button actions
typedef enum buttonAction_e
{
BTN_INACTIVE,
BTN_NOTHING,
BTN_TOGGLEMODE,
BTN_TOGGLEWIFI,
BTN_STARTPURGE
} buttonAction_t;
// Static variables to track button state and timing
static uint32_t buttonTimestamp = 0;
static buttonAction_t buttonAction = BTN_INACTIVE;
// Check if button is pressed (LOW)
if (digitalRead(GPIO_BUTTON) == LOW)
{
// Update button timestamp on the first button press
if (buttonTimestamp == 0)
buttonTimestamp = millis();
// Check and execute actions based on predefined time delays
if (buttonTimestamp + BUTTON_ACTION_DELAY_NOTHING < millis())
{
LED_Process(1, COLOR_WARM_WHITE);
buttonAction = BTN_NOTHING;
}
else if (buttonTimestamp + BUTTON_ACTION_DELAY_WIFI < millis())
{
LED_Process(1, LED_WIFI_BLINK);
buttonAction = BTN_TOGGLEWIFI;
}
else if (buttonTimestamp + BUTTON_ACTION_DELAY_PURGE < millis())
{
LED_Process(1, LED_PURGE_COLOR);
buttonAction = BTN_STARTPURGE;
}
else if (buttonTimestamp + BUTTON_ACTION_DELAY_TOGGLEMODE < millis())
{
uint32_t color = globals.systemStatus == sysStat_Normal ? LED_RAIN_COLOR : LED_NORMAL_COLOR;
LED_Process(1, color);
buttonAction = BTN_TOGGLEMODE;
}
}
else // Button is released
{
// Execute corresponding actions based on the detected button action
if (buttonAction != BTN_INACTIVE)
{
switch (buttonAction)
{
case BTN_TOGGLEWIFI:
toggleWiFiAP();
Debug_pushMessage("Starting WiFi AP\n");
break;
case BTN_STARTPURGE:
globals.systemStatus = sysStat_Purge;
globals.purgePulses = LubeConfig.BleedingPulses;
Debug_pushMessage("Starting Purge\n");
break;
case BTN_TOGGLEMODE:
switch (globals.systemStatus)
{
case sysStat_Normal:
globals.systemStatus = sysStat_Rain;
globals.resumeStatus = sysStat_Rain;
break;
case sysStat_Rain:
globals.systemStatus = sysStat_Normal;
globals.resumeStatus = sysStat_Normal;
break;
default:
break;
}
Debug_pushMessage("Toggling Mode\n");
break;
case BTN_NOTHING:
default:
Debug_pushMessage("Nothing or invalid\n");
break;
}
// Display feedback through LEDs
LED_Process(2);
}
// Reset button state and timestamp
buttonAction = BTN_INACTIVE;
buttonTimestamp = 0;
}
}
/** /**
* @brief Toggles the WiFi functionality based on the current status. * @brief Toggles the WiFi functionality based on the current status.
* *
@@ -722,6 +411,8 @@ void Button_Process()
*/ */
void toggleWiFiAP(bool shutdown) void toggleWiFiAP(bool shutdown)
{ {
LEDControl_SetOverride(LED_WIFI_COLOR, LED_PATTERN_BLINK_FAST, 2500);
// Check if WiFi is currently active // Check if WiFi is currently active
if (WiFi.getMode() != WIFI_OFF) if (WiFi.getMode() != WIFI_OFF)
{ {
@@ -788,6 +479,36 @@ void SystemShutdown(bool restart)
} }
} }
void onToggleMode()
{
if (globals.systemStatus == sysStat_Normal)
{
globals.systemStatus = sysStat_Rain;
globals.resumeStatus = sysStat_Rain;
}
else if (globals.systemStatus == sysStat_Rain)
{
globals.systemStatus = sysStat_Normal;
globals.resumeStatus = sysStat_Normal;
}
Debug_pushMessage("Toggling Mode\n");
}
void onStartPurge()
{
globals.systemStatus = sysStat_Purge;
globals.purgePulses = LubeConfig.BleedingPulses;
Debug_pushMessage("Starting Purge\n");
}
void onWashMode()
{
Debug_pushMessage("Wash mode start\n");
// Hier könntest du später gezieltes Nachölen implementieren
}
/** /**
* @brief Processes the impulses from the wheel speed sensor and converts them into traveled distance. * @brief Processes the impulses from the wheel speed sensor and converts them into traveled distance.
* *

View File

@@ -3,10 +3,10 @@
* *
* @brief Implementation file for converting structs to JSON objects. * @brief Implementation file for converting structs to JSON objects.
* *
* @note This file is auto-generated by a script on 2024-01-30 20:29:34. * @note This file is auto-generated by a script on 2025-06-15 11:37:51.
* *
* @author Marcel Peterkau * @author Marcel Peterkau
* @date 30.01.2024 * @date 15.06.2025
*/ */
@@ -26,6 +26,8 @@ void generateJsonObject_LubeConfig(JsonObject data)
data["RimDiameter_Inch"] = LubeConfig.RimDiameter_Inch; data["RimDiameter_Inch"] = LubeConfig.RimDiameter_Inch;
data["DistancePerRevolution_mm"] = LubeConfig.DistancePerRevolution_mm; data["DistancePerRevolution_mm"] = LubeConfig.DistancePerRevolution_mm;
data["BleedingPulses"] = LubeConfig.BleedingPulses; data["BleedingPulses"] = LubeConfig.BleedingPulses;
data["WashMode_Distance"] = LubeConfig.WashMode_Distance;
data["WashMode_Interval"] = LubeConfig.WashMode_Interval;
data["SpeedSource"] = LubeConfig.SpeedSource; data["SpeedSource"] = LubeConfig.SpeedSource;
data["GPSBaudRate"] = LubeConfig.GPSBaudRate; data["GPSBaudRate"] = LubeConfig.GPSBaudRate;
data["CANSource"] = LubeConfig.CANSource; data["CANSource"] = LubeConfig.CANSource;
@@ -52,4 +54,4 @@ void generateJsonObject_PersistenceData(JsonObject data)
// CODEGENERATOR_CHECKSUM: 59f35aadffd0bbef253210ea2fbaaf9a515553a2e3cc9bf4cfa2819b63c969ce // CODEGENERATOR_CHECKSUM: 4702cb49ea55617cbb34715164810bb58d3c3f46fb1653b6f47bd4fd9cb0031e

View File

@@ -480,7 +480,6 @@ void Websocket_HandleButtons(uint8_t *data)
else if (strcmp(identifier, "purgenow") == 0) else if (strcmp(identifier, "purgenow") == 0)
{ {
globals.systemStatus = sysStat_Purge; globals.systemStatus = sysStat_Purge;
globals.purgePulses = LubeConfig.BleedingPulses;
} }
else if (strcmp(identifier, "sourcesave") == 0) else if (strcmp(identifier, "sourcesave") == 0)
{ {
@@ -499,6 +498,7 @@ void Websocket_HandleButtons(uint8_t *data)
else if (strcmp(identifier, "resettank") == 0) else if (strcmp(identifier, "resettank") == 0)
{ {
PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000; PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000;
globals.requestEEAction = EE_PDS_SAVE;
} }
else else
{ {
@@ -580,6 +580,14 @@ void Websocket_HandleSettings(uint8_t *data)
{ {
strncpy(LubeConfig.wifi_client_password, value, sizeof(LubeConfig.wifi_client_password)); strncpy(LubeConfig.wifi_client_password, value, sizeof(LubeConfig.wifi_client_password));
} }
else if (strcmp(identifier, "washinterval") == 0)
{
LubeConfig.WashMode_Interval = atoi(value);
}
else if (strcmp(identifier, "washdistance") == 0)
{
LubeConfig.WashMode_Distance = atoi(value);
}
else else
{ {
Debug_pushMessage("Got unknown Settings-id and value '%s' from ws-client\n", identifier); Debug_pushMessage("Got unknown Settings-id and value '%s' from ws-client\n", identifier);
@@ -700,6 +708,8 @@ void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping)
const char mapping[] = "MAPPING_STATIC:" const char mapping[] = "MAPPING_STATIC:"
"lubedistancenormal;" "lubedistancenormal;"
"lubedistancerain;" "lubedistancerain;"
"washdistance;"
"washinterval;"
"tankcap;" "tankcap;"
"pumppulse;" "pumppulse;"
"tankwarn;" "tankwarn;"
@@ -730,6 +740,8 @@ void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping)
temp.concat(String(LubeConfig.DistancePerLube_Default) + ";"); temp.concat(String(LubeConfig.DistancePerLube_Default) + ";");
temp.concat(String(LubeConfig.DistancePerLube_Rain) + ";"); temp.concat(String(LubeConfig.DistancePerLube_Rain) + ";");
temp.concat(String(LubeConfig.WashMode_Distance) + ";");
temp.concat(String(LubeConfig.WashMode_Interval) + ";");
temp.concat(String(LubeConfig.tankCapacity_ml) + ";"); temp.concat(String(LubeConfig.tankCapacity_ml) + ";");
temp.concat(String(LubeConfig.amountPerDose_microL) + ";"); temp.concat(String(LubeConfig.amountPerDose_microL) + ";");
temp.concat(String(LubeConfig.TankRemindAtPercentage) + ";"); temp.concat(String(LubeConfig.TankRemindAtPercentage) + ";");