Compare commits
17 Commits
12ee18adee
...
master
Author | SHA1 | Date | |
---|---|---|---|
17ce546255 | |||
1126111edb | |||
a9053997a1 | |||
27993d72ee | |||
04705ce666 | |||
98629b744d | |||
c8c67551fd | |||
2b588b3be2 | |||
3a6c102b45 | |||
c6d65f50bf | |||
3e69485696 | |||
ec9a75e472 | |||
1966705f7f | |||
9cb3a61184 | |||
f735ea7b0d | |||
05f476bae2 | |||
c998cce1a8 |
26
Jenkinsfile
vendored
26
Jenkinsfile
vendored
@@ -58,25 +58,13 @@ wifi_ap_password = DummyAP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('📦 Find & Archive Firmware') {
|
stage('📦 Archive Firmware & FS') {
|
||||||
steps {
|
steps {
|
||||||
dir('Software') {
|
dir('Software') {
|
||||||
script {
|
echo "🔍 Archiviere Artefakte (.fw.bin / .fs.gz)…"
|
||||||
echo "🔍 Suche nach Firmware (.fw.bin) und Filesystem (.fs.gz) Artefakten..."
|
archiveArtifacts artifacts: '.pio/build/**/*.fw.bin, .pio/build/**/*.fs.gz',
|
||||||
|
allowEmptyArchive: true,
|
||||||
def firmwareFiles = findFiles(glob: '.pio/build/**/*.fw.bin')
|
fingerprint: true
|
||||||
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(', ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
Reverse-Engineering CAN-Bus/.gitignore
vendored
Normal file
45
Reverse-Engineering CAN-Bus/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.pkl
|
||||||
|
*.pklz
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Build
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# Logs (Ordner behalten, Dateien ignorieren)
|
||||||
|
logs/*
|
||||||
|
!logs/.gitkeep
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Virtual Environments
|
||||||
|
venv/
|
||||||
|
.env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# System-Dateien
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Backup-Dateien
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Projekt-spezifische
|
||||||
|
settings.json
|
||||||
|
settings.json.bak
|
||||||
|
tmp/
|
90
Reverse-Engineering CAN-Bus/HOW-TO-REVERSE.md
Normal file
90
Reverse-Engineering CAN-Bus/HOW-TO-REVERSE.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# HOW-TO_REVERSE – Praxisleitfaden fürs CAN-Reverse-Engineering
|
||||||
|
|
||||||
|
Dieses How-To ist dein Werkzeugkasten, um aus nackten Frames echte Signale zu destillieren. Es ist kein Orakel, sondern ein **Experimentier-Protokoll**: miss, verifiziere, falsifiziere. Nutze es zusammen mit der GUI/CLI (Splitter, Explorer, Batch-Analyzer, Range-/Unsupervised-Fit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0) Vorbereitungen (Daten sammeln wie ein Ingenieur)
|
||||||
|
- **Zustände trennen**: *Zündung an*, *Motor aus*, *Motor an Leerlauf*, *Schieben*, *aufgebockt Hinterrad drehen*, *kurze Fahrt*.
|
||||||
|
- **Aktoren toggeln**: Blinker, Bremse, Licht, Lüfter → generiere Ground-Truth für Bits/Flags.
|
||||||
|
- **Nur RX** analysieren, wenn deine Hardware parallel TX sendet (Störmuster vermeiden).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Frame- & ID-Ebene zuerst
|
||||||
|
- **Repetition Rate (Hz)**: Zyklische Sensor-IDs senden stabil.
|
||||||
|
- *Daumenwerte*: WheelSpeed 20–100 Hz, RPM 10–50 Hz, TPS/APS 20–100 Hz, Lenkwinkel 50–100 Hz, Temperaturen 1–10 Hz.
|
||||||
|
- **Jitter** der Periodizität: Streuung der Inter-Arrival-Times. Niedrig = sauberer Zyklus.
|
||||||
|
- **DLC-Stabilität**: schwankende Payload-Länge → ggf. ISO-TP / Multiplex.
|
||||||
|
- **Change-Density**: Anteil Frames mit Payload-Änderung. Zu hoch → Counter/Checksumme, zu niedrig → Status/Träge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Byte/Bit-Signaturen
|
||||||
|
- **Bit-Flip-Rate** pro Bit: ~50% → Flag/Event; sehr regelmäßig → Pattern/Timer.
|
||||||
|
- **Rolling Counter**: 4/8-bit Sequenzen (0..15/255), oft konstant steigend.
|
||||||
|
- **Checksumme**: Byte hängt deterministisch von anderen Bytes ab; häufig letzte Position.
|
||||||
|
- **Endianness**: 16-bit LE/BE testen. Monotone Trends/kleine Deltas weisen auf richtige Byteordnung.
|
||||||
|
- **Quantisierung**: typische Schrittweiten (z. B. 0.5 °C, 0.25 km/h).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Physik als Filter (Slew-Rate & Grenzen)
|
||||||
|
Miss **ΔWert/Δt** robust (95-Perzentil, nicht Max):
|
||||||
|
- **Temperaturen**: sehr träge → ΔT/s ≪ 1 °C/s.
|
||||||
|
- **Fahrgeschwindigkeit**: 0→100 km/h < 1 s unrealistisch; grob ≤ 30–50 km/h/s (Straße).
|
||||||
|
- **RPM**: schnelle Sprünge möglich, aber nicht teleport. 1k→8k in 1–3 s plausibel.
|
||||||
|
- **Lenkwinkel**: schnell, aber begrenzt; **Jerk** (ΔΔ/Δt) nicht absurd.
|
||||||
|
|
||||||
|
Alles, was diese Checks bricht, ist selten dein gesuchtes physikalisches Signal (oder deine Skalierung ist falsch).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Korrelation & Kausalität
|
||||||
|
- **Cross-Korrelation**: RPM ↔ WheelSpeed (Gang drin), Brake-Bit ↔ Pressure, Blinker-Bit ↔ Blinkfrequenz (~1–2 Hz).
|
||||||
|
- **Gang** aus Ratio (RPM/Speed) ableiten und Kandidaten validieren.
|
||||||
|
- **Event-Marker** setzen und zeitgleichen Byte-Kipp suchen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Protokoll & Multiplex
|
||||||
|
- **ISO-TP**: Muster `0x10 len …` (First Frame), `0x21…` (Consecutive). Enthält selten einzelne Sensorkanäle.
|
||||||
|
- **Multiplexer**: Ein Byte schaltet die „Seite“ der Payload um. Erkennbar am Sprungverhalten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Statistik-Fingerabdrücke
|
||||||
|
- **Unique-Ratio** = |unique|/n. Zu klein → Flag/Konstante; moderat → analog.
|
||||||
|
- **Entropy** pro Byte → Daten/Checksumme vs. Status.
|
||||||
|
- **Plateaus/Hysterese**: aktualisiert nur bei Δ≥Schwelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Scale/Offset systematisch schätzen
|
||||||
|
- **Scale-Raster**: Dekaden + praxisnahe Werte (0.0625, 0.1, 0.25, 0.5, 0.75, 1, 2, 5, 10 …).
|
||||||
|
- **Offset** via **Intervall-Überdeckung**: wähle das Offset, das die meisten Samples in [rmin, rmax] bringt.
|
||||||
|
- **Vorzeichen prüfen**: signed/unsigned, ggf. negative Scales zulassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Workflow-Cookbook
|
||||||
|
1. **Splitten** (Logs → Traces).
|
||||||
|
2. **ID-Explorer/Batch**: Periodizität, Change-Density, 8/16-bit Plots.
|
||||||
|
3. **Range-Fit** mit physikalischen Ranges *und* **Slew-Limits** (Δ/Δt) + **Rate/Jitter-Constraints**.
|
||||||
|
4. **Cross-Checks**: Kandidaten gegen andere Kanäle testen (RPM↔Speed, Brake↔Pressure).
|
||||||
|
5. **Iterieren**: Range/Constraints verfeinern, Plots sichten, Hypothesen anpassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) Typische Fallen
|
||||||
|
- Falsche Endianness → Teleports.
|
||||||
|
- Counter/Checksumme im selben 16-bit-Wort → zuerst trennen.
|
||||||
|
- DLC<8 → 16-bit-Kombis fehlen; keine Dummies einstreuen.
|
||||||
|
- ×10/×100-Skalierung: Δ/Δt wirkt absurd groß.
|
||||||
|
- BCD/ASCII in Diag/Odometer – nicht physikalisch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10) Ziel: Berichte statt Bauchgefühl
|
||||||
|
Automatisiere Tests & Schwellen. Lass Skripts einen **Analysebericht** schreiben: „PASS (smooth, low jitter, rate ok)“ vs. „FAIL (jitter, slope99 zu hoch, hit-ratio zu klein)“.
|
||||||
|
Das minimiert Confirmation Bias – und macht Ergebnisse reproduzierbar.
|
272
Reverse-Engineering CAN-Bus/README.md
Normal file
272
Reverse-Engineering CAN-Bus/README.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Kettenöler – CAN Reverse-Engineering Toolkit
|
||||||
|
|
||||||
|
Toolsuite (GUI + CLI) zum Analysieren von CAN-Logs im **Kettenöler-Format**.
|
||||||
|
Funktionen: Logs **splitten** (pro CAN-ID), **explorative Visualisierung** (8-/16-Bit, LE/BE), **Batch-Analysen** über viele `.trace`, **Ranking** plausibler Signale und **Range-Fit** (lineare Abbildung `phys = raw*scale + offset`), optional **unsupervised** ohne vorgegebene Range.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features (Überblick)
|
||||||
|
|
||||||
|
* **Einheitliche GUI** (`main.py`) mit globalem Header (Workdir, Ordnerstruktur, Log-Auswahl).
|
||||||
|
* **Gemeinsame Trace-Auswahl** in allen Trace-Tabs (gleiches Panel, synchronisiert über Tabs):
|
||||||
|
|
||||||
|
* **ID Explorer** (Multi-Select)
|
||||||
|
* **Traces Batch-Analyse** (Multi-Select oder kompletter Ordner)
|
||||||
|
* **Range-Fit** (Single-Select, supervised *oder* unsupervised)
|
||||||
|
* **Splitter**: Logs → `.trace` pro CAN-ID (`traces/…`, inkl. `overview_ids.csv`).
|
||||||
|
* **Einzel-ID-Explorer**: Plots aller Byte-Kanäle (8-Bit) und Nachbar-Wortkombis (16-Bit LE/BE) + Kurzstatistik.
|
||||||
|
* **Batch-Analyzer**: Kennzahlen/Plots für alle `.trace` in einem Ordner, globales Ranking.
|
||||||
|
* **Range-/Unsupervised-Fit**:
|
||||||
|
|
||||||
|
* *Supervised*: findet `scale` & `offset` für Zielbereich `[rmin, rmax]` (Offset via Intervall-Überdeckung, Scale aus plausibler Menge).
|
||||||
|
* *Unsupervised*: identifiziert „ruhige“ physikalische Kandidaten ohne Range (Smoothness/Varianz/Rate/Spannweite).
|
||||||
|
* **Output-Hygiene**: Ergebnisse stets unter `analyze_out/<timestamp>_<tool>/…`, optionale Zeitstempel-Unterordner verhindern Überschreiben.
|
||||||
|
* **Projektdatei** (`Projekt.json`): speichert Workdir, Subfolder, Log-Auswahl, aktiven Traces-Ordner, etc.
|
||||||
|
* **„Neuester Split“**-Button: springt in den jüngsten Unterordner von `traces/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository-Komponenten
|
||||||
|
|
||||||
|
* **GUI**
|
||||||
|
|
||||||
|
* `main.py` – zentrales Frontend mit Tabs (Multi-Log Analyse, ID Explorer, Traces Batch-Analyse, Range-Fit).
|
||||||
|
* **CLI-Tools**
|
||||||
|
|
||||||
|
* `can_split_by_id.py` – Splittet Logs nach CAN-ID → `.trace`.
|
||||||
|
* `id_signal_explorer.py` – Visualisiert/analysiert eine `.trace` (8-Bit, 16-Bit LE/BE) + `summary_stats.csv`.
|
||||||
|
* `trace_batch_analyzer.py` – Batch-Analyse für viele `.trace` + globales Ranking.
|
||||||
|
* `trace_signal_fitter.py` – **Range-Fit** (scale/offset) **oder** **Unsupervised-Fit** (ohne Range).
|
||||||
|
|
||||||
|
> Optional/Alt: `can_universal_signal_finder.py` – ursprünglicher Multi-Log-Analyzer (Ranking auf Rohdatenebene).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
* **Python** ≥ 3.10
|
||||||
|
* Abhängigkeiten: `pandas`, `numpy`, `matplotlib`
|
||||||
|
* Setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logformat (Kettenöler)
|
||||||
|
|
||||||
|
Eine Zeile pro Frame:
|
||||||
|
|
||||||
|
```
|
||||||
|
<timestamp_ms> <TX|RX> 0x<ID_HEX> <DLC> <byte0> <byte1> ... <byte7>
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```
|
||||||
|
123456 RX 0x208 8 11 22 33 44 55 66 77 88
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projekt-/Ordnerstruktur
|
||||||
|
|
||||||
|
Ein **Workdir** bündelt alles zu einem Fahrzeug/Projekt:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Workdir>/
|
||||||
|
Projekt.json # GUI-Einstellungen
|
||||||
|
logs/ # Input-Logs
|
||||||
|
traces/ # per-ID .trace (vom Split)
|
||||||
|
analyze_out/ # Ergebnisse; je Run eigener Timestamp-Unterordner
|
||||||
|
```
|
||||||
|
|
||||||
|
**Namenskonventionen**
|
||||||
|
|
||||||
|
* Split-Ergebnisse: `traces/<timestamp?>/0x<ID>_<ursprungslog>.trace`
|
||||||
|
* Outputs: `analyze_out/<YYYYMMDD_HHMMSS>_<tool>/…`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modelle-Ordner & Git
|
||||||
|
|
||||||
|
Wenn du pro Modell arbeitest, z. B.:
|
||||||
|
|
||||||
|
```
|
||||||
|
models/
|
||||||
|
Triumph 2023/
|
||||||
|
logs/
|
||||||
|
traces/
|
||||||
|
analyze_out/
|
||||||
|
Projekt.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Lege in `models/` folgende **`.gitignore`** ab, damit `traces/` und `analyze_out/` **in jedem Modell-Unterordner** ignoriert werden – `logs/` und `.json` bleiben versioniert:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
*/traces/
|
||||||
|
*/traces/**
|
||||||
|
*/analyze_out/
|
||||||
|
*/analyze_out/**
|
||||||
|
|
||||||
|
traces/
|
||||||
|
traces/**
|
||||||
|
analyze_out/
|
||||||
|
analyze_out/**
|
||||||
|
|
||||||
|
# optional: typos
|
||||||
|
*/analyze.out/
|
||||||
|
*/analyze.out/**
|
||||||
|
analyze.out/
|
||||||
|
analyze.out/**
|
||||||
|
```
|
||||||
|
|
||||||
|
Leere Ordner wie `logs/` ggf. mit `.gitkeep` befüllen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GUI-Benutzung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Globaler Header (immer oben)
|
||||||
|
|
||||||
|
* **Workdir** wählen, **Logs scannen** → Liste aller gefundenen Logfiles (Multi-Select).
|
||||||
|
* Subfolder einstellen: `logs`, `traces`, `analyze_out` (alle **parallel** im Workdir).
|
||||||
|
* **Projekt speichern/laden** (`Projekt.json`).
|
||||||
|
* Beim Workdir-Wechsel/Projekt-Laden setzt die GUI den **aktiven Traces-Ordner** automatisch auf `traces/` bzw. den **jüngsten** Unterordner.
|
||||||
|
|
||||||
|
### Einheitliches Trace-Panel (in allen Trace-Tabs)
|
||||||
|
|
||||||
|
* Links: Liste der `.trace`
|
||||||
|
* Rechts: **Traces-Ordner wählen**, **Workdir/traces**, **Neuester Split**, **Refresh**, (optional **Alle**, **Keine**)
|
||||||
|
* Änderungen am Ordner/Liste wirken **sofort in allen Tabs**.
|
||||||
|
|
||||||
|
### Tab: Multi-Log Analyse
|
||||||
|
|
||||||
|
* Ranking direkt aus Logs (Include/Exclude-IDs, optional Range mit `scale/offset`).
|
||||||
|
* Output: `analyze_out/<ts>_multilog/…`
|
||||||
|
* Optional: „Jede Logdatei separat“ → je Log eigener Unterordner.
|
||||||
|
|
||||||
|
### Tab: ID Explorer
|
||||||
|
|
||||||
|
* **Split** (aus Header-Logauswahl): Logs → `.trace` nach `traces[/<ts>]`, plus `overview_ids.csv`.
|
||||||
|
Danach wird der neue Traces-Pfad **automatisch aktiviert**.
|
||||||
|
* **Einzel-ID Analyse** (Multi-Select):
|
||||||
|
|
||||||
|
* Plots: Byte\[0..7] (8-Bit) + LE/BE für Paare (0-1 … 6-7)
|
||||||
|
* `summary_stats.csv` pro Trace
|
||||||
|
* Output: `analyze_out/<ts>_id_explore/…`
|
||||||
|
|
||||||
|
### Tab: Traces Batch-Analyse
|
||||||
|
|
||||||
|
* Nutzt die gemeinsame Trace-Liste.
|
||||||
|
* **Ohne Auswahl** → kompletter Ordner; **mit Auswahl** → es wird ein Subset-Ordner gebaut (Hardlinks/Kopie) und nur dieses analysiert.
|
||||||
|
* Parameter: `--rx-only`, `scale`, `offset`, `range-min/max`, `top`, `--plots`.
|
||||||
|
* Output:
|
||||||
|
|
||||||
|
* je Trace: `*_combostats.csv` (+ Plots),
|
||||||
|
* global: `summary_top_combinations.csv`
|
||||||
|
* unter `analyze_out/<ts>_trace_batch/…`
|
||||||
|
|
||||||
|
### Tab: Range-Fit (Single-Select)
|
||||||
|
|
||||||
|
* **Zwei Modi**:
|
||||||
|
|
||||||
|
1. **Supervised** (Range-Min/Max gesetzt): findet `scale` & `offset`, maximiert **Hit-Ratio** im Zielbereich.
|
||||||
|
Output: `<trace>_encoding_candidates.csv` + phys-Plots (Top-N).
|
||||||
|
2. **Unsupervised** (Range leer): bewertet Kandidaten nach **Smoothness**, **Spannweite**, **Varianz**, **Rate**, **Uniqueness**.
|
||||||
|
Output: `<trace>_unsupervised_candidates.csv` + Roh-Plots (Top-N).
|
||||||
|
* Optionen: `nur RX`, `negative Scale erlauben` (nur supervised), `Min. Hit-Ratio`, `Min. Smoothness`, `Plots Top-N`, `Output-Label`.
|
||||||
|
* Output: `analyze_out/<ts>_rangefit/…`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI-Quickstart
|
||||||
|
|
||||||
|
### 1) Splitten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 can_split_by_id.py logs/run1.log logs/run2.log \
|
||||||
|
--outdir <Workdir>/traces/20250827_1200 \
|
||||||
|
--rx-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Einzel-ID-Explorer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 id_signal_explorer.py <Workdir>/traces/20250827_1200/0x208_run1.trace \
|
||||||
|
--outdir <Workdir>/analyze_out/20250827_1210_id_explore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) Batch-Analyse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 trace_batch_analyzer.py \
|
||||||
|
--traces-dir <Workdir>/traces/20250827_1200 \
|
||||||
|
--outdir <Workdir>/analyze_out/20250827_1220_trace_batch \
|
||||||
|
--rx-only --plots --top 8 \
|
||||||
|
--range-min 31 --range-max 80
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4) Range-/Unsupervised-Fit (eine `.trace`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supervised (z. B. Kühlmittel 31..80°C)
|
||||||
|
python3 trace_signal_fitter.py <trace> \
|
||||||
|
--rmin 31 --rmax 80 \
|
||||||
|
--outdir <Workdir>/analyze_out/20250827_1230_rangefit \
|
||||||
|
--plots-top 8 --min-hit 0.5 --allow-neg-scale
|
||||||
|
|
||||||
|
# Unsupervised (ohne Range)
|
||||||
|
python3 trace_signal_fitter.py <trace> \
|
||||||
|
--outdir <Workdir>/analyze_out/20250827_1240_unsupervised \
|
||||||
|
--plots-top 8 --min-smooth 0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Algorithmen & Heuristiken
|
||||||
|
|
||||||
|
* **Kombinationen**:
|
||||||
|
|
||||||
|
* 8-Bit: `D0..D7`
|
||||||
|
* 16-Bit (adjazent): LE & BE für Paare `(0,1)…(6,7)`
|
||||||
|
*(32-Bit & bit-gepackte Felder: auf der Roadmap)*
|
||||||
|
|
||||||
|
* **Prefilter** (für „ruhige“ physikalische Größen):
|
||||||
|
Mindestanzahl Samples, nicht (nahezu) konstant, keine exzessiven Sprünge (p95 der |Δ| relativ zur Spannweite).
|
||||||
|
|
||||||
|
* **Range-Fit**:
|
||||||
|
Für jeden Kandidaten `raw` wird über eine Menge plausibler **Scales** gesucht; für jedes `scale` wird das **Offset** via **Intervall-Überdeckung** bestimmt (`rmin ≤ scale*raw_i + offset ≤ rmax`). Ranking: Hit-Ratio ↓, dann Glattheit (p95 phys) ↑, Rate ↓, n ↓.
|
||||||
|
|
||||||
|
* **Unsupervised**:
|
||||||
|
**Smoothness** = `1 − clamp(p95(|Δ|)/span, 0..1)`; zusätzlich **span**, **var**, **rate**, **uniq\_ratio**. Ranking auf diese Metriken.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tipps & Troubleshooting
|
||||||
|
|
||||||
|
* **Keine Kandidaten (Range-Fit)**: `--min-hit` senken, `--allow-neg-scale` testen, Range prüfen, längeres/variableres Log nutzen.
|
||||||
|
* **Alles wird gefiltert (Unsupervised)**: `--min-smooth` senken; ggf. `--rx-only` aktivieren.
|
||||||
|
* **Leere/komische Plots**: DLC < 8 → teils keine 16-Bit-Kombis; Frames sehr selten → Rate niedrig.
|
||||||
|
* **Ordner stets sauber**: Zeitstempel-Unterordner aktiv lassen; pro Run eigene Artefakte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
* 32-Bit-Kombinationen, bit-gepackte Felder.
|
||||||
|
* Histogramme, Autokorrelation, Ausreißer-Detektoren.
|
||||||
|
* vordefinierte Signal-Profile (z. B. *WheelSpeed*, *CoolantTemp*).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lizenz / Haftung
|
||||||
|
|
||||||
|
Nur zu Analyse-/Reverse-Engineering-Zwecken. Nutzung auf eigene Verantwortung.
|
95
Reverse-Engineering CAN-Bus/can_split_by_id.py
Normal file
95
Reverse-Engineering CAN-Bus/can_split_by_id.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
LOG_PATTERN = re.compile(r"(\d+)\s+(TX|RX)\s+0x([0-9A-Fa-f]+)\s+(\d+)\s+((?:[0-9A-Fa-f]{2}\s+)+)")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Split Kettenöler CAN log(s) into per-ID .trace files and build an overview")
|
||||||
|
ap.add_argument("logs", nargs="+", help="Input log file(s)")
|
||||||
|
ap.add_argument("--outdir", default="traces", help="Output directory for per-ID trace files")
|
||||||
|
ap.add_argument("--rx-only", action="store_true", help="Keep only RX frames in traces and stats")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
outdir = Path(args.outdir)
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
writers = {}
|
||||||
|
stats = defaultdict(lambda: {
|
||||||
|
"id_hex": None, "rx":0, "tx":0, "count":0, "first_ts":None, "last_ts":None,
|
||||||
|
"first_file":None, "dlc_set": set()
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_writer(can_id_hex: str, src_name: str):
|
||||||
|
# filename pattern: 0xID_<srcfile>.trace
|
||||||
|
safe_src = Path(src_name).name
|
||||||
|
fn = outdir / f"{can_id_hex}_{safe_src}.trace"
|
||||||
|
if fn not in writers:
|
||||||
|
writers[fn] = fn.open("a", encoding="utf-8")
|
||||||
|
return writers[fn]
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
written = 0
|
||||||
|
for p in args.logs:
|
||||||
|
with open(p, "r", errors="ignore") as f:
|
||||||
|
for line in f:
|
||||||
|
m = LOG_PATTERN.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
ts = int(m.group(1))
|
||||||
|
dr = m.group(2)
|
||||||
|
cid_hex = m.group(3).upper()
|
||||||
|
dlc = int(m.group(4))
|
||||||
|
data = m.group(5)
|
||||||
|
|
||||||
|
total += 1
|
||||||
|
if args.rx_only and dr != "RX":
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = int(cid_hex, 16)
|
||||||
|
s = stats[key]
|
||||||
|
s["id_hex"] = f"0x{cid_hex}"
|
||||||
|
s["count"] += 1
|
||||||
|
s["rx"] += 1 if dr == "RX" else 0
|
||||||
|
s["tx"] += 1 if dr == "TX" else 0
|
||||||
|
s["first_ts"] = ts if s["first_ts"] is None else min(s["first_ts"], ts)
|
||||||
|
s["last_ts"] = ts if s["last_ts"] is None else max(s["last_ts"], ts)
|
||||||
|
s["first_file"] = s["first_file"] or Path(p).name
|
||||||
|
s["dlc_set"].add(dlc)
|
||||||
|
|
||||||
|
w = get_writer(f"0x{cid_hex}", Path(p).name)
|
||||||
|
w.write(line)
|
||||||
|
written += 1
|
||||||
|
|
||||||
|
for fh in writers.values():
|
||||||
|
fh.close()
|
||||||
|
|
||||||
|
# build overview CSV
|
||||||
|
import pandas as pd
|
||||||
|
rows = []
|
||||||
|
for cid, s in stats.items():
|
||||||
|
dur_ms = 0 if s["first_ts"] is None else (s["last_ts"] - s["first_ts"])
|
||||||
|
rate_hz = (s["rx"] if args.rx_only else s["count"]) / (dur_ms/1000.0) if dur_ms > 0 else 0.0
|
||||||
|
rows.append({
|
||||||
|
"id_dec": cid,
|
||||||
|
"id_hex": s["id_hex"],
|
||||||
|
"count": s["count"],
|
||||||
|
"rx": s["rx"],
|
||||||
|
"tx": s["tx"],
|
||||||
|
"duration_s": round(dur_ms/1000.0, 6),
|
||||||
|
"rate_hz_est": round(rate_hz, 6),
|
||||||
|
"first_file": s["first_file"],
|
||||||
|
"dlc_variants": ",".join(sorted(str(x) for x in s["dlc_set"])),
|
||||||
|
})
|
||||||
|
df = pd.DataFrame(rows).sort_values(["rate_hz_est","count"], ascending=[False, False])
|
||||||
|
csv_path = outdir / "overview_ids.csv"
|
||||||
|
df.to_csv(csv_path, index=False)
|
||||||
|
|
||||||
|
print(f"Done. Parsed {total} lines, wrote {written} lines into per-ID traces at {outdir}.")
|
||||||
|
print(f"Overview: {csv_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
272
Reverse-Engineering CAN-Bus/can_universal_signal_finder.py
Normal file
272
Reverse-Engineering CAN-Bus/can_universal_signal_finder.py
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Optional, Dict
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
LOG_PATTERN = re.compile(r"(\d+)\s+(TX|RX)\s+0x([0-9A-Fa-f]+)\s+\d+\s+((?:[0-9A-Fa-f]{2}\s+)+)")
|
||||||
|
|
||||||
|
def parse_log(path: Path) -> pd.DataFrame:
|
||||||
|
rows = []
|
||||||
|
with open(path, "r", errors="ignore") as f:
|
||||||
|
for line in f:
|
||||||
|
m = LOG_PATTERN.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
ts = int(m.group(1))
|
||||||
|
direction = m.group(2)
|
||||||
|
can_id = int(m.group(3), 16)
|
||||||
|
data = [int(x, 16) for x in m.group(4).split() if x.strip()]
|
||||||
|
rows.append((path.name, ts, direction, can_id, data))
|
||||||
|
df = pd.DataFrame(rows, columns=["file","ts","dir","id","data"])
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
# time base per file → seconds from file start
|
||||||
|
df["time_s"] = df.groupby("file")["ts"].transform(lambda s: (s - s.min())/1000.0)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def le16(data: List[int], offset: int) -> Optional[int]:
|
||||||
|
if len(data) < offset+2:
|
||||||
|
return None
|
||||||
|
return data[offset] | (data[offset+1] << 8)
|
||||||
|
|
||||||
|
def be16(data: List[int], offset: int) -> Optional[int]:
|
||||||
|
if len(data) < offset+2:
|
||||||
|
return None
|
||||||
|
return (data[offset] << 8) | data[offset+1]
|
||||||
|
|
||||||
|
def phys(val: float, scale: float, offs: float) -> float:
|
||||||
|
return val*scale + offs
|
||||||
|
|
||||||
|
def decode_series(arr_data: List[List[int]], endian: str, offset: int) -> List[Optional[int]]:
|
||||||
|
out = []
|
||||||
|
for d in arr_data:
|
||||||
|
v = le16(d, offset) if endian == "le" else be16(d, offset)
|
||||||
|
out.append(v)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def score_values(vals: np.ndarray) -> Dict[str, float]:
|
||||||
|
if len(vals) < 3:
|
||||||
|
return {"variance":0.0, "changes":0, "unique_ratio":0.0}
|
||||||
|
var = float(np.var(vals))
|
||||||
|
changes = int(np.count_nonzero(np.diff(vals)))
|
||||||
|
unique_ratio = len(set(vals.tolist()))/len(vals)
|
||||||
|
return {"variance":var, "changes":changes, "unique_ratio":unique_ratio}
|
||||||
|
|
||||||
|
def analyze(df: pd.DataFrame, include_ids: Optional[List[int]], exclude_ids: Optional[List[int]]):
|
||||||
|
# Group by ID and try each 16-bit word
|
||||||
|
combos = []
|
||||||
|
ids = sorted(df["id"].unique().tolist())
|
||||||
|
if include_ids:
|
||||||
|
ids = [i for i in ids if i in include_ids]
|
||||||
|
if exclude_ids:
|
||||||
|
ids = [i for i in ids if i not in exclude_ids]
|
||||||
|
|
||||||
|
for cid in ids:
|
||||||
|
grp = df[df["id"]==cid]
|
||||||
|
for endian in ("le","be"):
|
||||||
|
for off in (0,2,4,6):
|
||||||
|
dec = decode_series(grp["data"].tolist(), endian, off)
|
||||||
|
# filter Nones
|
||||||
|
pairs = [(t, v) for t, v in zip(grp["time_s"].tolist(), dec) if v is not None]
|
||||||
|
if len(pairs) < 4:
|
||||||
|
continue
|
||||||
|
times = np.array([p[0] for p in pairs], dtype=float)
|
||||||
|
vals = np.array([p[1] for p in pairs], dtype=float)
|
||||||
|
sc = score_values(vals)
|
||||||
|
combos.append({
|
||||||
|
"id": cid,
|
||||||
|
"endian": endian,
|
||||||
|
"offset": off,
|
||||||
|
"n": len(vals),
|
||||||
|
"variance": sc["variance"],
|
||||||
|
"changes": sc["changes"],
|
||||||
|
"unique_ratio": sc["unique_ratio"],
|
||||||
|
"rate_hz": float(len(vals)) / (times.max()-times.min()+1e-9)
|
||||||
|
})
|
||||||
|
cand_df = pd.DataFrame(combos)
|
||||||
|
return cand_df
|
||||||
|
|
||||||
|
def range_filter_stats(vals: np.ndarray, scale: float, offs: float, rmin: Optional[float], rmax: Optional[float]) -> Dict[str, float]:
|
||||||
|
if vals.size == 0:
|
||||||
|
return {"hit_ratio":0.0, "min_phys":np.nan, "max_phys":np.nan}
|
||||||
|
phys_vals = vals*scale + offs
|
||||||
|
if rmin is None and rmax is None:
|
||||||
|
return {"hit_ratio":1.0, "min_phys":float(np.min(phys_vals)), "max_phys":float(np.max(phys_vals))}
|
||||||
|
mask = np.ones_like(phys_vals, dtype=bool)
|
||||||
|
if rmin is not None:
|
||||||
|
mask &= (phys_vals >= rmin)
|
||||||
|
if rmax is not None:
|
||||||
|
mask &= (phys_vals <= rmax)
|
||||||
|
hit_ratio = float(np.count_nonzero(mask))/len(phys_vals)
|
||||||
|
return {"hit_ratio":hit_ratio, "min_phys":float(np.min(phys_vals)), "max_phys":float(np.max(phys_vals))}
|
||||||
|
|
||||||
|
def export_candidate_timeseries(df: pd.DataFrame, cid: int, endian: str, off: int, scale: float, offs: float, outdir: Path, basename_hint: str):
|
||||||
|
sub = df[df["id"]==cid].copy()
|
||||||
|
if sub.empty:
|
||||||
|
return False, None
|
||||||
|
dec = decode_series(sub["data"].tolist(), endian, off)
|
||||||
|
sub["raw16"] = dec
|
||||||
|
sub = sub.dropna(subset=["raw16"]).copy()
|
||||||
|
if sub.empty:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
sub["phys"] = sub["raw16"].astype(float)*scale + offs
|
||||||
|
# Save CSV
|
||||||
|
csv_path = outdir / f"{basename_hint}_0x{cid:X}_{endian}_off{off}.csv"
|
||||||
|
sub[["file","time_s","id","raw16","phys"]].to_csv(csv_path, index=False)
|
||||||
|
|
||||||
|
# Plot (single-plot image)
|
||||||
|
plt.figure(figsize=(10,5))
|
||||||
|
plt.plot(sub["time_s"].to_numpy(), sub["phys"].to_numpy(), marker="o")
|
||||||
|
plt.xlabel("Zeit (s)")
|
||||||
|
plt.ylabel("Wert (phys)")
|
||||||
|
plt.title(f"{basename_hint} 0x{cid:X} ({endian} @ +{off})")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
img_path = outdir / f"{basename_hint}_0x{cid:X}_{endian}_off{off}.png"
|
||||||
|
plt.savefig(img_path, dpi=150)
|
||||||
|
plt.close()
|
||||||
|
return True, (csv_path, img_path)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Universal CAN signal finder (WheelSpeed etc.) for Kettenöler logs")
|
||||||
|
ap.add_argument("logs", nargs="+", help="Log-Dateien (gleiche Struktur wie Kettenöler)")
|
||||||
|
ap.add_argument("--outdir", default="analyze_out", help="Ausgabeverzeichnis")
|
||||||
|
ap.add_argument("--top", type=int, default=20, help="Top-N Kandidaten global (nach Variance) exportieren, falls Range-Filter nichts findet")
|
||||||
|
ap.add_argument("--include-ids", default="", help="Nur diese IDs (kommagetrennt, z.B. 0x208,0x209)")
|
||||||
|
ap.add_argument("--exclude-ids", default="", help="Diese IDs ausschließen (kommagetrennt)")
|
||||||
|
ap.add_argument("--scale", type=float, default=1.0, help="Skalierung: phys = raw*scale + offset")
|
||||||
|
ap.add_argument("--offset", type=float, default=0.0, help="Offset: phys = raw*scale + offset")
|
||||||
|
ap.add_argument("--range-min", type=float, default=None, help="Min physischer Zielbereich (nach Scale/Offset)")
|
||||||
|
ap.add_argument("--range-max", type=float, default=None, help="Max physischer Zielbereich (nach Scale/Offset)")
|
||||||
|
ap.add_argument("--range-hit-ratio", type=float, default=0.6, help="Mindestanteil der Werte im Zielbereich [0..1]")
|
||||||
|
ap.add_argument("--per-id-limit", type=int, default=2, help="Max Anzahl Dekodierungen pro ID (z.B. beste zwei Offsets/Endianness)")
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
# Parse include/exclude lists
|
||||||
|
def parse_ids(s: str):
|
||||||
|
if not s.strip():
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for tok in s.split(","):
|
||||||
|
tok = tok.strip()
|
||||||
|
if not tok:
|
||||||
|
continue
|
||||||
|
if tok.lower().startswith("0x"):
|
||||||
|
out.append(int(tok,16))
|
||||||
|
else:
|
||||||
|
out.append(int(tok))
|
||||||
|
return out
|
||||||
|
|
||||||
|
include_ids = parse_ids(args.include_ids)
|
||||||
|
exclude_ids = parse_ids(args.exclude_ids)
|
||||||
|
|
||||||
|
# Load logs
|
||||||
|
frames = []
|
||||||
|
for p in args.logs:
|
||||||
|
df = parse_log(Path(p))
|
||||||
|
if df.empty:
|
||||||
|
print(f"Warn: {p} ergab keine Daten oder passte nicht zum Muster.", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
frames.append(df)
|
||||||
|
if not frames:
|
||||||
|
print("Keine Daten.", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
df_all = pd.concat(frames, ignore_index=True)
|
||||||
|
outdir = Path(args.outdir)
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Analyze all combos
|
||||||
|
cand = analyze(df_all, include_ids, exclude_ids)
|
||||||
|
if cand.empty:
|
||||||
|
print("Keine dekodierbaren 16-bit Felder gefunden.", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
# Range filter pass
|
||||||
|
cand = cand.sort_values(["variance","changes","unique_ratio"], ascending=[False, False, False]).reset_index(drop=True)
|
||||||
|
|
||||||
|
# For each candidate row, compute range-hit stats
|
||||||
|
hits = []
|
||||||
|
for _, row in cand.iterrows():
|
||||||
|
cid = int(row["id"])
|
||||||
|
endian = row["endian"]
|
||||||
|
off = int(row["offset"])
|
||||||
|
|
||||||
|
sub = df_all[df_all["id"]==cid]
|
||||||
|
dec = decode_series(sub["data"].tolist(), endian, off)
|
||||||
|
vals = np.array([v for v in dec if v is not None], dtype=float)
|
||||||
|
if vals.size == 0:
|
||||||
|
continue
|
||||||
|
rng = range_filter_stats(vals, args.scale, args.offset, args.range_min, args.range_max)
|
||||||
|
hits.append((rng["hit_ratio"], rng["min_phys"], rng["max_phys"]))
|
||||||
|
if hits:
|
||||||
|
cand[["hit_ratio","min_phys","max_phys"]] = pd.DataFrame(hits, index=cand.index)
|
||||||
|
else:
|
||||||
|
cand["hit_ratio"] = 0.0
|
||||||
|
cand["min_phys"] = np.nan
|
||||||
|
cand["max_phys"] = np.nan
|
||||||
|
|
||||||
|
# Export global candidate table
|
||||||
|
cand_out = outdir / "candidates_global.csv"
|
||||||
|
cand.to_csv(cand_out, index=False)
|
||||||
|
print(f"Globales Kandidaten-CSV: {cand_out}")
|
||||||
|
|
||||||
|
# Decide which candidates to export as timeseries
|
||||||
|
selected = []
|
||||||
|
if args.range_min is not None or args.range_max is not None:
|
||||||
|
# choose those meeting ratio threshold; group by ID and take best few per ID
|
||||||
|
ok = cand[cand["hit_ratio"] >= args.range_hit_ratio].copy()
|
||||||
|
if ok.empty:
|
||||||
|
print("Range-Filter hat keine Kandidaten gefunden; falle zurück auf Top-N nach Varianz.", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
# per ID, take best by hit_ratio then variance
|
||||||
|
for cid, grp in ok.groupby("id"):
|
||||||
|
grp = grp.sort_values(["hit_ratio","variance","changes","unique_ratio"], ascending=[False, False, False, False])
|
||||||
|
selected.extend(grp.head(args.per_id_limit).to_dict("records"))
|
||||||
|
if not selected:
|
||||||
|
# fallback → global top-N by variance (limit per ID)
|
||||||
|
per_id_count = {}
|
||||||
|
for _, row in cand.iterrows():
|
||||||
|
cid = int(row["id"]); per_id_count.setdefault(cid,0)
|
||||||
|
if len(selected) >= args.top:
|
||||||
|
break
|
||||||
|
if per_id_count[cid] >= args.per_id_limit:
|
||||||
|
continue
|
||||||
|
selected.append(row.to_dict())
|
||||||
|
per_id_count[cid] += 1
|
||||||
|
|
||||||
|
# Export per-candidate CSVs and plots
|
||||||
|
exp_index = []
|
||||||
|
base_hint = "decoded"
|
||||||
|
for row in selected:
|
||||||
|
cid = int(row["id"])
|
||||||
|
endian = row["endian"]
|
||||||
|
off = int(row["offset"])
|
||||||
|
ok, pair = export_candidate_timeseries(df_all, cid, endian, off, args.scale, args.offset, outdir, base_hint)
|
||||||
|
if ok and pair:
|
||||||
|
exp_index.append({
|
||||||
|
"id": cid,
|
||||||
|
"endian": endian,
|
||||||
|
"offset": off,
|
||||||
|
"csv": str(pair[0]),
|
||||||
|
"plot": str(pair[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
idx_df = pd.DataFrame(exp_index)
|
||||||
|
idx_path = outdir / "exports_index.csv"
|
||||||
|
idx_df.to_csv(idx_path, index=False)
|
||||||
|
print(f"Export-Index: {idx_path}")
|
||||||
|
|
||||||
|
print("Fertig. Tipp: Mit --range-min/--range-max und --scale/--offset kannst du auf plausible physikalische Bereiche filtern.")
|
||||||
|
print("Beispiel: --scale 0.01 --range-min 0 --range-max 250 (wenn raw≈cm/s → km/h)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
142
Reverse-Engineering CAN-Bus/id_signal_explorer.py
Normal file
142
Reverse-Engineering CAN-Bus/id_signal_explorer.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
LOG_PATTERN = re.compile(r"(\d+)\s+(TX|RX)\s+0x([0-9A-Fa-f]+)\s+\d+\s+((?:[0-9A-Fa-f]{2}\s+)+)")
|
||||||
|
|
||||||
|
def parse_trace(path: Path) -> pd.DataFrame:
|
||||||
|
rows = []
|
||||||
|
with open(path, "r", errors="ignore") as f:
|
||||||
|
for line in f:
|
||||||
|
m = LOG_PATTERN.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
ts = int(m.group(1))
|
||||||
|
direction = m.group(2)
|
||||||
|
can_id = int(m.group(3), 16)
|
||||||
|
data = [int(x, 16) for x in m.group(4).split() if x.strip()]
|
||||||
|
rows.append((ts, direction, can_id, data))
|
||||||
|
df = pd.DataFrame(rows, columns=["ts","dir","id","data"])
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
df["time_s"] = (df["ts"] - df["ts"].min())/1000.0
|
||||||
|
return df
|
||||||
|
|
||||||
|
def be16(b):
|
||||||
|
return (b[0]<<8) | b[1]
|
||||||
|
|
||||||
|
def le16(b):
|
||||||
|
return b[0] | (b[1]<<8)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Per-ID explorer: generate plots for 8-bit and 16-bit combinations")
|
||||||
|
ap.add_argument("trace", help="Single-ID .trace file (from can_split_by_id.py)")
|
||||||
|
ap.add_argument("--outdir", default=None, help="Output directory; default: <trace>_explore")
|
||||||
|
ap.add_argument("--prefix", default="viz", help="File prefix for exports")
|
||||||
|
ap.add_argument("--rx-only", action="store_true", help="Use only RX frames")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
trace = Path(args.trace)
|
||||||
|
df = parse_trace(trace)
|
||||||
|
if df.empty:
|
||||||
|
print("No data in trace.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.rx_only:
|
||||||
|
df = df[df["dir"]=="RX"].copy()
|
||||||
|
if df.empty:
|
||||||
|
print("No RX frames.", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
outdir = Path(args.outdir) if args.outdir else trace.with_suffix("").parent / (trace.stem + "_explore")
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# --- 8-bit channels ---
|
||||||
|
for idx in range(8):
|
||||||
|
vals = [d[idx] if len(d)>idx else None for d in df["data"].tolist()]
|
||||||
|
times = [t for t, v in zip(df["time_s"].tolist(), vals) if v is not None]
|
||||||
|
series = [v for v in vals if v is not None]
|
||||||
|
if not series:
|
||||||
|
continue
|
||||||
|
plt.figure(figsize=(10,4))
|
||||||
|
plt.plot(times, series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)")
|
||||||
|
plt.ylabel(f"Byte[{idx}] (8-bit)")
|
||||||
|
plt.title(f"{trace.name} – 8-bit Byte {idx}")
|
||||||
|
plt.grid(True)
|
||||||
|
fn = outdir / f"{args.prefix}_byte{idx}.png"
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(fn, dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
# --- 16-bit combos ---
|
||||||
|
pairs = [(i,i+1) for i in range(7)]
|
||||||
|
# LE
|
||||||
|
for i,j in pairs:
|
||||||
|
times, series = [], []
|
||||||
|
for t, d in zip(df["time_s"].tolist(), df["data"].tolist()):
|
||||||
|
if len(d) > j:
|
||||||
|
series.append(le16([d[i], d[j]])); times.append(t)
|
||||||
|
if not series:
|
||||||
|
continue
|
||||||
|
plt.figure(figsize=(10,4))
|
||||||
|
plt.plot(times, series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)")
|
||||||
|
plt.ylabel(f"LE16 @{i}-{j}")
|
||||||
|
plt.title(f"{trace.name} – LE16 Bytes {i}-{j}")
|
||||||
|
plt.grid(True)
|
||||||
|
fn = outdir / f"{args.prefix}_le16_{i}-{j}.png"
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(fn, dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
# BE
|
||||||
|
for i,j in pairs:
|
||||||
|
times, series = [], []
|
||||||
|
for t, d in zip(df["time_s"].tolist(), df["data"].tolist()):
|
||||||
|
if len(d) > j:
|
||||||
|
series.append(be16([d[i], d[j]])); times.append(t)
|
||||||
|
if not series:
|
||||||
|
continue
|
||||||
|
plt.figure(figsize=(10,4))
|
||||||
|
plt.plot(times, series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)")
|
||||||
|
plt.ylabel(f"BE16 @{i}-{j}")
|
||||||
|
plt.title(f"{trace.name} – BE16 Bytes {i}-{j}")
|
||||||
|
plt.grid(True)
|
||||||
|
fn = outdir / f"{args.prefix}_be16_{i}-{j}.png"
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(fn, dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
# Summary stats
|
||||||
|
stats = []
|
||||||
|
# 8-bit stats
|
||||||
|
for idx in range(8):
|
||||||
|
vals = [d[idx] if len(d)>idx else None for d in df["data"].tolist()]
|
||||||
|
vals = [v for v in vals if v is not None]
|
||||||
|
if not vals:
|
||||||
|
continue
|
||||||
|
arr = np.array(vals, dtype=float)
|
||||||
|
stats.append({"type":"byte8", "slot":idx, "min":float(arr.min()), "max":float(arr.max()), "var":float(arr.var())})
|
||||||
|
# 16-bit stats
|
||||||
|
for i,j in pairs:
|
||||||
|
vals = [le16([d[i],d[j]]) for d in df["data"].tolist() if len(d)>j]
|
||||||
|
if vals:
|
||||||
|
arr = np.array(vals, dtype=float)
|
||||||
|
stats.append({"type":"le16", "slot":f"{i}-{j}", "min":float(arr.min()), "max":float(arr.max()), "var":float(arr.var())})
|
||||||
|
vals = [be16([d[i],d[j]]) for d in df["data"].tolist() if len(d)>j]
|
||||||
|
if vals:
|
||||||
|
arr = np.array(vals, dtype=float)
|
||||||
|
stats.append({"type":"be16", "slot":f"{i}-{j}", "min":float(arr.min()), "max":float(arr.max()), "var":float(arr.var())})
|
||||||
|
|
||||||
|
pd.DataFrame(stats).to_csv(outdir / "summary_stats.csv", index=False)
|
||||||
|
print(f"Exported 8-bit & 16-bit plots and summary_stats.csv to {outdir}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
973
Reverse-Engineering CAN-Bus/main.py
Normal file
973
Reverse-Engineering CAN-Bus/main.py
Normal file
@@ -0,0 +1,973 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, filedialog, messagebox
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
SCRIPT_NAME = "can_universal_signal_finder.py"
|
||||||
|
SPLIT_SCRIPT = "can_split_by_id.py"
|
||||||
|
EXPLORE_SCRIPT = "id_signal_explorer.py"
|
||||||
|
TRACE_BATCH = "trace_batch_analyzer.py"
|
||||||
|
RANGE_FITTER = "trace_signal_fitter.py"
|
||||||
|
|
||||||
|
LOG_PATTERNS = ("*.log", "*.txt")
|
||||||
|
TRACE_PATTERNS = ("*.trace",)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- helpers ----------------
|
||||||
|
def now_stamp():
|
||||||
|
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
|
||||||
|
def find_logs(root: Path, rel_logs_dir: str):
|
||||||
|
base = (root / rel_logs_dir) if rel_logs_dir else root
|
||||||
|
found = []
|
||||||
|
if not base.exists():
|
||||||
|
return found
|
||||||
|
for pat in LOG_PATTERNS:
|
||||||
|
found += [str(p) for p in base.glob(pat)]
|
||||||
|
found += [str(p) for p in base.rglob(pat)] # include subdirs
|
||||||
|
return sorted(set(found))
|
||||||
|
|
||||||
|
|
||||||
|
def find_traces(base: Path):
|
||||||
|
"""Liste .trace im Basisordner und eine Ebene tiefer."""
|
||||||
|
files = []
|
||||||
|
if not base.exists():
|
||||||
|
return files
|
||||||
|
for pat in TRACE_PATTERNS:
|
||||||
|
files += [str(p) for p in base.glob(pat)]
|
||||||
|
files += [str(p) for p in base.glob(f"*/*{pat[1:]}")] # eine Ebene tiefer
|
||||||
|
return sorted(set(files))
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dir(p: Path):
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def latest_subdir(base: Path) -> Path:
|
||||||
|
"""Neuester Unterordner in base, sonst base selbst."""
|
||||||
|
if not base.exists():
|
||||||
|
return base
|
||||||
|
subs = [p for p in base.iterdir() if p.is_dir()]
|
||||||
|
if not subs:
|
||||||
|
return base
|
||||||
|
return max(subs, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- shared app state ----------------
|
||||||
|
class AppState:
|
||||||
|
def __init__(self):
|
||||||
|
# core paths
|
||||||
|
self.workdir = tk.StringVar(value="")
|
||||||
|
self.logs_dir = tk.StringVar(value="logs")
|
||||||
|
self.traces_dir = tk.StringVar(value="traces")
|
||||||
|
self.analyze_out_base = tk.StringVar(value="analyze_out")
|
||||||
|
|
||||||
|
# discovered logs
|
||||||
|
self.available_logs = [] # absolute paths
|
||||||
|
self.selected_log_indices = [] # indices in header listbox
|
||||||
|
|
||||||
|
# project defaults
|
||||||
|
self.timestamp_runs = tk.BooleanVar(value=True)
|
||||||
|
|
||||||
|
# shared traces directory + file list
|
||||||
|
self.traces_current_dir = tk.StringVar(value="") # absoluter Pfad zum aktuell angezeigten Traces-Ordner
|
||||||
|
self.traces_files = [] # Liste der .trace in current_dir (inkl. eine Ebene tiefer)
|
||||||
|
self._trace_observers = [] # callbacks, die Liste aktualisieren
|
||||||
|
|
||||||
|
# hook: wenn der Pfad geändert wird, scannen
|
||||||
|
self.traces_current_dir.trace_add("write", self._on_traces_dir_changed)
|
||||||
|
|
||||||
|
# --- path helpers ---
|
||||||
|
def workdir_path(self) -> Path:
|
||||||
|
wd = self.workdir.get().strip() or "."
|
||||||
|
return Path(wd)
|
||||||
|
|
||||||
|
def logs_base_path(self) -> Path:
|
||||||
|
return self.workdir_path() / (self.logs_dir.get().strip() or "logs")
|
||||||
|
|
||||||
|
def traces_base_path(self) -> Path:
|
||||||
|
return self.workdir_path() / (self.traces_dir.get().strip() or "traces")
|
||||||
|
|
||||||
|
def analyze_out_root(self) -> Path:
|
||||||
|
return self.workdir_path() / (self.analyze_out_base.get().strip() or "analyze_out")
|
||||||
|
|
||||||
|
# --- traces state ---
|
||||||
|
def add_trace_observer(self, cb):
|
||||||
|
if cb not in self._trace_observers:
|
||||||
|
self._trace_observers.append(cb)
|
||||||
|
|
||||||
|
def _notify_trace_observers(self):
|
||||||
|
for cb in list(self._trace_observers):
|
||||||
|
try:
|
||||||
|
cb(self.traces_files)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_traces_dir_changed(self, *_):
|
||||||
|
base = Path(self.traces_current_dir.get().strip() or str(self.traces_base_path()))
|
||||||
|
self.traces_files = find_traces(base)
|
||||||
|
self._notify_trace_observers()
|
||||||
|
|
||||||
|
def set_traces_dir(self, path: str):
|
||||||
|
self.traces_current_dir.set(path) # löst automatisch scan + notify aus
|
||||||
|
|
||||||
|
def refresh_traces(self):
|
||||||
|
# retrigger write to force refresh
|
||||||
|
self._on_traces_dir_changed()
|
||||||
|
|
||||||
|
def set_traces_to_default_or_latest(self):
|
||||||
|
base = self.traces_base_path()
|
||||||
|
target = latest_subdir(base)
|
||||||
|
self.set_traces_dir(str(target))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- header (workdir + logs selection) ----------------
|
||||||
|
class Header(ttk.Frame):
|
||||||
|
def __init__(self, master, state: AppState):
|
||||||
|
super().__init__(master, padding=8)
|
||||||
|
self.state = state
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.columnconfigure(1, weight=1)
|
||||||
|
self.columnconfigure(3, weight=1)
|
||||||
|
# row 0: workdir + scan
|
||||||
|
ttk.Label(self, text="Workdir:").grid(row=0, column=0, sticky="w")
|
||||||
|
self.ent_workdir = ttk.Entry(self, textvariable=self.state.workdir)
|
||||||
|
self.ent_workdir.grid(row=0, column=1, sticky="ew", padx=6)
|
||||||
|
ttk.Button(self, text="Wählen…", command=self.pick_workdir).grid(row=0, column=2, padx=5)
|
||||||
|
ttk.Button(self, text="Logs scannen", command=self.scan_logs).grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
|
# row 1: subfolders + timestamp checkbox
|
||||||
|
ttk.Label(self, text="Logs-Unterordner:").grid(row=1, column=0, sticky="w")
|
||||||
|
ttk.Entry(self, textvariable=self.state.logs_dir, width=24).grid(row=1, column=1, sticky="w", padx=6)
|
||||||
|
|
||||||
|
ttk.Label(self, text="Traces-Unterordner:").grid(row=1, column=2, sticky="w")
|
||||||
|
ttk.Entry(self, textvariable=self.state.traces_dir, width=24).grid(row=1, column=3, sticky="w", padx=6)
|
||||||
|
|
||||||
|
ttk.Label(self, text="Analyze-Output:").grid(row=2, column=0, sticky="w")
|
||||||
|
ttk.Entry(self, textvariable=self.state.analyze_out_base, width=24).grid(row=2, column=1, sticky="w", padx=6)
|
||||||
|
ttk.Checkbutton(self, text="Zeitstempel-Unterordner pro Run", variable=self.state.timestamp_runs).grid(row=2, column=2, columnspan=2, sticky="w")
|
||||||
|
|
||||||
|
# row 3: logs list
|
||||||
|
frm = ttk.LabelFrame(self, text="Gefundene Logdateien (Mehrfachauswahl möglich)")
|
||||||
|
frm.grid(row=3, column=0, columnspan=4, sticky="nsew", pady=(8,0))
|
||||||
|
self.rowconfigure(3, weight=1)
|
||||||
|
frm.columnconfigure(0, weight=1)
|
||||||
|
frm.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.lst_logs = tk.Listbox(frm, height=6, selectmode=tk.EXTENDED)
|
||||||
|
self.lst_logs.grid(row=0, column=0, sticky="nsew", padx=(8,4), pady=8)
|
||||||
|
|
||||||
|
btns = ttk.Frame(frm)
|
||||||
|
btns.grid(row=0, column=1, sticky="ns", padx=(4,8), pady=8)
|
||||||
|
ttk.Button(btns, text="Alle wählen", command=self.select_all).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Keine", command=self.select_none).pack(fill="x", pady=2)
|
||||||
|
ttk.Separator(btns, orient="horizontal").pack(fill="x", pady=6)
|
||||||
|
ttk.Button(btns, text="Manuell hinzufügen…", command=self.add_logs_manual).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Entfernen", command=self.remove_selected_logs).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Liste leeren", command=self.clear_logs).pack(fill="x", pady=2)
|
||||||
|
ttk.Separator(btns, orient="horizontal").pack(fill="x", pady=6)
|
||||||
|
ttk.Button(btns, text="Projekt speichern…", command=self.save_project).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Projekt laden…", command=self.load_project).pack(fill="x", pady=2)
|
||||||
|
|
||||||
|
# ---- actions ----
|
||||||
|
def pick_workdir(self):
|
||||||
|
d = filedialog.askdirectory(title="Workdir auswählen")
|
||||||
|
if d:
|
||||||
|
self.state.workdir.set(d)
|
||||||
|
self.scan_logs()
|
||||||
|
# automatisch auch traces default/latest setzen
|
||||||
|
self.state.set_traces_to_default_or_latest()
|
||||||
|
|
||||||
|
def scan_logs(self):
|
||||||
|
wd = self.state.workdir_path()
|
||||||
|
logs_dir = self.state.logs_dir.get().strip()
|
||||||
|
found = find_logs(wd, logs_dir)
|
||||||
|
self.state.available_logs = found
|
||||||
|
self.lst_logs.delete(0, tk.END)
|
||||||
|
for p in found:
|
||||||
|
self.lst_logs.insert(tk.END, p)
|
||||||
|
# default-select all
|
||||||
|
self.lst_logs.select_set(0, tk.END)
|
||||||
|
self.state.selected_log_indices = list(range(len(found)))
|
||||||
|
|
||||||
|
def select_all(self):
|
||||||
|
self.lst_logs.select_set(0, tk.END)
|
||||||
|
self.state.selected_log_indices = list(range(self.lst_logs.size()))
|
||||||
|
|
||||||
|
def select_none(self):
|
||||||
|
self.lst_logs.select_clear(0, tk.END)
|
||||||
|
self.state.selected_log_indices = []
|
||||||
|
|
||||||
|
def add_logs_manual(self):
|
||||||
|
paths = filedialog.askopenfilenames(title="Logdateien auswählen", filetypes=[("Logfiles","*.log *.txt"),("Alle Dateien","*.*")])
|
||||||
|
if not paths: return
|
||||||
|
for p in paths:
|
||||||
|
if p not in self.state.available_logs:
|
||||||
|
self.state.available_logs.append(p)
|
||||||
|
self.lst_logs.insert(tk.END, p)
|
||||||
|
# if workdir empty, infer from first added
|
||||||
|
if not self.state.workdir.get().strip():
|
||||||
|
self.state.workdir.set(str(Path(paths[0]).resolve().parent))
|
||||||
|
# auch traces default/latest
|
||||||
|
self.state.set_traces_to_default_or_latest()
|
||||||
|
|
||||||
|
def remove_selected_logs(self):
|
||||||
|
sel = list(self.lst_logs.curselection())
|
||||||
|
sel.reverse()
|
||||||
|
for i in sel:
|
||||||
|
p = self.lst_logs.get(i)
|
||||||
|
if p in self.state.available_logs:
|
||||||
|
self.state.available_logs.remove(p)
|
||||||
|
self.lst_logs.delete(i)
|
||||||
|
self.state.selected_log_indices = [i for i in range(self.lst_logs.size()) if self.lst_logs.select_includes(i)]
|
||||||
|
|
||||||
|
def clear_logs(self):
|
||||||
|
self.state.available_logs = []
|
||||||
|
self.lst_logs.delete(0, tk.END)
|
||||||
|
self.state.selected_log_indices = []
|
||||||
|
|
||||||
|
def selected_logs(self):
|
||||||
|
idx = self.lst_logs.curselection()
|
||||||
|
if not idx:
|
||||||
|
return []
|
||||||
|
return [self.lst_logs.get(i) for i in idx]
|
||||||
|
|
||||||
|
# ---- project save/load ----
|
||||||
|
def collect_project(self):
|
||||||
|
return {
|
||||||
|
"workdir": self.state.workdir.get(),
|
||||||
|
"logs_dir": self.state.logs_dir.get(),
|
||||||
|
"traces_dir": self.state.traces_dir.get(),
|
||||||
|
"analyze_out_base": self.state.analyze_out_base.get(),
|
||||||
|
"timestamp_runs": bool(self.state.timestamp_runs.get()),
|
||||||
|
"available_logs": self.state.available_logs,
|
||||||
|
"selected_indices": list(self.lst_logs.curselection()),
|
||||||
|
"traces_current_dir": self.state.traces_current_dir.get(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply_project(self, cfg):
|
||||||
|
self.state.workdir.set(cfg.get("workdir",""))
|
||||||
|
self.state.logs_dir.set(cfg.get("logs_dir","logs"))
|
||||||
|
self.state.traces_dir.set(cfg.get("traces_dir","traces"))
|
||||||
|
self.state.analyze_out_base.set(cfg.get("analyze_out_base","analyze_out"))
|
||||||
|
self.state.timestamp_runs.set(cfg.get("timestamp_runs", True))
|
||||||
|
# restore logs
|
||||||
|
self.scan_logs()
|
||||||
|
# If project contained explicit available_logs, merge
|
||||||
|
for p in cfg.get("available_logs", []):
|
||||||
|
if p not in self.state.available_logs:
|
||||||
|
self.state.available_logs.append(p)
|
||||||
|
self.lst_logs.insert(tk.END, p)
|
||||||
|
# re-select indices if valid
|
||||||
|
self.lst_logs.select_clear(0, tk.END)
|
||||||
|
for i in cfg.get("selected_indices", []):
|
||||||
|
if 0 <= i < self.lst_logs.size():
|
||||||
|
self.lst_logs.select_set(i)
|
||||||
|
# traces current dir: sofern vorhanden nutzen, sonst default/latest
|
||||||
|
tdir = cfg.get("traces_current_dir", "")
|
||||||
|
if tdir and Path(tdir).exists():
|
||||||
|
self.state.set_traces_dir(tdir)
|
||||||
|
else:
|
||||||
|
self.state.set_traces_to_default_or_latest()
|
||||||
|
|
||||||
|
def save_project(self):
|
||||||
|
cfg = self.collect_project()
|
||||||
|
path = filedialog.asksaveasfilename(title="Projekt speichern", defaultextension=".json", filetypes=[("Projektdatei","*.json")])
|
||||||
|
if not path: return
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
messagebox.showinfo("Gespeichert", f"Projekt gespeichert:\n{path}")
|
||||||
|
|
||||||
|
def load_project(self):
|
||||||
|
path = filedialog.askopenfilename(title="Projekt laden", filetypes=[("Projektdatei","*.json"),("Alle Dateien","*.*")])
|
||||||
|
if not path: return
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
self.apply_project(cfg)
|
||||||
|
messagebox.showinfo("Geladen", f"Projekt geladen:\n{path}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- shared Trace Panel ----------------
|
||||||
|
class TracePanel(ttk.LabelFrame):
|
||||||
|
"""
|
||||||
|
Einheitliche Trace-Auswahl: Liste links, Buttons rechts.
|
||||||
|
Nutzt AppState.traces_current_dir + AppState.traces_files.
|
||||||
|
single_select=True => Listbox SINGLE, versteckt 'Alle/Keine'.
|
||||||
|
"""
|
||||||
|
def __init__(self, master, state: AppState, title="Traces", single_select=False, height=10):
|
||||||
|
super().__init__(master, text=title)
|
||||||
|
self.state = state
|
||||||
|
self.single_select = single_select
|
||||||
|
self.height = height
|
||||||
|
self._build_ui()
|
||||||
|
# subscribe to state updates
|
||||||
|
self.state.add_trace_observer(self._on_traces_updated)
|
||||||
|
# initial fill from state
|
||||||
|
self._on_traces_updated(self.state.traces_files)
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.columnconfigure(0, weight=1)
|
||||||
|
self.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
selectmode = tk.SINGLE if self.single_select else tk.EXTENDED
|
||||||
|
self.lst = tk.Listbox(self, height=self.height, selectmode=selectmode)
|
||||||
|
self.lst.grid(row=0, column=0, sticky="nsew", padx=(8,4), pady=8)
|
||||||
|
|
||||||
|
btns = ttk.Frame(self)
|
||||||
|
btns.grid(row=0, column=1, sticky="ns", padx=(4,8), pady=8)
|
||||||
|
|
||||||
|
ttk.Button(btns, text="Traces-Ordner wählen…", command=self._pick_traces_dir).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Workdir/traces", command=self._use_default_traces).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Neuester Split", command=self._use_latest_split).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Refresh", command=self._refresh_traces).pack(fill="x", pady=6)
|
||||||
|
if not self.single_select:
|
||||||
|
ttk.Button(btns, text="Alle wählen", command=lambda: self.lst.select_set(0, tk.END)).pack(fill="x", pady=2)
|
||||||
|
ttk.Button(btns, text="Keine", command=lambda: self.lst.select_clear(0, tk.END)).pack(fill="x", pady=2)
|
||||||
|
|
||||||
|
# --- state sync ---
|
||||||
|
def _on_traces_updated(self, files):
|
||||||
|
# refresh list content
|
||||||
|
cur_sel_paths = self.get_selected()
|
||||||
|
self.lst.delete(0, tk.END)
|
||||||
|
for p in files:
|
||||||
|
self.lst.insert(tk.END, p)
|
||||||
|
# try to restore selection
|
||||||
|
if cur_sel_paths:
|
||||||
|
path_to_index = {self.lst.get(i): i for i in range(self.lst.size())}
|
||||||
|
for p in cur_sel_paths:
|
||||||
|
if p in path_to_index:
|
||||||
|
self.lst.select_set(path_to_index[p])
|
||||||
|
|
||||||
|
def _pick_traces_dir(self):
|
||||||
|
d = filedialog.askdirectory(title="Traces-Ordner wählen", initialdir=str(self.state.traces_base_path()))
|
||||||
|
if d:
|
||||||
|
self.state.set_traces_dir(d)
|
||||||
|
|
||||||
|
def _use_default_traces(self):
|
||||||
|
# default or latest under Workdir/traces
|
||||||
|
self.state.set_traces_to_default_or_latest()
|
||||||
|
|
||||||
|
def _use_latest_split(self):
|
||||||
|
base = self.state.traces_base_path()
|
||||||
|
target = latest_subdir(base)
|
||||||
|
self.state.set_traces_dir(str(target))
|
||||||
|
|
||||||
|
def _refresh_traces(self):
|
||||||
|
self.state.refresh_traces()
|
||||||
|
|
||||||
|
def get_selected(self):
|
||||||
|
idx = self.lst.curselection()
|
||||||
|
return [self.lst.get(i) for i in idx]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Tab 1: Multi-Log Analyse (ranking optional) ----------------
|
||||||
|
class TabAnalyze(ttk.Frame):
|
||||||
|
def __init__(self, master, state: AppState, header: Header):
|
||||||
|
super().__init__(master, padding=10)
|
||||||
|
self.state = state
|
||||||
|
self.header = header
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.columnconfigure(0, weight=1)
|
||||||
|
self.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
|
# params
|
||||||
|
params = ttk.LabelFrame(self, text="Analyse-Parameter")
|
||||||
|
params.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
for c in (1,3):
|
||||||
|
params.columnconfigure(c, weight=1)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Include-IDs (z.B. 0x208,0x209):").grid(row=0, column=0, sticky="w")
|
||||||
|
self.include_var = tk.StringVar(value="")
|
||||||
|
ttk.Entry(params, textvariable=self.include_var).grid(row=0, column=1, sticky="ew", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Exclude-IDs:").grid(row=0, column=2, sticky="w")
|
||||||
|
self.exclude_var = tk.StringVar(value="")
|
||||||
|
ttk.Entry(params, textvariable=self.exclude_var).grid(row=0, column=3, sticky="ew", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Scale:").grid(row=1, column=0, sticky="w")
|
||||||
|
self.scale_var = tk.DoubleVar(value=1.0)
|
||||||
|
ttk.Entry(params, textvariable=self.scale_var, width=12).grid(row=1, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Offset:").grid(row=1, column=2, sticky="w")
|
||||||
|
self.offset_var = tk.DoubleVar(value=0.0)
|
||||||
|
ttk.Entry(params, textvariable=self.offset_var, width=12).grid(row=1, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Range-Min:").grid(row=2, column=0, sticky="w")
|
||||||
|
self.rmin_var = tk.StringVar(value="")
|
||||||
|
ttk.Entry(params, textvariable=self.rmin_var, width=12).grid(row=2, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Range-Max:").grid(row=2, column=2, sticky="w")
|
||||||
|
self.rmax_var = tk.StringVar(value="")
|
||||||
|
ttk.Entry(params, textvariable=self.rmax_var, width=12).grid(row=2, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Range-Hit-Ratio (0..1):").grid(row=3, column=0, sticky="w")
|
||||||
|
self.hit_ratio_var = tk.DoubleVar(value=0.6)
|
||||||
|
ttk.Entry(params, textvariable=self.hit_ratio_var, width=12).grid(row=3, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Top-N (Fallback):").grid(row=3, column=2, sticky="w")
|
||||||
|
self.top_var = tk.IntVar(value=20)
|
||||||
|
ttk.Entry(params, textvariable=self.top_var, width=12).grid(row=3, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(params, text="Per-ID-Limit:").grid(row=4, column=0, sticky="w")
|
||||||
|
self.per_id_limit_var = tk.IntVar(value=2)
|
||||||
|
ttk.Entry(params, textvariable=self.per_id_limit_var, width=12).grid(row=4, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
self.run_separately_var = tk.BooleanVar(value=False)
|
||||||
|
ttk.Checkbutton(params, text="Jede Logdatei separat laufen lassen", variable=self.run_separately_var).grid(row=4, column=2, columnspan=2, sticky="w", padx=5)
|
||||||
|
|
||||||
|
# run + console
|
||||||
|
run = ttk.Frame(self)
|
||||||
|
run.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
ttk.Button(run, text="Analyse starten (Ranking)", command=self.on_run).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
out = ttk.LabelFrame(self, text="Ausgabe")
|
||||||
|
out.grid(row=2, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
out.columnconfigure(0, weight=1); out.rowconfigure(0, weight=1)
|
||||||
|
self.txt = tk.Text(out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sb = ttk.Scrollbar(out, orient="vertical", command=self.txt.yview); sb.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.txt.configure(yscrollcommand=sb.set)
|
||||||
|
|
||||||
|
def on_run(self):
|
||||||
|
logs = self.header.selected_logs()
|
||||||
|
if not logs:
|
||||||
|
messagebox.showwarning("Hinweis", "Bitte oben im Header Logdateien auswählen.")
|
||||||
|
return
|
||||||
|
t = threading.Thread(target=self._run_worker, args=(logs,), daemon=True)
|
||||||
|
self.txt.delete("1.0", tk.END)
|
||||||
|
self._append("Starte Analyse…\n")
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _run_worker(self, logs):
|
||||||
|
script_path = Path(__file__).parent / SCRIPT_NAME
|
||||||
|
if not script_path.exists():
|
||||||
|
self._append(f"[Fehler] Script nicht gefunden: {script_path}\n"); return
|
||||||
|
|
||||||
|
# output root: workdir/analyze_out/<ts>_multilog
|
||||||
|
out_root = self.state.analyze_out_root()
|
||||||
|
stamp = now_stamp() + "_multilog"
|
||||||
|
outdir = ensure_dir(out_root / stamp)
|
||||||
|
|
||||||
|
def build_args():
|
||||||
|
args = [sys.executable, str(script_path)]
|
||||||
|
if self.include_var.get().strip():
|
||||||
|
args += ["--include-ids", self.include_var.get().strip()]
|
||||||
|
if self.exclude_var.get().strip():
|
||||||
|
args += ["--exclude-ids", self.exclude_var.get().strip()]
|
||||||
|
args += ["--scale", str(self.scale_var.get()), "--offset", str(self.offset_var.get())]
|
||||||
|
if self.rmin_var.get().strip(): args += ["--range-min", self.rmin_var.get().strip()]
|
||||||
|
if self.rmax_var.get().strip(): args += ["--range-max", self.rmax_var.get().strip()]
|
||||||
|
args += ["--range-hit-ratio", str(self.hit_ratio_var.get())]
|
||||||
|
args += ["--top", str(self.top_var.get()), "--per-id-limit", str(self.per_id_limit_var.get())]
|
||||||
|
return args
|
||||||
|
|
||||||
|
if self.run_separately_var.get():
|
||||||
|
for p in logs:
|
||||||
|
sub = ensure_dir(outdir / Path(p).stem)
|
||||||
|
cmd = build_args() + ["--outdir", str(sub), p]
|
||||||
|
self._run_cmd(cmd)
|
||||||
|
else:
|
||||||
|
cmd = build_args() + ["--outdir", str(outdir)] + logs
|
||||||
|
self._run_cmd(cmd)
|
||||||
|
|
||||||
|
self._append(f"\nDone. Output: {outdir}\n")
|
||||||
|
|
||||||
|
def _run_cmd(self, cmd):
|
||||||
|
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||||||
|
try:
|
||||||
|
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||||||
|
for line in proc.stdout: self._append(line)
|
||||||
|
rc = proc.wait()
|
||||||
|
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
|
||||||
|
except Exception as e:
|
||||||
|
self._append(f"[Fehler] {e}\n")
|
||||||
|
|
||||||
|
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Tab 2: ID Explorer (split + single-ID analyze) ----------------
|
||||||
|
class TabExplorer(ttk.Frame):
|
||||||
|
def __init__(self, master, state: AppState, header: Header):
|
||||||
|
super().__init__(master, padding=10)
|
||||||
|
self.state = state
|
||||||
|
self.header = header
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.columnconfigure(0, weight=1)
|
||||||
|
self.rowconfigure(3, weight=1)
|
||||||
|
|
||||||
|
# split controls
|
||||||
|
frm_split = ttk.LabelFrame(self, text="Split: Logs → per-ID Traces")
|
||||||
|
frm_split.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
frm_split.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
self.rx_only_var = tk.BooleanVar(value=False)
|
||||||
|
self.ts_split_var = tk.BooleanVar(value=True)
|
||||||
|
|
||||||
|
ttk.Label(frm_split, text="Ziel (Workdir/traces[/timestamp])").grid(row=0, column=0, sticky="w", padx=5)
|
||||||
|
ttk.Label(frm_split, textvariable=self.state.traces_dir).grid(row=0, column=1, sticky="w")
|
||||||
|
ttk.Checkbutton(frm_split, text="nur RX", variable=self.rx_only_var).grid(row=1, column=0, sticky="w", padx=5)
|
||||||
|
ttk.Checkbutton(frm_split, text="Zeitstempel-Unterordner", variable=self.ts_split_var).grid(row=1, column=1, sticky="w", padx=5)
|
||||||
|
ttk.Button(frm_split, text="Split starten", command=self.on_split).grid(row=1, column=2, sticky="e", padx=5)
|
||||||
|
|
||||||
|
# unified trace panel (multi-select)
|
||||||
|
self.trace_panel = TracePanel(self, self.state, title="Traces im ausgewählten Ordner", single_select=False, height=10)
|
||||||
|
self.trace_panel.grid(row=1, column=0, sticky="nsew", padx=5, pady=(8,10))
|
||||||
|
|
||||||
|
# single-ID analyze
|
||||||
|
frm_one = ttk.LabelFrame(self, text="Einzel-ID Analyse (Plots + summary_stats)")
|
||||||
|
frm_one.grid(row=2, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
frm_one.columnconfigure(1, weight=1)
|
||||||
|
ttk.Label(frm_one, text="Output-Basis (unter Workdir/analyze_out):").grid(row=0, column=0, sticky="w")
|
||||||
|
self.one_out_base = tk.StringVar(value="id_explore")
|
||||||
|
ttk.Entry(frm_one, textvariable=self.one_out_base).grid(row=0, column=1, sticky="ew", padx=5)
|
||||||
|
self.ts_one = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(frm_one, text="Zeitstempel-Unterordner", variable=self.ts_one).grid(row=0, column=2, sticky="w", padx=5)
|
||||||
|
ttk.Button(frm_one, text="Analyse starten", command=self.on_one_analyze).grid(row=0, column=3, sticky="e", padx=5)
|
||||||
|
|
||||||
|
# console
|
||||||
|
out = ttk.LabelFrame(self, text="Ausgabe")
|
||||||
|
out.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
out.columnconfigure(0, weight=1); out.rowconfigure(0, weight=1)
|
||||||
|
self.txt = tk.Text(out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sb = ttk.Scrollbar(out, orient="vertical", command=self.txt.yview); sb.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.txt.configure(yscrollcommand=sb.set)
|
||||||
|
|
||||||
|
def on_split(self):
|
||||||
|
logs = self.header.selected_logs()
|
||||||
|
if not logs:
|
||||||
|
messagebox.showwarning("Hinweis", "Bitte oben im Header Logdateien auswählen."); return
|
||||||
|
outdir = self.state.traces_base_path()
|
||||||
|
if self.ts_split_var.get(): outdir = outdir / now_stamp()
|
||||||
|
ensure_dir(outdir)
|
||||||
|
cmd = [sys.executable, str(Path(__file__).parent / SPLIT_SCRIPT), "--outdir", str(outdir)]
|
||||||
|
if self.rx_only_var.get(): cmd.append("--rx-only")
|
||||||
|
cmd += logs
|
||||||
|
self._run_cmd(cmd)
|
||||||
|
# nach dem Split: globalen Traces-Ordner setzen (neuester Ordner)
|
||||||
|
self.state.set_traces_dir(str(outdir))
|
||||||
|
|
||||||
|
def on_one_analyze(self):
|
||||||
|
sel = self.trace_panel.get_selected()
|
||||||
|
if not sel:
|
||||||
|
messagebox.showwarning("Hinweis", "Bitte mindestens eine .trace auswählen."); return
|
||||||
|
out_root = self.state.analyze_out_root()
|
||||||
|
stamp = now_stamp() + "_id_explore" if self.ts_one.get() else "id_explore"
|
||||||
|
outdir = ensure_dir(out_root / stamp)
|
||||||
|
for trace in sel:
|
||||||
|
cmd = [sys.executable, str(Path(__file__).parent / EXPLORE_SCRIPT), "--outdir", str(outdir), trace]
|
||||||
|
self._run_cmd(cmd)
|
||||||
|
self._append(f"\nDone. Output: {outdir}\n")
|
||||||
|
|
||||||
|
def _run_cmd(self, cmd):
|
||||||
|
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||||||
|
try:
|
||||||
|
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||||||
|
for line in proc.stdout: self._append(line)
|
||||||
|
rc = proc.wait()
|
||||||
|
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
|
||||||
|
except Exception as e:
|
||||||
|
self._append(f"[Fehler] {e}\n")
|
||||||
|
|
||||||
|
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- Tab 3: Traces Batch-Analyse ----------------
|
||||||
|
class TabTraceBatch(ttk.Frame):
|
||||||
|
def __init__(self, master, state: AppState, header: Header):
|
||||||
|
super().__init__(master, padding=10)
|
||||||
|
self.state = state
|
||||||
|
self.header = header
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.columnconfigure(0, weight=1)
|
||||||
|
self.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
|
# unified trace panel (multi-select)
|
||||||
|
self.trace_panel = TracePanel(self, self.state, title="Traces (Ordner/Subset wählen)", single_select=False, height=10)
|
||||||
|
self.trace_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=(5,10))
|
||||||
|
|
||||||
|
# Params
|
||||||
|
pr = ttk.LabelFrame(self, text="Analyse-Parameter")
|
||||||
|
pr.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
for c in (1,3):
|
||||||
|
pr.columnconfigure(c, weight=1)
|
||||||
|
|
||||||
|
self.rx_only = tk.BooleanVar(value=False)
|
||||||
|
ttk.Checkbutton(pr, text="nur RX", variable=self.rx_only).grid(row=0, column=0, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(pr, text="Scale").grid(row=0, column=1, sticky="e")
|
||||||
|
self.scale = tk.DoubleVar(value=1.0)
|
||||||
|
ttk.Entry(pr, textvariable=self.scale, width=12).grid(row=0, column=2, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(pr, text="Offset").grid(row=0, column=3, sticky="e")
|
||||||
|
self.offset = tk.DoubleVar(value=0.0)
|
||||||
|
ttk.Entry(pr, textvariable=self.offset, width=12).grid(row=0, column=4, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(pr, text="Range-Min").grid(row=1, column=1, sticky="e")
|
||||||
|
self.rmin = tk.StringVar(value="")
|
||||||
|
ttk.Entry(pr, textvariable=self.rmin, width=12).grid(row=1, column=2, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(pr, text="Range-Max").grid(row=1, column=3, sticky="e")
|
||||||
|
self.rmax = tk.StringVar(value="")
|
||||||
|
ttk.Entry(pr, textvariable=self.rmax, width=12).grid(row=1, column=4, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(pr, text="Top pro Trace").grid(row=2, column=1, sticky="e")
|
||||||
|
self.top = tk.IntVar(value=8)
|
||||||
|
ttk.Entry(pr, textvariable=self.top, width=12).grid(row=2, column=2, sticky="w", padx=5)
|
||||||
|
|
||||||
|
self.use_ts = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(pr, text="Zeitstempel-Unterordner", variable=self.use_ts).grid(row=2, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
# Run & console
|
||||||
|
run = ttk.Frame(self)
|
||||||
|
run.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
ttk.Button(run, text="Batch starten", command=self.on_run).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
out = ttk.LabelFrame(self, text="Ausgabe")
|
||||||
|
out.grid(row=4, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
out.columnconfigure(0, weight=1); out.rowconfigure(0, weight=1)
|
||||||
|
self.txt = tk.Text(out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sb = ttk.Scrollbar(out, orient="vertical", command=self.txt.yview); sb.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.txt.configure(yscrollcommand=sb.set)
|
||||||
|
|
||||||
|
def on_run(self):
|
||||||
|
# nutze Auswahl oder – falls leer – kompletten Ordner
|
||||||
|
selected = self.trace_panel.get_selected()
|
||||||
|
traces_dir = Path(self.state.traces_current_dir.get().strip() or str(self.state.traces_base_path()))
|
||||||
|
if not traces_dir.exists():
|
||||||
|
messagebox.showwarning("Hinweis", "Bitte gültigen Traces-Ordner wählen."); return
|
||||||
|
|
||||||
|
out_root = self.state.analyze_out_root()
|
||||||
|
label = "trace_batch"
|
||||||
|
stamp = now_stamp() + "_" + label if self.use_ts.get() else label
|
||||||
|
outdir = ensure_dir(out_root / stamp)
|
||||||
|
|
||||||
|
# falls Auswahl getroffen wurde, temporären Subset-Ordner bauen
|
||||||
|
subset_dir = None
|
||||||
|
if selected:
|
||||||
|
subset_dir = ensure_dir(outdir / "_subset")
|
||||||
|
for p in selected:
|
||||||
|
src = Path(p)
|
||||||
|
dst = subset_dir / src.name
|
||||||
|
try:
|
||||||
|
# versuchen Hardlink (schnell, platzsparend)
|
||||||
|
if dst.exists():
|
||||||
|
dst.unlink()
|
||||||
|
os.link(src, dst)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: Kopieren
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
run_dir = subset_dir if subset_dir else traces_dir
|
||||||
|
|
||||||
|
cmd = [sys.executable, str(Path(__file__).parent/TRACE_BATCH),
|
||||||
|
"--traces-dir", str(run_dir), "--outdir", str(outdir),
|
||||||
|
"--scale", str(self.scale.get()), "--offset", str(self.offset.get()),
|
||||||
|
"--top", str(self.top.get()), "--plots"]
|
||||||
|
if self.rmin.get().strip(): cmd += ["--range-min", self.rmin.get().strip()]
|
||||||
|
if self.rmax.get().strip(): cmd += ["--range-max", self.rmax.get().strip()]
|
||||||
|
if self.rx_only.get(): cmd.append("--rx-only")
|
||||||
|
|
||||||
|
self._run_cmd(cmd)
|
||||||
|
self._append(f"\nDone. Output: {outdir}\n")
|
||||||
|
|
||||||
|
def _run_cmd(self, cmd):
|
||||||
|
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||||||
|
try:
|
||||||
|
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||||||
|
for line in proc.stdout: self._append(line)
|
||||||
|
rc = proc.wait()
|
||||||
|
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
|
||||||
|
except Exception as e:
|
||||||
|
self._append(f"[Fehler] {e}\n")
|
||||||
|
|
||||||
|
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||||||
|
|
||||||
|
# ---------------- Tab 4: Range-Fit (supervised + unsupervised, mit Physik-Constraints) ----------------
|
||||||
|
class TabRangeFit(ttk.Frame):
|
||||||
|
def __init__(self, master, state: AppState, header: Header):
|
||||||
|
super().__init__(master, padding=10)
|
||||||
|
self.state = state
|
||||||
|
self.header = header
|
||||||
|
self._last_outdir = None
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.columnconfigure(0, weight=1)
|
||||||
|
self.rowconfigure(3, weight=1)
|
||||||
|
|
||||||
|
# unified trace panel (single-select)
|
||||||
|
self.trace_panel = TracePanel(self, self.state, title="Trace wählen (Single)", single_select=True, height=10)
|
||||||
|
self.trace_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=(5,10))
|
||||||
|
|
||||||
|
# Parameter Frames
|
||||||
|
frm_params = ttk.Frame(self)
|
||||||
|
frm_params.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
for c in range(6):
|
||||||
|
frm_params.columnconfigure(c, weight=1)
|
||||||
|
|
||||||
|
# --- Supervised (Range & Physik) ---
|
||||||
|
box_sup = ttk.LabelFrame(frm_params, text="Supervised (Range-Fit) – lasse leer für Unsupervised")
|
||||||
|
box_sup.grid(row=0, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
|
||||||
|
for c in range(6):
|
||||||
|
box_sup.columnconfigure(c, weight=1)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Range-Min").grid(row=0, column=0, sticky="e")
|
||||||
|
self.rmin = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.rmin, width=12).grid(row=0, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Range-Max").grid(row=0, column=2, sticky="e")
|
||||||
|
self.rmax = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.rmax, width=12).grid(row=0, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Min. Hit-Ratio (0..1)").grid(row=0, column=4, sticky="e")
|
||||||
|
self.min_hit = tk.DoubleVar(value=0.5)
|
||||||
|
ttk.Entry(box_sup, textvariable=self.min_hit, width=10).grid(row=0, column=5, sticky="w", padx=5)
|
||||||
|
|
||||||
|
self.allow_neg = tk.BooleanVar(value=False)
|
||||||
|
ttk.Checkbutton(box_sup, text="negative Scale erlauben", variable=self.allow_neg).grid(row=1, column=0, columnspan=2, sticky="w")
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Rate-Min (Hz)").grid(row=1, column=2, sticky="e")
|
||||||
|
self.rate_min = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.rate_min, width=10).grid(row=1, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Rate-Max (Hz)").grid(row=1, column=4, sticky="e")
|
||||||
|
self.rate_max = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.rate_max, width=10).grid(row=1, column=5, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Jitter-Max (ms)").grid(row=2, column=0, sticky="e")
|
||||||
|
self.jitter_max = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.jitter_max, width=10).grid(row=2, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Max-Slope-Abs (phys/s)").grid(row=2, column=2, sticky="e")
|
||||||
|
self.slope_abs = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.slope_abs, width=12).grid(row=2, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Max-Slope-Frac (/s)").grid(row=2, column=4, sticky="e")
|
||||||
|
self.slope_frac = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.slope_frac, width=12).grid(row=2, column=5, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Slope-Quantile").grid(row=3, column=0, sticky="e")
|
||||||
|
self.slope_q = tk.DoubleVar(value=0.95) # 0.95 oder 0.99
|
||||||
|
ttk.Entry(box_sup, textvariable=self.slope_q, width=10).grid(row=3, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_sup, text="Min-Unique-Ratio").grid(row=3, column=2, sticky="e")
|
||||||
|
self.min_uniq = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_sup, textvariable=self.min_uniq, width=10).grid(row=3, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
# --- Unsupervised ---
|
||||||
|
box_uns = ttk.LabelFrame(frm_params, text="Unsupervised (ohne Range)")
|
||||||
|
box_uns.grid(row=1, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
|
||||||
|
for c in range(6):
|
||||||
|
box_uns.columnconfigure(c, weight=1)
|
||||||
|
|
||||||
|
ttk.Label(box_uns, text="Min. Smoothness (0..1)").grid(row=0, column=0, sticky="e")
|
||||||
|
self.min_smooth = tk.DoubleVar(value=0.2)
|
||||||
|
ttk.Entry(box_uns, textvariable=self.min_smooth, width=12).grid(row=0, column=1, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_uns, text="Max-Slope-Frac-RAW (/s)").grid(row=0, column=2, sticky="e")
|
||||||
|
self.max_slope_frac_raw = tk.StringVar(value="")
|
||||||
|
ttk.Entry(box_uns, textvariable=self.max_slope_frac_raw, width=12).grid(row=0, column=3, sticky="w", padx=5)
|
||||||
|
|
||||||
|
# --- Allgemein/Output ---
|
||||||
|
box_out = ttk.LabelFrame(frm_params, text="Allgemein & Output")
|
||||||
|
box_out.grid(row=2, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
|
||||||
|
for c in range(6):
|
||||||
|
box_out.columnconfigure(c, weight=1)
|
||||||
|
|
||||||
|
self.rx_only = tk.BooleanVar(value=False)
|
||||||
|
ttk.Checkbutton(box_out, text="nur RX", variable=self.rx_only).grid(row=0, column=0, sticky="w")
|
||||||
|
|
||||||
|
ttk.Label(box_out, text="Plots Top-N").grid(row=0, column=1, sticky="e")
|
||||||
|
self.plots_top = tk.IntVar(value=8)
|
||||||
|
ttk.Entry(box_out, textvariable=self.plots_top, width=10).grid(row=0, column=2, sticky="w", padx=5)
|
||||||
|
|
||||||
|
ttk.Label(box_out, text="Output-Label").grid(row=0, column=3, sticky="e")
|
||||||
|
self.out_label = tk.StringVar(value="rangefit")
|
||||||
|
ttk.Entry(box_out, textvariable=self.out_label, width=18).grid(row=0, column=4, sticky="w", padx=5)
|
||||||
|
|
||||||
|
self.use_ts = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(box_out, text="Zeitstempel-Unterordner", variable=self.use_ts).grid(row=0, column=5, sticky="w")
|
||||||
|
|
||||||
|
# Start + Konsole + Aktionen
|
||||||
|
frm_run = ttk.Frame(self)
|
||||||
|
frm_run.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
ttk.Button(frm_run, text="Start Range-/Unsupervised-Fit", command=self._on_run).pack(side="left", padx=5)
|
||||||
|
ttk.Button(frm_run, text="Report öffnen", command=self._open_last_report).pack(side="left", padx=5)
|
||||||
|
ttk.Button(frm_run, text="Output-Ordner öffnen", command=self._open_last_outdir).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
frm_out = ttk.LabelFrame(self, text="Ausgabe")
|
||||||
|
frm_out.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
|
frm_out.columnconfigure(0, weight=1); frm_out.rowconfigure(0, weight=1)
|
||||||
|
self.txt = tk.Text(frm_out, height=14); self.txt.grid(row=0, column=0, sticky="nsew")
|
||||||
|
sbo = ttk.Scrollbar(frm_out, orient="vertical", command=self.txt.yview); sbo.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.txt.configure(yscrollcommand=sbo.set)
|
||||||
|
|
||||||
|
# --- helpers ---
|
||||||
|
def _append(self, s):
|
||||||
|
self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||||||
|
|
||||||
|
def _stamp(self):
|
||||||
|
import datetime as _dt
|
||||||
|
return _dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
def _build_outdir(self, supervised: bool) -> Path:
|
||||||
|
out_root = self.state.analyze_out_root()
|
||||||
|
label = (self.out_label.get().strip() or ("rangefit" if supervised else "unsupervised"))
|
||||||
|
stamp = f"{self._stamp()}_{label}" if self.use_ts.get() else label
|
||||||
|
outdir = out_root / stamp
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._last_outdir = outdir
|
||||||
|
return outdir
|
||||||
|
|
||||||
|
def _selected_trace(self):
|
||||||
|
sel = self.trace_panel.get_selected()
|
||||||
|
if not sel:
|
||||||
|
messagebox.showwarning("Hinweis", "Bitte genau eine .trace-Datei auswählen.")
|
||||||
|
return None
|
||||||
|
if len(sel) != 1:
|
||||||
|
messagebox.showwarning("Hinweis", "Range-Fit benötigt genau eine .trace-Datei (Single-Select).")
|
||||||
|
return None
|
||||||
|
return sel[0]
|
||||||
|
|
||||||
|
def _maybe(self, val: str, flag: str, args: list):
|
||||||
|
v = (val or "").strip()
|
||||||
|
if v != "":
|
||||||
|
args += [flag, v]
|
||||||
|
|
||||||
|
def _open_path(self, p: Path):
|
||||||
|
try:
|
||||||
|
if sys.platform.startswith("darwin"):
|
||||||
|
subprocess.Popen(["open", str(p)])
|
||||||
|
elif os.name == "nt":
|
||||||
|
os.startfile(str(p)) # type: ignore
|
||||||
|
else:
|
||||||
|
subprocess.Popen(["xdg-open", str(p)])
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showwarning("Fehler", f"Konnte nicht öffnen:\n{p}\n{e}")
|
||||||
|
|
||||||
|
def _open_last_outdir(self):
|
||||||
|
if self._last_outdir and self._last_outdir.exists():
|
||||||
|
self._open_path(self._last_outdir)
|
||||||
|
else:
|
||||||
|
messagebox.showinfo("Hinweis", "Noch kein Output-Ordner vorhanden.")
|
||||||
|
|
||||||
|
def _open_last_report(self):
|
||||||
|
if not (self._last_outdir and self._last_outdir.exists()):
|
||||||
|
messagebox.showinfo("Hinweis", "Noch kein Report erzeugt.")
|
||||||
|
return
|
||||||
|
# versuche ein *_report.md im letzten Outdir zu finden
|
||||||
|
md = list(Path(self._last_outdir).glob("*_report.md"))
|
||||||
|
if not md:
|
||||||
|
messagebox.showinfo("Hinweis", "Kein Report gefunden.")
|
||||||
|
return
|
||||||
|
self._open_path(md[0])
|
||||||
|
|
||||||
|
def _on_run(self):
|
||||||
|
trace = self._selected_trace()
|
||||||
|
if not trace:
|
||||||
|
return
|
||||||
|
|
||||||
|
# supervised?
|
||||||
|
rmin = self.rmin.get().strip()
|
||||||
|
rmax = self.rmax.get().strip()
|
||||||
|
supervised = bool(rmin) and bool(rmax)
|
||||||
|
|
||||||
|
outdir = self._build_outdir(supervised)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
str(Path(__file__).parent / RANGE_FITTER),
|
||||||
|
trace,
|
||||||
|
"--outdir", str(outdir),
|
||||||
|
"--plots-top", str(self.plots_top.get()),
|
||||||
|
]
|
||||||
|
if self.rx_only.get():
|
||||||
|
cmd.append("--rx-only")
|
||||||
|
|
||||||
|
if supervised:
|
||||||
|
cmd += ["--rmin", rmin, "--rmax", rmax, "--min-hit", str(self.min_hit.get())]
|
||||||
|
if self.allow_neg.get():
|
||||||
|
cmd.append("--allow-neg-scale")
|
||||||
|
self._maybe(self.rate_min.get(), "--rate-min", cmd)
|
||||||
|
self._maybe(self.rate_max.get(), "--rate-max", cmd)
|
||||||
|
self._maybe(self.jitter_max.get(), "--jitter-max-ms", cmd)
|
||||||
|
self._maybe(self.slope_abs.get(), "--max-slope-abs", cmd)
|
||||||
|
self._maybe(self.slope_frac.get(), "--max-slope-frac", cmd)
|
||||||
|
cmd += ["--slope-quantile", str(self.slope_q.get())]
|
||||||
|
self._maybe(self.min_uniq.get(), "--min-uniq-ratio", cmd)
|
||||||
|
else:
|
||||||
|
# unsupervised
|
||||||
|
cmd += ["--min-smooth", str(self.min_smooth.get())]
|
||||||
|
self._maybe(self.max_slope_frac_raw.get(), "--max-slope-frac-raw", cmd)
|
||||||
|
cmd += ["--slope-quantile", str(self.slope_q.get())] # wird intern für p95/p99 gewählt
|
||||||
|
|
||||||
|
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||||||
|
t = threading.Thread(target=self._run_cmd, args=(cmd,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _run_cmd(self, cmd):
|
||||||
|
try:
|
||||||
|
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||||||
|
for line in proc.stdout:
|
||||||
|
self._append(line)
|
||||||
|
rc = proc.wait()
|
||||||
|
if rc != 0:
|
||||||
|
self._append(f"[Exit-Code {rc}]\n")
|
||||||
|
else:
|
||||||
|
self._append(f"\nDone. Output: {self._last_outdir}\n")
|
||||||
|
except Exception as e:
|
||||||
|
self._append(f"[Fehler] {e}\n")
|
||||||
|
|
||||||
|
# ---------------- App Shell ----------------
|
||||||
|
class App(tk.Tk):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.title("CAN Universal Signal Finder – GUI")
|
||||||
|
self.geometry("1180x860")
|
||||||
|
self.configure(padx=8, pady=8)
|
||||||
|
|
||||||
|
# shared state
|
||||||
|
self.state = AppState()
|
||||||
|
|
||||||
|
# header (always visible)
|
||||||
|
self.header = Header(self, self.state)
|
||||||
|
self.header.pack(fill="x", side="top")
|
||||||
|
|
||||||
|
# Tabs
|
||||||
|
nb = ttk.Notebook(self)
|
||||||
|
nb.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
self.tab_analyze = TabAnalyze(nb, self.state, self.header)
|
||||||
|
self.tab_explorer = TabExplorer(nb, self.state, self.header)
|
||||||
|
self.tab_batch = TabTraceBatch(nb, self.state, self.header)
|
||||||
|
self.tab_rangefit = TabRangeFit(nb, self.state, self.header)
|
||||||
|
|
||||||
|
nb.add(self.tab_analyze, text="Multi-Log Analyse")
|
||||||
|
nb.add(self.tab_explorer, text="ID Explorer")
|
||||||
|
nb.add(self.tab_batch, text="Traces Batch-Analyse")
|
||||||
|
nb.add(self.tab_rangefit, text="Range-Fit")
|
||||||
|
|
||||||
|
# init: traces auf default/latest stellen
|
||||||
|
self.state.set_traces_to_default_or_latest()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = App()
|
||||||
|
app.mainloop()
|
18
Reverse-Engineering CAN-Bus/models/.gitignore
vendored
Normal file
18
Reverse-Engineering CAN-Bus/models/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Ignoriere in JEDEM unmittelbaren Unterordner von models/
|
||||||
|
# die Verzeichnisse "traces" und "analyze_out"
|
||||||
|
*/traces/
|
||||||
|
*/traces/**
|
||||||
|
*/analyze_out/
|
||||||
|
*/analyze_out/**
|
||||||
|
|
||||||
|
# Falls jemand versehentlich direkt unter models/ solche Ordner anlegt, auch ignorieren:
|
||||||
|
traces/
|
||||||
|
traces/**
|
||||||
|
analyze_out/
|
||||||
|
analyze_out/**
|
||||||
|
|
||||||
|
# (Optional, falls du dich mal vertippst)
|
||||||
|
*/analyze.out/
|
||||||
|
*/analyze.out/**
|
||||||
|
analyze.out/
|
||||||
|
analyze.out/**
|
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
possible CAN Ids !? (from Forum somwhere)
|
||||||
|
Message ID:
|
||||||
|
|
||||||
|
0x540 - byte 0 - bits 6...4 - Gear Position - 0 = N, 1-6 = gears 1-6
|
||||||
|
bit 1 - Neutral Light - 1 = on, 0 = off
|
||||||
|
bit 2 - Check engine light????
|
||||||
|
0x550 - byte 0 - bits 2...0 - Coolant bars on dashboard
|
||||||
|
- bit 3 - Warning light - 1 = on, 0 = off
|
||||||
|
0x570 - bytes 2-3 - Coolant temp - (256 * byte 3 + byte 2) / 10 = Temp in Degrees C
|
||||||
|
0x518 - Possible revs - divide by 4
|
||||||
|
0x519 - Similar to 0x518 Possibly TPS unsure. Doesn't actuate when only tps is rotated.
|
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"workdir": "models/Triumph Speed Twin 1200 RS (2025)",
|
||||||
|
"logs_dir": "logs",
|
||||||
|
"traces_dir": "traces",
|
||||||
|
"analyze_out_base": "analyze_out",
|
||||||
|
"timestamp_runs": true,
|
||||||
|
"available_logs": [
|
||||||
|
"models/Triumph Speed Twin 1200 RS (2025)/logs/cantrace-raw-2025-08-27T17-45-27-980Z-1.log"
|
||||||
|
],
|
||||||
|
"selected_indices": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
3
Reverse-Engineering CAN-Bus/requirements.txt
Normal file
3
Reverse-Engineering CAN-Bus/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pandas>=2.0.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
matplotlib>=3.7.0
|
18
Reverse-Engineering CAN-Bus/start.sh
Executable file
18
Reverse-Engineering CAN-Bus/start.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Choose python (allow override with $PYTHON)
|
||||||
|
PYTHON_BIN="${PYTHON:-python3}"
|
||||||
|
VENV_DIR=".venv"
|
||||||
|
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
exec python main.py "$@"
|
186
Reverse-Engineering CAN-Bus/trace_batch_analyzer.py
Normal file
186
Reverse-Engineering CAN-Bus/trace_batch_analyzer.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
LOG_PATTERN = re.compile(r"(\d+)\s+(TX|RX)\s+0x([0-9A-Fa-f]+)\s+\d+\s+((?:[0-9A-Fa-f]{2}\s+)+)")
|
||||||
|
|
||||||
|
def parse_trace(path: Path, rx_only=False) -> pd.DataFrame:
|
||||||
|
rows = []
|
||||||
|
with open(path, "r", errors="ignore") as f:
|
||||||
|
for line in f:
|
||||||
|
m = LOG_PATTERN.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
ts = int(m.group(1))
|
||||||
|
dr = m.group(2)
|
||||||
|
if rx_only and dr != "RX":
|
||||||
|
continue
|
||||||
|
cid = int(m.group(3), 16)
|
||||||
|
data = [int(x, 16) for x in m.group(4).split() if x.strip()]
|
||||||
|
rows.append((ts, dr, cid, data))
|
||||||
|
df = pd.DataFrame(rows, columns=["ts","dir","id","data"])
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
df["time_s"] = (df["ts"] - df["ts"].min())/1000.0
|
||||||
|
return df
|
||||||
|
|
||||||
|
def be16(a,b): return (a<<8)|b
|
||||||
|
def le16(a,b): return a | (b<<8)
|
||||||
|
|
||||||
|
def analyze_one_trace(df: pd.DataFrame, scale=1.0, offs=0.0, rmin=None, rmax=None):
|
||||||
|
"""Return stats for all 8-bit bytes and all adjacent 16-bit pairs (LE/BE)."""
|
||||||
|
stats = []
|
||||||
|
# 8-bit
|
||||||
|
for i in range(8):
|
||||||
|
vals = [d[i] for d in df["data"] if len(d)>i]
|
||||||
|
if not vals: continue
|
||||||
|
arr = np.array(vals, dtype=float)
|
||||||
|
phys = arr*scale + offs
|
||||||
|
hit = np.ones_like(phys, dtype=bool)
|
||||||
|
if rmin is not None: hit &= (phys>=rmin)
|
||||||
|
if rmax is not None: hit &= (phys<=rmax)
|
||||||
|
stats.append({
|
||||||
|
"type":"byte8","slot":str(i),
|
||||||
|
"n":len(arr),
|
||||||
|
"min":float(arr.min()),"max":float(arr.max()),"var":float(arr.var()),
|
||||||
|
"hit_ratio": float(np.count_nonzero(hit))/len(hit) if len(hit)>0 else 0.0,
|
||||||
|
"min_phys": float(phys.min()), "max_phys": float(phys.max())
|
||||||
|
})
|
||||||
|
# 16-bit
|
||||||
|
pairs = [(i,i+1) for i in range(7)]
|
||||||
|
for i,j in pairs:
|
||||||
|
# LE
|
||||||
|
vals = [le16(d[i],d[j]) for d in df["data"] if len(d)>j]
|
||||||
|
if vals:
|
||||||
|
arr = np.array(vals, dtype=float); phys = arr*scale + offs
|
||||||
|
hit = np.ones_like(phys, dtype=bool)
|
||||||
|
if rmin is not None: hit &= (phys>=rmin)
|
||||||
|
if rmax is not None: hit &= (phys<=rmax)
|
||||||
|
stats.append({
|
||||||
|
"type":"le16","slot":f"{i}-{j}",
|
||||||
|
"n":len(arr),
|
||||||
|
"min":float(arr.min()),"max":float(arr.max()),"var":float(arr.var()),
|
||||||
|
"hit_ratio": float(np.count_nonzero(hit))/len(hit) if len(hit)>0 else 0.0,
|
||||||
|
"min_phys": float(phys.min()), "max_phys": float(phys.max())
|
||||||
|
})
|
||||||
|
# BE
|
||||||
|
vals = [be16(d[i],d[j]) for d in df["data"] if len(d)>j]
|
||||||
|
if vals:
|
||||||
|
arr = np.array(vals, dtype=float); phys = arr*scale + offs
|
||||||
|
hit = np.ones_like(phys, dtype=bool)
|
||||||
|
if rmin is not None: hit &= (phys>=rmin)
|
||||||
|
if rmax is not None: hit &= (phys<=rmax)
|
||||||
|
stats.append({
|
||||||
|
"type":"be16","slot":f"{i}-{j}",
|
||||||
|
"n":len(arr),
|
||||||
|
"min":float(arr.min()),"max":float(arr.max()),"var":float(arr.var()),
|
||||||
|
"hit_ratio": float(np.count_nonzero(hit))/len(hit) if len(hit)>0 else 0.0,
|
||||||
|
"min_phys": float(phys.min()), "max_phys": float(phys.max())
|
||||||
|
})
|
||||||
|
return pd.DataFrame(stats)
|
||||||
|
|
||||||
|
def plot_one_trace(df: pd.DataFrame, outdir: Path, prefix: str):
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
# 8-bit plots
|
||||||
|
for i in range(8):
|
||||||
|
times, series = [], []
|
||||||
|
for t,d in zip(df["time_s"], df["data"]):
|
||||||
|
if len(d)>i:
|
||||||
|
times.append(t); series.append(d[i])
|
||||||
|
if not series: continue
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.figure(figsize=(10,4))
|
||||||
|
plt.plot(times, series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)"); plt.ylabel(f"Byte[{i}] (8-bit)")
|
||||||
|
plt.title(f"{prefix} – 8-bit Byte {i}")
|
||||||
|
plt.grid(True); plt.tight_layout()
|
||||||
|
plt.savefig(outdir / f"{prefix}_byte{i}.png", dpi=150); plt.close()
|
||||||
|
# 16-bit plots (LE/BE)
|
||||||
|
pairs = [(i,i+1) for i in range(7)]
|
||||||
|
for i,j in pairs:
|
||||||
|
times, series = [], []
|
||||||
|
for t,d in zip(df["time_s"], df["data"]):
|
||||||
|
if len(d)>j: times.append(t); series.append(le16(d[i],d[j]))
|
||||||
|
if series:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.figure(figsize=(10,4))
|
||||||
|
plt.plot(times, series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)"); plt.ylabel(f"LE16 @{i}-{j}")
|
||||||
|
plt.title(f"{prefix} – LE16 {i}-{j}")
|
||||||
|
plt.grid(True); plt.tight_layout()
|
||||||
|
plt.savefig(outdir / f"{prefix}_le16_{i}-{j}.png", dpi=150); plt.close()
|
||||||
|
times, series = [], []
|
||||||
|
for t,d in zip(df["time_s"], df["data"]):
|
||||||
|
if len(d)>j: times.append(t); series.append(be16(d[i],d[j]))
|
||||||
|
if series:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.figure(figsize=(10,4))
|
||||||
|
plt.plot(times, series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)"); plt.ylabel(f"BE16 @{i}-{j}")
|
||||||
|
plt.title(f"{prefix} – BE16 {i}-{j}")
|
||||||
|
plt.grid(True); plt.tight_layout()
|
||||||
|
plt.savefig(outdir / f"{prefix}_be16_{i}-{j}.png", dpi=150); plt.close()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Batch analyze per-ID traces and rank 8/16-bit combinations")
|
||||||
|
ap.add_argument("--traces-dir", required=True, help="Directory containing *.trace files")
|
||||||
|
ap.add_argument("--outdir", required=True, help="Output directory for analysis results")
|
||||||
|
ap.add_argument("--rx-only", action="store_true", help="Use RX frames only")
|
||||||
|
ap.add_argument("--plots", action="store_true", help="Also generate plots for each trace")
|
||||||
|
ap.add_argument("--scale", type=float, default=1.0, help="phys = raw*scale + offset")
|
||||||
|
ap.add_argument("--offset", type=float, default=0.0, help="phys = raw*scale + offset")
|
||||||
|
ap.add_argument("--range-min", type=float, default=None, help="physical min (after scale/offset)")
|
||||||
|
ap.add_argument("--range-max", type=float, default=None, help="physical max (after scale/offset)")
|
||||||
|
ap.add_argument("--top", type=int, default=8, help="Export top combos per trace to summary")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
tdir = Path(args.traces_dir)
|
||||||
|
outdir = Path(args.outdir); outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
traces = sorted([p for p in tdir.glob("*.trace")])
|
||||||
|
if not traces:
|
||||||
|
print("No .trace files found.", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
global_rows = []
|
||||||
|
for tr in traces:
|
||||||
|
df = parse_trace(tr, rx_only=args.rx_only)
|
||||||
|
if df.empty:
|
||||||
|
continue
|
||||||
|
stats = analyze_one_trace(df, args.scale, args.offset, args.range_min, args.range_max)
|
||||||
|
# Ranking: primarily by hit_ratio (if range given), else by variance; break ties by var then n
|
||||||
|
if args.range_min is not None or args.range_max is not None:
|
||||||
|
stats = stats.sort_values(["hit_ratio","var","n"], ascending=[False, False, False])
|
||||||
|
else:
|
||||||
|
stats = stats.sort_values(["var","n"], ascending=[False, False])
|
||||||
|
# write per-trace csv
|
||||||
|
per_csv = outdir / f"{tr.stem}_combostats.csv"
|
||||||
|
stats.to_csv(per_csv, index=False)
|
||||||
|
|
||||||
|
# append top rows with trace id hint
|
||||||
|
stem = tr.stem # e.g., 0x208_log1
|
||||||
|
for _, row in stats.head(args.top).iterrows():
|
||||||
|
r = row.to_dict()
|
||||||
|
r["trace"] = stem
|
||||||
|
global_rows.append(r)
|
||||||
|
|
||||||
|
# plots (optional) into a subdir per trace
|
||||||
|
if args.plots:
|
||||||
|
plot_dir = outdir / f"{tr.stem}_plots"
|
||||||
|
plot_one_trace(df, plot_dir, prefix=tr.stem)
|
||||||
|
|
||||||
|
# global summary
|
||||||
|
if global_rows:
|
||||||
|
gdf = pd.DataFrame(global_rows)
|
||||||
|
gdf.to_csv(outdir / "summary_top_combinations.csv", index=False)
|
||||||
|
print(f"Global summary written: {outdir/'summary_top_combinations.csv'}")
|
||||||
|
|
||||||
|
print(f"Processed {len(traces)} trace files. Results at: {outdir}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
660
Reverse-Engineering CAN-Bus/trace_signal_fitter.py
Normal file
660
Reverse-Engineering CAN-Bus/trace_signal_fitter.py
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
trace_signal_fitter.py – Advanced Range-/Unsupervised-Fit mit Physik-Constraints & Bericht
|
||||||
|
|
||||||
|
Modi:
|
||||||
|
1) Range-Fit (supervised): --rmin/--rmax gesetzt → finde scale & offset, maximiere Hit-Ratio in [rmin, rmax].
|
||||||
|
2) Unsupervised: ohne Range → plausible Rohsignale nach Smoothness/Var/Rate/Span.
|
||||||
|
|
||||||
|
Neu:
|
||||||
|
- Periodizität: Rate (Hz), Jitter (std der Inter-Arrival-Times), CV.
|
||||||
|
- Slew-Rate: p95/p99 von |Δ|/s (supervised in phys-Einheit, unsupervised normiert auf Roh-Span).
|
||||||
|
- Grenzwerte als Argumente (--rate-min/max, --jitter-max-ms, --max-slope-abs, --max-slope-frac, ...).
|
||||||
|
- Zusätzlich signed 16-bit Varianten (le16s/be16s).
|
||||||
|
- JSON + Markdown-Bericht pro Trace mit PASS/FAIL und Begründungen.
|
||||||
|
|
||||||
|
Logformat (Kettenöler):
|
||||||
|
<timestamp_ms> <TX|RX> 0x<ID_HEX> <DLC> <byte0> <byte1> ... <byte7>
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- supervised: <trace>_encoding_candidates.csv, Plots, <trace>_report.md, <trace>_report.json
|
||||||
|
- unsupervised: <trace>_unsupervised_candidates.csv, Plots, <trace>_report.md, <trace>_report.json
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Dict, Iterable
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Parsing ----------
|
||||||
|
|
||||||
|
def parse_trace(path: Path, rx_only: bool = False) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Robustes Parsen des Kettenöler-Formats:
|
||||||
|
<ts_ms> <TX|RX> 0x<ID> <DLC> <b0> <b1> ... (hex)
|
||||||
|
"""
|
||||||
|
rows = []
|
||||||
|
with open(path, "r", errors="ignore") as f:
|
||||||
|
for line in f:
|
||||||
|
parts = line.strip().split()
|
||||||
|
if len(parts) < 4:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ts = int(parts[0])
|
||||||
|
dr = parts[1]
|
||||||
|
if rx_only and dr != "RX":
|
||||||
|
continue
|
||||||
|
cid = int(parts[2], 16) if parts[2].lower().startswith("0x") else int(parts[2], 16)
|
||||||
|
dlc = int(parts[3])
|
||||||
|
bytes_hex = parts[4:4+dlc] if dlc > 0 else []
|
||||||
|
data = []
|
||||||
|
for b in bytes_hex:
|
||||||
|
try:
|
||||||
|
data.append(int(b, 16))
|
||||||
|
except Exception:
|
||||||
|
data.append(0)
|
||||||
|
rows.append((ts, dr, cid, data))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
df = pd.DataFrame(rows, columns=["ts", "dir", "id", "data"])
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
df["time_s"] = (df["ts"] - df["ts"].min()) / 1000.0
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Helpers ----------
|
||||||
|
|
||||||
|
def be16(a: int, b: int) -> int: return (a << 8) | b
|
||||||
|
def le16(a: int, b: int) -> int: return a | (b << 8)
|
||||||
|
def s16(u: int) -> int: return u if u < 0x8000 else u - 0x10000
|
||||||
|
|
||||||
|
def p_quant_abs_diff(arr: np.ndarray, q: float) -> float:
|
||||||
|
if arr.size < 2:
|
||||||
|
return 0.0
|
||||||
|
d = np.abs(np.diff(arr))
|
||||||
|
return float(np.percentile(d, q * 100))
|
||||||
|
|
||||||
|
def p_quant(arr: np.ndarray, q: float) -> float:
|
||||||
|
if arr.size == 0:
|
||||||
|
return 0.0
|
||||||
|
return float(np.percentile(arr, q * 100))
|
||||||
|
|
||||||
|
def interarrival_metrics(times: np.ndarray) -> Dict[str, float]:
|
||||||
|
if times.size < 2:
|
||||||
|
return {"rate_hz": 0.0, "period_mean": 0.0, "period_std": 0.0, "jitter_cv": 0.0, "n": int(times.size)}
|
||||||
|
dt = np.diff(times)
|
||||||
|
period_mean = float(np.mean(dt))
|
||||||
|
period_std = float(np.std(dt))
|
||||||
|
rate_hz = 1.0 / period_mean if period_mean > 0 else 0.0
|
||||||
|
jitter_cv = (period_std / period_mean) if period_mean > 0 else 0.0
|
||||||
|
return {"rate_hz": rate_hz, "period_mean": period_mean, "period_std": period_std, "jitter_cv": jitter_cv, "n": int(times.size)}
|
||||||
|
|
||||||
|
def slope_metrics(values: np.ndarray, times: np.ndarray) -> Dict[str, float]:
|
||||||
|
if values.size < 2:
|
||||||
|
return {"slope_p95": 0.0, "slope_p99": 0.0, "jerk_p95": 0.0}
|
||||||
|
dv = np.abs(np.diff(values))
|
||||||
|
dt = np.diff(times)
|
||||||
|
# vermeide Division durch 0
|
||||||
|
dt = np.where(dt <= 0, np.nan, dt)
|
||||||
|
slope = dv / dt
|
||||||
|
slope = slope[~np.isnan(slope)]
|
||||||
|
if slope.size == 0:
|
||||||
|
return {"slope_p95": 0.0, "slope_p99": 0.0, "jerk_p95": 0.0}
|
||||||
|
jerk = np.abs(np.diff(slope))
|
||||||
|
return {
|
||||||
|
"slope_p95": float(np.percentile(slope, 95)),
|
||||||
|
"slope_p99": float(np.percentile(slope, 99)),
|
||||||
|
"jerk_p95": float(np.percentile(jerk, 95)) if jerk.size > 0 else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prefilter(vals: np.ndarray) -> Tuple[bool, Dict[str, float]]:
|
||||||
|
if vals.size < 12:
|
||||||
|
return False, {"reason": "too_few_samples"}
|
||||||
|
uniq = np.unique(vals)
|
||||||
|
if uniq.size <= 2:
|
||||||
|
return False, {"reason": "too_constant"}
|
||||||
|
p95 = p_quant_abs_diff(vals, 0.95)
|
||||||
|
if p95 == 0:
|
||||||
|
return False, {"reason": "no_changes"}
|
||||||
|
r = float(np.percentile(vals, 97) - np.percentile(vals, 3) + 1e-9)
|
||||||
|
if p95 > 0.5 * r:
|
||||||
|
return False, {"reason": "too_jumpi"}
|
||||||
|
return True, {"p95_abs_diff": p95, "span_est": r}
|
||||||
|
|
||||||
|
def try_scaleset() -> List[float]:
|
||||||
|
base = [
|
||||||
|
1e-3, 2e-3, 5e-3,
|
||||||
|
1e-2, 2e-2, 5e-2,
|
||||||
|
0.05, 0.0625, 0.1, 0.125, 0.2, 0.25, 0.5,
|
||||||
|
0.75, 0.8, 1.0, 1.25, 2.0, 5.0, 10.0
|
||||||
|
]
|
||||||
|
return sorted(set(base))
|
||||||
|
|
||||||
|
def interval_best_offset(raw: np.ndarray, scale: float, rmin: float, rmax: float) -> Tuple[float, float]:
|
||||||
|
"""
|
||||||
|
Finde das Offset, das die meisten Werte (scale*raw + offset) in [rmin, rmax] bringt.
|
||||||
|
Sweep über Intervallgrenzen (klassische "interval stabbing" Lösung).
|
||||||
|
"""
|
||||||
|
a = rmin - scale * raw
|
||||||
|
b = rmax - scale * raw
|
||||||
|
lo = np.minimum(a, b)
|
||||||
|
hi = np.maximum(a, b)
|
||||||
|
events = []
|
||||||
|
for L, H in zip(lo, hi):
|
||||||
|
events.append((L, +1))
|
||||||
|
events.append((H, -1))
|
||||||
|
events.sort(key=lambda t: (t[0], -t[1]))
|
||||||
|
best = -1
|
||||||
|
cur = 0
|
||||||
|
best_x = None
|
||||||
|
for x, v in events:
|
||||||
|
cur += v
|
||||||
|
if cur > best:
|
||||||
|
best = cur
|
||||||
|
best_x = x
|
||||||
|
hit_ratio = float(best) / float(len(raw)) if len(raw) else 0.0
|
||||||
|
return float(best_x if best_x is not None else 0.0), hit_ratio
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Candidate Generation ----------
|
||||||
|
|
||||||
|
def gen_candidates(df: pd.DataFrame) -> Iterable[Tuple[str, np.ndarray, np.ndarray]]:
|
||||||
|
"""
|
||||||
|
Liefert (label, values, times) für:
|
||||||
|
- 8-bit Bytes D0..D7
|
||||||
|
- 16-bit adjazente Paare (LE/BE) + signed Varianten
|
||||||
|
Times wird auf die gefilterten Indizes gemappt (DLC-abhängig).
|
||||||
|
"""
|
||||||
|
times_all = df["time_s"].to_numpy(dtype=float)
|
||||||
|
data = df["data"].tolist()
|
||||||
|
|
||||||
|
# 8-bit
|
||||||
|
for i in range(8):
|
||||||
|
idx = [k for k, d in enumerate(data) if len(d) > i]
|
||||||
|
if len(idx) < 3:
|
||||||
|
continue
|
||||||
|
vals = np.array([data[k][i] for k in idx], dtype=float)
|
||||||
|
t = times_all[idx]
|
||||||
|
yield f"byte[{i}]", vals, t
|
||||||
|
|
||||||
|
# 16-bit adjazent
|
||||||
|
for i in range(7):
|
||||||
|
j = i + 1
|
||||||
|
idx = [k for k, d in enumerate(data) if len(d) > j]
|
||||||
|
if len(idx) < 3:
|
||||||
|
continue
|
||||||
|
a = [data[k][i] for k in idx]
|
||||||
|
b = [data[k][j] for k in idx]
|
||||||
|
u_le = np.array([le16(x, y) for x, y in zip(a, b)], dtype=float)
|
||||||
|
u_be = np.array([be16(x, y) for x, y in zip(a, b)], dtype=float)
|
||||||
|
s_le = np.array([s16(le16(x, y)) for x, y in zip(a, b)], dtype=float)
|
||||||
|
s_be = np.array([s16(be16(x, y)) for x, y in zip(a, b)], dtype=float)
|
||||||
|
t = times_all[idx]
|
||||||
|
yield f"le16[{i}-{j}]", u_le, t
|
||||||
|
yield f"be16[{i}-{j}]", u_be, t
|
||||||
|
yield f"le16s[{i}-{j}]", s_le, t
|
||||||
|
yield f"be16s[{i}-{j}]", s_be, t
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Evaluation ----------
|
||||||
|
|
||||||
|
def evaluate_supervised(label: str,
|
||||||
|
vals: np.ndarray,
|
||||||
|
times: np.ndarray,
|
||||||
|
rmin: float,
|
||||||
|
rmax: float,
|
||||||
|
allow_neg_scale: bool,
|
||||||
|
constraints: Dict[str, float]) -> Dict[str, float] | None:
|
||||||
|
ok, meta = prefilter(vals)
|
||||||
|
if not ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
scales = try_scaleset()
|
||||||
|
if allow_neg_scale:
|
||||||
|
scales += [-s for s in scales if s > 0]
|
||||||
|
|
||||||
|
best = {"hit_ratio": -1.0, "scale": None, "offset": 0.0}
|
||||||
|
for s in scales:
|
||||||
|
o, hr = interval_best_offset(vals, s, rmin, rmax)
|
||||||
|
if hr > best["hit_ratio"]:
|
||||||
|
best = {"scale": s, "offset": float(o), "hit_ratio": hr}
|
||||||
|
|
||||||
|
phys = vals * best["scale"] + best["offset"]
|
||||||
|
within = (phys >= rmin) & (phys <= rmax)
|
||||||
|
in_count = int(np.count_nonzero(within))
|
||||||
|
|
||||||
|
p95_raw = p_quant_abs_diff(vals, 0.95)
|
||||||
|
p95_phys = p_quant_abs_diff(phys, 0.95)
|
||||||
|
|
||||||
|
ia = interarrival_metrics(times[:len(vals)])
|
||||||
|
sm = slope_metrics(phys, times[:len(phys)])
|
||||||
|
|
||||||
|
prange = (rmax - rmin) if (rmax > rmin) else 1.0
|
||||||
|
slope_p95_frac = sm["slope_p95"] / prange
|
||||||
|
slope_p99_frac = sm["slope_p99"] / prange
|
||||||
|
|
||||||
|
failures = []
|
||||||
|
|
||||||
|
if constraints.get("rate_min") is not None and ia["rate_hz"] < constraints["rate_min"] - 1e-9:
|
||||||
|
failures.append(f"rate {ia['rate_hz']:.2f}Hz < min {constraints['rate_min']:.2f}Hz")
|
||||||
|
if constraints.get("rate_max") is not None and ia["rate_hz"] > constraints["rate_max"] + 1e-9:
|
||||||
|
failures.append(f"rate {ia['rate_hz']:.2f}Hz > max {constraints['rate_max']:.2f}Hz")
|
||||||
|
|
||||||
|
if constraints.get("jitter_max_ms") is not None:
|
||||||
|
jitter_ms = ia["period_std"] * 1000.0
|
||||||
|
if jitter_ms > constraints["jitter_max_ms"] + 1e-9:
|
||||||
|
failures.append(f"jitter {jitter_ms:.1f}ms > max {constraints['jitter_max_ms']:.1f}ms")
|
||||||
|
|
||||||
|
def _resolve_abs_slope_limit():
|
||||||
|
if constraints.get("max_slope_abs") is not None:
|
||||||
|
return constraints["max_slope_abs"]
|
||||||
|
if constraints.get("max_slope_frac") is not None:
|
||||||
|
return constraints["max_slope_frac"] * prange
|
||||||
|
return None
|
||||||
|
|
||||||
|
max_s_abs = _resolve_abs_slope_limit()
|
||||||
|
if max_s_abs is not None:
|
||||||
|
q = constraints.get("slope_quantile", 0.95)
|
||||||
|
qv = sm["slope_p95"] if q <= 0.95 else sm["slope_p99"]
|
||||||
|
if qv > max_s_abs + 1e-9:
|
||||||
|
failures.append(f"slope(q={q:.2f}) {qv:.3g} > max {max_s_abs:.3g}")
|
||||||
|
|
||||||
|
uniq_ratio = len(np.unique(vals)) / float(len(vals))
|
||||||
|
if constraints.get("min_uniq_ratio") is not None and uniq_ratio < constraints["min_uniq_ratio"] - 1e-9:
|
||||||
|
failures.append(f"uniq_ratio {uniq_ratio:.3f} < min {constraints['min_uniq_ratio']:.3f}")
|
||||||
|
|
||||||
|
passed = (len(failures) == 0)
|
||||||
|
|
||||||
|
# Quality Score
|
||||||
|
score = best["hit_ratio"]
|
||||||
|
if max_s_abs is not None and max_s_abs > 0:
|
||||||
|
slope_pen = min(sm["slope_p95"] / max_s_abs, 1.0)
|
||||||
|
score *= (1.0 - 0.3 * slope_pen)
|
||||||
|
if constraints.get("jitter_max_ms") is not None:
|
||||||
|
jitter_ms = ia["period_std"] * 1000.0
|
||||||
|
jitter_pen = min(jitter_ms / constraints["jitter_max_ms"], 1.0)
|
||||||
|
score *= (1.0 - 0.2 * jitter_pen)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"mode": "range_fit",
|
||||||
|
"n": int(vals.size),
|
||||||
|
"raw_min": float(np.min(vals)),
|
||||||
|
"raw_max": float(np.max(vals)),
|
||||||
|
"raw_var": float(np.var(vals)),
|
||||||
|
"p95_absdiff_raw": float(p95_raw),
|
||||||
|
"scale": float(best["scale"]),
|
||||||
|
"offset": float(best["offset"]),
|
||||||
|
"hit_ratio": float(best["hit_ratio"]),
|
||||||
|
"in_count": in_count,
|
||||||
|
"phys_min": float(np.min(phys)),
|
||||||
|
"phys_max": float(np.max(phys)),
|
||||||
|
"p95_absdiff_phys": float(p95_phys),
|
||||||
|
"span_phys": float(np.percentile(phys, 97) - np.percentile(phys, 3)),
|
||||||
|
"rate_hz_est": float(ia["rate_hz"]),
|
||||||
|
"period_std_ms": float(ia["period_std"] * 1000.0),
|
||||||
|
"jitter_cv": float(ia["jitter_cv"]),
|
||||||
|
"slope_p95_per_s": float(sm["slope_p95"]),
|
||||||
|
"slope_p99_per_s": float(sm["slope_p99"]),
|
||||||
|
"slope_p95_frac": float(slope_p95_frac),
|
||||||
|
"slope_p99_frac": float(slope_p99_frac),
|
||||||
|
"uniq_ratio": float(uniq_ratio),
|
||||||
|
"passed": bool(passed),
|
||||||
|
"fail_reasons": "; ".join(failures),
|
||||||
|
"quality_score": float(score),
|
||||||
|
}
|
||||||
|
|
||||||
|
def evaluate_unsupervised(label: str,
|
||||||
|
vals: np.ndarray,
|
||||||
|
times: np.ndarray,
|
||||||
|
min_smooth: float = 0.2,
|
||||||
|
max_slope_frac_raw: float | None = None,
|
||||||
|
slope_quantile: float = 0.95) -> Dict[str, float] | None:
|
||||||
|
if vals.size < 12:
|
||||||
|
return None
|
||||||
|
p95 = p_quant_abs_diff(vals, 0.95)
|
||||||
|
span = float(np.percentile(vals, 97) - np.percentile(vals, 3) + 1e-9)
|
||||||
|
smooth = 1.0 - min(max(p95 / span, 0.0), 1.0)
|
||||||
|
uniq_ratio = float(len(np.unique(vals))) / float(vals.size)
|
||||||
|
var = float(np.var(vals))
|
||||||
|
|
||||||
|
ia = interarrival_metrics(times[:len(vals)])
|
||||||
|
sm = slope_metrics(vals, times[:len(vals)])
|
||||||
|
slope_q = sm["slope_p95"] if slope_quantile <= 0.95 else sm["slope_p99"]
|
||||||
|
slope_frac_raw = (slope_q / span) if span > 0 else 0.0
|
||||||
|
|
||||||
|
if uniq_ratio <= 0.02:
|
||||||
|
return None
|
||||||
|
if smooth < min_smooth:
|
||||||
|
return None
|
||||||
|
if (max_slope_frac_raw is not None) and (slope_frac_raw > max_slope_frac_raw):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"mode": "unsupervised",
|
||||||
|
"n": int(vals.size),
|
||||||
|
"raw_min": float(np.min(vals)),
|
||||||
|
"raw_max": float(np.max(vals)),
|
||||||
|
"raw_var": var,
|
||||||
|
"span_raw": span,
|
||||||
|
"p95_absdiff_raw": float(p95),
|
||||||
|
"smoothness": float(smooth),
|
||||||
|
"uniq_ratio": float(uniq_ratio),
|
||||||
|
"rate_hz_est": float(ia["rate_hz"]),
|
||||||
|
"period_std_ms": float(ia["period_std"] * 1000.0),
|
||||||
|
"jitter_cv": float(ia["jitter_cv"]),
|
||||||
|
"slope_q_raw": float(slope_q),
|
||||||
|
"slope_frac_raw": float(slope_frac_raw),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Plot & Report ----------
|
||||||
|
|
||||||
|
def plot_timeseries(times: np.ndarray, series: np.ndarray, out_png: Path, title: str, ylabel: str) -> None:
|
||||||
|
plt.figure(figsize=(10, 4))
|
||||||
|
plt.plot(times[:len(series)], series, marker=".", linestyle="-")
|
||||||
|
plt.xlabel("Zeit (s)")
|
||||||
|
plt.ylabel(ylabel)
|
||||||
|
plt.title(title)
|
||||||
|
plt.grid(True)
|
||||||
|
plt.tight_layout()
|
||||||
|
out_png.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
plt.savefig(out_png, dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
def df_to_md_table(df: pd.DataFrame) -> str:
|
||||||
|
"""Robustes Markdown-Table: nutzt to_markdown falls vorhanden, sonst CSV in Codeblock."""
|
||||||
|
try:
|
||||||
|
return df.to_markdown(index=False) # benötigt evtl. 'tabulate'
|
||||||
|
except Exception:
|
||||||
|
return "```\n" + df.to_csv(index=False) + "```"
|
||||||
|
|
||||||
|
def write_report_md(path: Path, header: dict, top_rows: pd.DataFrame, failures: pd.DataFrame, mode: str, links: dict) -> None:
|
||||||
|
md = []
|
||||||
|
md.append(f"# Trace Report – {header.get('trace_name','')}")
|
||||||
|
md.append("")
|
||||||
|
md.append(f"- **Mode:** {mode}")
|
||||||
|
for k, v in header.items():
|
||||||
|
if k in ("trace_name",):
|
||||||
|
continue
|
||||||
|
md.append(f"- **{k}**: {v}")
|
||||||
|
md.append("")
|
||||||
|
|
||||||
|
if mode == "range_fit":
|
||||||
|
md.append("## Top-Kandidaten (Range-Fit)")
|
||||||
|
md.append("Hit-Ratio, Slope/Jitter & Score – beste zuerst.\n")
|
||||||
|
if top_rows is not None and not top_rows.empty:
|
||||||
|
md.append(df_to_md_table(top_rows))
|
||||||
|
else:
|
||||||
|
md.append("_Keine Kandidaten über Schwelle._")
|
||||||
|
md.append("")
|
||||||
|
if failures is not None and not failures.empty:
|
||||||
|
md.append("## Ausgeschlossene Kandidaten (Gründe)\n")
|
||||||
|
md.append(df_to_md_table(failures[["label", "fail_reasons"]]))
|
||||||
|
else:
|
||||||
|
md.append("## Top-Kandidaten (Unsupervised)\n")
|
||||||
|
if top_rows is not None and not top_rows.empty:
|
||||||
|
md.append(df_to_md_table(top_rows))
|
||||||
|
else:
|
||||||
|
md.append("_Keine plausiblen Rohsignale._")
|
||||||
|
|
||||||
|
md.append("\n## Artefakte")
|
||||||
|
for k, v in links.items():
|
||||||
|
md.append(f"- **{k}**: `{v}`")
|
||||||
|
path.write_text("\n".join(md), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Main ----------
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Range-/Unsupervised-Fit mit physikbasierten Constraints + Bericht")
|
||||||
|
ap.add_argument("trace", help="Pfad zur .trace Datei")
|
||||||
|
|
||||||
|
# supervision
|
||||||
|
ap.add_argument("--rmin", type=float, default=None)
|
||||||
|
ap.add_argument("--rmax", type=float, default=None)
|
||||||
|
ap.add_argument("--allow-neg-scale", action="store_true")
|
||||||
|
|
||||||
|
# shared
|
||||||
|
ap.add_argument("--rx-only", action="store_true")
|
||||||
|
ap.add_argument("--outdir", default=".")
|
||||||
|
ap.add_argument("--plots-top", type=int, default=8)
|
||||||
|
|
||||||
|
# supervised thresholds
|
||||||
|
ap.add_argument("--min-hit", type=float, default=0.5)
|
||||||
|
ap.add_argument("--rate-min", type=float, default=None)
|
||||||
|
ap.add_argument("--rate-max", type=float, default=None)
|
||||||
|
ap.add_argument("--jitter-max-ms", type=float, default=None)
|
||||||
|
ap.add_argument("--max-slope-abs", type=float, default=None, help="Max |Δphys|/s (z. B. °C/s, km/h/s)")
|
||||||
|
ap.add_argument("--max-slope-frac", type=float, default=None, help="Max |Δphys|/s relativ zu (rmax-rmin)")
|
||||||
|
ap.add_argument("--slope-quantile", type=float, default=0.95, help="0.95 oder 0.99")
|
||||||
|
ap.add_argument("--min-uniq-ratio", type=float, default=None)
|
||||||
|
|
||||||
|
# unsupervised thresholds
|
||||||
|
ap.add_argument("--min-smooth", type=float, default=0.2)
|
||||||
|
ap.add_argument("--max-slope-frac-raw", type=float, default=None, help="roh: (|Δraw|/s)/Span")
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
trace = Path(args.trace)
|
||||||
|
df = parse_trace(trace, rx_only=args.rx_only)
|
||||||
|
if df.empty:
|
||||||
|
print("Keine Daten in Trace.", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
supervised = (args.rmin is not None) and (args.rmax is not None)
|
||||||
|
outdir = Path(args.outdir)
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if supervised:
|
||||||
|
constraints = {
|
||||||
|
"rate_min": args.rate_min,
|
||||||
|
"rate_max": args.rate_max,
|
||||||
|
"jitter_max_ms": args.jitter_max_ms,
|
||||||
|
"max_slope_abs": args.max_slope_abs,
|
||||||
|
"max_slope_frac": args.max_slope_frac,
|
||||||
|
"slope_quantile": args.slope_quantile,
|
||||||
|
"min_uniq_ratio": args.min_uniq_ratio,
|
||||||
|
}
|
||||||
|
results = []
|
||||||
|
rejected = []
|
||||||
|
for label, series, times in gen_candidates(df):
|
||||||
|
r = evaluate_supervised(label, series, times, args.rmin, args.rmax, args.allow_neg_scale, constraints)
|
||||||
|
if r is None:
|
||||||
|
continue
|
||||||
|
if r["hit_ratio"] >= args.min_hit:
|
||||||
|
(results if r["passed"] else rejected).append({**r, "trace": trace.stem})
|
||||||
|
|
||||||
|
if not results and not rejected:
|
||||||
|
print("Keine Kandidaten über Schwelle gefunden.", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
df_ok = pd.DataFrame(results).sort_values(
|
||||||
|
["quality_score", "hit_ratio", "p95_absdiff_phys", "rate_hz_est", "n"],
|
||||||
|
ascending=[False, False, True, False, False]
|
||||||
|
)
|
||||||
|
df_rej = pd.DataFrame(rejected)
|
||||||
|
|
||||||
|
csv_path = outdir / f"{trace.stem}_encoding_candidates.csv"
|
||||||
|
if not df_ok.empty:
|
||||||
|
df_ok.to_csv(csv_path, index=False)
|
||||||
|
print(f"Kandidaten-CSV: {csv_path}")
|
||||||
|
|
||||||
|
# Plots für Top-Kandidaten (oder Rejected, falls keine OK)
|
||||||
|
top_for_plots = df_ok if not df_ok.empty else df_rej
|
||||||
|
data = df["data"].tolist()
|
||||||
|
times_all = df["time_s"].to_numpy(dtype=float)
|
||||||
|
|
||||||
|
def reconstruct_vals(label: str) -> np.ndarray | None:
|
||||||
|
if label.startswith("byte["):
|
||||||
|
i = int(label.split("[")[1].split("]")[0])
|
||||||
|
idx = [k for k, d in enumerate(data) if len(d) > i]
|
||||||
|
if not idx: return None
|
||||||
|
return np.array([data[k][i] for k in idx], dtype=float), times_all[idx]
|
||||||
|
elif label.startswith(("le16", "be16", "le16s", "be16s")):
|
||||||
|
signed = label.startswith(("le16s", "be16s"))
|
||||||
|
i, j = map(int, label.split("[")[1].split("]")[0].split("-"))
|
||||||
|
idx = [k for k, d in enumerate(data) if len(d) > j]
|
||||||
|
if not idx: return None
|
||||||
|
a = [data[k][i] for k in idx]
|
||||||
|
b = [data[k][j] for k in idx]
|
||||||
|
if label.startswith("le16"):
|
||||||
|
v = [le16(x, y) for x, y in zip(a, b)]
|
||||||
|
else:
|
||||||
|
v = [be16(x, y) for x, y in zip(a, b)]
|
||||||
|
if signed:
|
||||||
|
v = [s16(int(x)) for x in v]
|
||||||
|
return np.array(v, dtype=float), times_all[idx]
|
||||||
|
return None
|
||||||
|
|
||||||
|
for _, row in top_for_plots.head(max(1, args.plots_top)).iterrows():
|
||||||
|
rec = reconstruct_vals(row["label"])
|
||||||
|
if rec is None:
|
||||||
|
continue
|
||||||
|
vals, tt = rec
|
||||||
|
phys = vals * row["scale"] + row["offset"]
|
||||||
|
out_png = outdir / f"{trace.stem}_{row['label'].replace('[','_').replace(']','')}.png"
|
||||||
|
plot_timeseries(tt[:len(phys)], phys, out_png,
|
||||||
|
f"{trace.name} – {row['label']} (scale={row['scale']:.6g}, offset={row['offset']:.6g})",
|
||||||
|
"phys (geschätzt)")
|
||||||
|
|
||||||
|
# Bericht
|
||||||
|
hdr = {
|
||||||
|
"trace_name": trace.name,
|
||||||
|
"mode": "range_fit",
|
||||||
|
"rmin": args.rmin,
|
||||||
|
"rmax": args.rmax,
|
||||||
|
"min_hit": args.min_hit,
|
||||||
|
"rate_min": args.rate_min,
|
||||||
|
"rate_max": args.rate_max,
|
||||||
|
"jitter_max_ms": args.jitter_max_ms,
|
||||||
|
"max_slope_abs": args.max_slope_abs,
|
||||||
|
"max_slope_frac": args.max_slope_frac,
|
||||||
|
"slope_quantile": args.slope_quantile,
|
||||||
|
}
|
||||||
|
top_view = df_ok.head(12)[
|
||||||
|
["label", "quality_score", "hit_ratio", "scale", "offset",
|
||||||
|
"rate_hz_est", "period_std_ms", "slope_p95_per_s", "slope_p99_per_s",
|
||||||
|
"p95_absdiff_phys", "uniq_ratio"]
|
||||||
|
] if not df_ok.empty else pd.DataFrame()
|
||||||
|
fail_view = df_rej[["label", "fail_reasons"]] if not df_rej.empty else pd.DataFrame()
|
||||||
|
|
||||||
|
md_path = outdir / f"{trace.stem}_report.md"
|
||||||
|
json_path = outdir / f"{trace.stem}_report.json"
|
||||||
|
write_report_md(md_path, hdr, top_view, fail_view, "range_fit",
|
||||||
|
{"candidates_csv": str(csv_path) if not df_ok.empty else "(leer)"})
|
||||||
|
with open(json_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({
|
||||||
|
"header": hdr,
|
||||||
|
"accepted": df_ok.to_dict(orient="records"),
|
||||||
|
"rejected": df_rej.to_dict(orient="records"),
|
||||||
|
}, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"Report: {md_path}")
|
||||||
|
print(f"Report JSON: {json_path}")
|
||||||
|
|
||||||
|
if not df_ok.empty:
|
||||||
|
print("\nTop-Kandidaten:")
|
||||||
|
cols = ["label", "quality_score", "hit_ratio", "scale", "offset",
|
||||||
|
"rate_hz_est", "period_std_ms", "slope_p95_per_s", "slope_p99_per_s"]
|
||||||
|
print(df_ok.head(10)[cols].to_string(index=False))
|
||||||
|
else:
|
||||||
|
print("\nKeine Kandidaten PASS; siehe Gründe in report.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Unsupervised
|
||||||
|
results = []
|
||||||
|
for label, series, times in gen_candidates(df):
|
||||||
|
r = evaluate_unsupervised(label, series, times,
|
||||||
|
min_smooth=args.min_smooth,
|
||||||
|
max_slope_frac_raw=args.max_slope_frac_raw,
|
||||||
|
slope_quantile=args.slope_quantile)
|
||||||
|
if r is None:
|
||||||
|
continue
|
||||||
|
r["trace"] = trace.stem
|
||||||
|
results.append(r)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
print("Keine plausiblen Rohsignale gefunden. Tipp: --min-smooth senken.", file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
df_res = pd.DataFrame(results).sort_values(
|
||||||
|
["smoothness", "span_raw", "raw_var", "rate_hz_est", "n"],
|
||||||
|
ascending=[False, False, False, False, False]
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_path = outdir / f"{trace.stem}_unsupervised_candidates.csv"
|
||||||
|
df_res.to_csv(csv_path, index=False)
|
||||||
|
print(f"Unsupervised-CSV: {csv_path}")
|
||||||
|
|
||||||
|
# Plots der Top-N (Rohwerte)
|
||||||
|
data = df["data"].tolist()
|
||||||
|
times_all = df["time_s"].to_numpy(dtype=float)
|
||||||
|
|
||||||
|
def reconstruct_raw(label: str) -> Tuple[np.ndarray, np.ndarray] | None:
|
||||||
|
if label.startswith("byte["):
|
||||||
|
i = int(label.split("[")[1].split("]")[0])
|
||||||
|
idx = [k for k, d in enumerate(data) if len(d) > i]
|
||||||
|
if not idx: return None
|
||||||
|
return np.array([data[k][i] for k in idx], dtype=float), times_all[idx]
|
||||||
|
elif label.startswith(("le16", "be16", "le16s", "be16s")):
|
||||||
|
signed = label.startswith(("le16s", "be16s"))
|
||||||
|
i, j = map(int, label.split("[")[1].split("]")[0].split("-"))
|
||||||
|
idx = [k for k, d in enumerate(data) if len(d) > j]
|
||||||
|
if not idx: return None
|
||||||
|
a = [data[k][i] for k in idx]
|
||||||
|
b = [data[k][j] for k in idx]
|
||||||
|
if label.startswith("le16"):
|
||||||
|
v = [le16(x, y) for x, y in zip(a, b)]
|
||||||
|
else:
|
||||||
|
v = [be16(x, y) for x, y in zip(a, b)]
|
||||||
|
if signed:
|
||||||
|
v = [s16(int(x)) for x in v]
|
||||||
|
return np.array(v, dtype=float), times_all[idx]
|
||||||
|
return None
|
||||||
|
|
||||||
|
for _, row in df_res.head(max(1, args.plots_top)).iterrows():
|
||||||
|
rec = reconstruct_raw(row["label"])
|
||||||
|
if rec is None:
|
||||||
|
continue
|
||||||
|
vals, tt = rec
|
||||||
|
out_png = outdir / f"{trace.stem}_{row['label'].replace('[','_').replace(']','')}_raw.png"
|
||||||
|
plot_timeseries(tt[:len(vals)], vals, out_png,
|
||||||
|
f"{trace.name} – {row['label']} (raw)", "raw")
|
||||||
|
|
||||||
|
# Bericht
|
||||||
|
hdr = {
|
||||||
|
"trace_name": trace.name,
|
||||||
|
"mode": "unsupervised",
|
||||||
|
"min_smooth": args.min_smooth,
|
||||||
|
"max_slope_frac_raw": args.max_slope_frac_raw,
|
||||||
|
}
|
||||||
|
top_view = df_res.head(12)[
|
||||||
|
["label", "smoothness", "span_raw", "raw_var",
|
||||||
|
"rate_hz_est", "period_std_ms", "slope_frac_raw", "uniq_ratio"]
|
||||||
|
]
|
||||||
|
md_path = outdir / f"{trace.stem}_report.md"
|
||||||
|
json_path = outdir / f"{trace.stem}_report.json"
|
||||||
|
write_report_md(md_path, hdr, top_view, pd.DataFrame(), "unsupervised",
|
||||||
|
{"candidates_csv": str(csv_path)})
|
||||||
|
with open(json_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({
|
||||||
|
"header": hdr,
|
||||||
|
"accepted": df_res.to_dict(orient="records"),
|
||||||
|
}, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"Report: {md_path}")
|
||||||
|
print(f"Report JSON: {json_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@@ -1,8 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
revision = (
|
|
||||||
subprocess.check_output(["git", "rev-parse", "--short=10", "HEAD"])
|
|
||||||
.strip()
|
|
||||||
.decode("utf-8")
|
|
||||||
)
|
|
||||||
print("-DGIT_REV='\"%s\"'" % revision)
|
|
@@ -1,9 +1,87 @@
|
|||||||
Import("env") # pylint: disable=undefined-variable
|
# run_pre.py — PlatformIO pre-build script
|
||||||
env.Execute("\"$PYTHONEXE\" -m pip install jinja2")
|
import os
|
||||||
env.Replace(PROGNAME="firmware_pcb_1.%s.fw" % env.GetProjectOption("custom_pcb_revision"))
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
Import("env") # provided by PlatformIO
|
||||||
|
|
||||||
|
# ---- helper ----
|
||||||
|
def parse_ver(s: str):
|
||||||
|
"""
|
||||||
|
Accepts '1.04', '1.4', '1,04' etc. -> returns (major:int, minor:int, norm_str:'1.04')
|
||||||
|
"""
|
||||||
|
s = (s or "").strip().replace(",", ".")
|
||||||
|
if not s:
|
||||||
|
return 0, 0, "0.00"
|
||||||
|
parts = s.split(".")
|
||||||
|
try:
|
||||||
|
major = int(parts[0])
|
||||||
|
minor = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
except ValueError:
|
||||||
|
major, minor = 0, 0
|
||||||
|
norm_str = f"{major}.{minor:02d}"
|
||||||
|
return major, minor, norm_str
|
||||||
|
|
||||||
|
def read_text_file(p: Path):
|
||||||
|
try:
|
||||||
|
return p.read_text(encoding="utf-8").strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def git_short_hash():
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["git", "rev-parse", "--short", "HEAD"],
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
).decode("utf-8").strip()
|
||||||
|
return out or "nogit"
|
||||||
|
except Exception:
|
||||||
|
return "nogit"
|
||||||
|
|
||||||
|
# ---- ensure jinja present, like before ----
|
||||||
|
env.Execute("\"$PYTHONEXE\" -m pip install jinja2")
|
||||||
|
|
||||||
|
# ---- keep your other pre-steps ----
|
||||||
import struct2json
|
import struct2json
|
||||||
import dtcs
|
import dtcs
|
||||||
|
|
||||||
struct2json.struct2json()
|
struct2json.struct2json()
|
||||||
dtcs.build_dtcs()
|
dtcs.build_dtcs()
|
||||||
|
|
||||||
|
# ---- collect inputs ----
|
||||||
|
proj_dir = Path(env["PROJECT_DIR"])
|
||||||
|
|
||||||
|
# user options from platformio.ini
|
||||||
|
pcb_rev = env.GetProjectOption("custom_pcb_revision", default="")
|
||||||
|
fw_ver_opt = env.GetProjectOption("custom_firmware_version", default="") # new
|
||||||
|
|
||||||
|
# required flash version from data/version
|
||||||
|
req_file = proj_dir / "data" / "version"
|
||||||
|
req_ver_raw = read_text_file(req_file)
|
||||||
|
|
||||||
|
fw_major, fw_minor, fw_norm = parse_ver(fw_ver_opt)
|
||||||
|
req_major, req_minor, req_norm = parse_ver(req_ver_raw)
|
||||||
|
|
||||||
|
githash = git_short_hash()
|
||||||
|
|
||||||
|
# ---- export as preprocessor defines ----
|
||||||
|
# numeric defines
|
||||||
|
env.Append(CPPDEFINES=[
|
||||||
|
("FW_VERSION_MAJOR", fw_major),
|
||||||
|
("FW_VERSION_MINOR", fw_minor),
|
||||||
|
("REQ_FLASH_MAJOR", req_major),
|
||||||
|
("REQ_FLASH_MINOR", req_minor),
|
||||||
|
])
|
||||||
|
|
||||||
|
# useful string defines (if du sie im Code/Logging brauchst)
|
||||||
|
env.Append(CPPDEFINES=[
|
||||||
|
("FW_VERSION_STR", f"\"{fw_norm}\""),
|
||||||
|
("REQ_FLASH_STR", f"\"{req_norm}\""),
|
||||||
|
("GIT_REV", f"\"{githash}\""),
|
||||||
|
])
|
||||||
|
|
||||||
|
# ---- build artifact name ----
|
||||||
|
# bisher: firmware_pcb_1.<pcb>.fw
|
||||||
|
# jetzt: firmware_pcb_<pcb>_v<fw>_<git>.fw (gut identifizierbar)
|
||||||
|
pcb_part = f"{pcb_rev}".strip() or "X"
|
||||||
|
fname = f"firmware_pcb_{pcb_part}_v{fw_norm}_{githash}.fw"
|
||||||
|
env.Replace(PROGNAME=fname)
|
||||||
|
@@ -191,6 +191,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<h4>CAN / OBD2 Trace</h4>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-center mb-2">
|
||||||
|
<!-- Beide Start-Buttons senden btn-trace-start; Modus kommt als value -->
|
||||||
|
<button id="trace-start" data-wsid="trace-start" value="raw" class="btn-wsevent btn btn-outline-primary">
|
||||||
|
Start CAN-Trace
|
||||||
|
</button>
|
||||||
|
<button id="trace-start-obd" data-wsid="trace-start" value="obd" class="btn-wsevent btn btn-outline-primary ml-2">
|
||||||
|
Start OBD-Trace
|
||||||
|
</button>
|
||||||
|
<button id="trace-stop" class="btn-wsevent btn btn-outline-danger ml-2">
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="trace-out" class="form-control" style="font-family:monospace" rows="8" readonly spellcheck="false"></textarea>
|
||||||
|
<small id="trace-status" class="form-text text-muted">Trace inaktiv</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
<!-- Div Group LiveDebug -->
|
<!-- Div Group LiveDebug -->
|
||||||
<!-- Div Group Device Reboot -->
|
<!-- Div Group Device Reboot -->
|
||||||
<hr />
|
<hr />
|
||||||
|
@@ -23,4 +23,4 @@ document
|
|||||||
var fileName = document.getElementById("fw-update-file").files[0].name;
|
var fileName = document.getElementById("fw-update-file").files[0].name;
|
||||||
var nextSibling = e.target.nextElementSibling;
|
var nextSibling = e.target.nextElementSibling;
|
||||||
nextSibling.innerText = fileName;
|
nextSibling.innerText = fileName;
|
||||||
});
|
});
|
@@ -5,6 +5,106 @@ var statusMapping;
|
|||||||
var staticMapping;
|
var staticMapping;
|
||||||
var overlay;
|
var overlay;
|
||||||
|
|
||||||
|
let traceActive = false;
|
||||||
|
let traceMode = null;
|
||||||
|
let traceFileName = "";
|
||||||
|
let traceUseFsAccess = false;
|
||||||
|
|
||||||
|
// File-System-Access-Stream (Chromium)
|
||||||
|
let traceWriter = null;
|
||||||
|
let traceEncoder = null;
|
||||||
|
let traceWriteQueue = Promise.resolve(); // für geordnete Writes
|
||||||
|
|
||||||
|
// Fallback: In-Memory-Sammeln (für Blob-Download bei STOP)
|
||||||
|
let traceMemParts = [];
|
||||||
|
|
||||||
|
// Textarea & Status
|
||||||
|
const TRACE_MAX_CHARS = 200000; // ~200 KB für die Anzeige
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
function nowIsoCompact() {
|
||||||
|
return new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
}
|
||||||
|
function genTraceFileName(mode) {
|
||||||
|
return `cantrace-${mode}-${nowIsoCompact()}.log`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTraceUI(active, mode, infoText) {
|
||||||
|
traceActive = !!active;
|
||||||
|
traceMode = active ? mode : null;
|
||||||
|
|
||||||
|
const btnRaw = $("trace-start");
|
||||||
|
const btnObd = $("trace-start-obd");
|
||||||
|
const btnStop = $("trace-stop");
|
||||||
|
const status = $("trace-status");
|
||||||
|
|
||||||
|
if (btnRaw) btnRaw.disabled = active;
|
||||||
|
if (btnObd) btnObd.disabled = active;
|
||||||
|
if (btnStop) btnStop.disabled = !active;
|
||||||
|
|
||||||
|
if (status)
|
||||||
|
status.textContent =
|
||||||
|
infoText || (active ? `Trace aktiv (${mode})` : "Trace inaktiv");
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceClear() {
|
||||||
|
const out = $("trace-out");
|
||||||
|
if (out) out.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function traceAppend(text) {
|
||||||
|
const out = $("trace-out");
|
||||||
|
if (!out || !text) return;
|
||||||
|
out.value += text;
|
||||||
|
if (out.value.length > TRACE_MAX_CHARS) {
|
||||||
|
out.value = out.value.slice(-TRACE_MAX_CHARS);
|
||||||
|
}
|
||||||
|
out.scrollTop = out.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerBlobDownload(filename, blob) {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKv(s) {
|
||||||
|
const out = Object.create(null);
|
||||||
|
s.split(";").forEach((part) => {
|
||||||
|
const eq = part.indexOf("=");
|
||||||
|
if (eq > 0) {
|
||||||
|
const k = part.slice(0, eq).trim();
|
||||||
|
const v = part.slice(eq + 1).trim();
|
||||||
|
if (k) out[k] = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// geordnete Writes auf File System Access Writer
|
||||||
|
function writeToFs(chunk) {
|
||||||
|
if (!traceUseFsAccess || !traceWriter) return;
|
||||||
|
const data = traceEncoder
|
||||||
|
? traceEncoder.encode(chunk)
|
||||||
|
: new TextEncoder().encode(chunk);
|
||||||
|
traceWriteQueue = traceWriteQueue
|
||||||
|
.then(() => traceWriter.write(data))
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Ihr JavaScript-Code hier, einschließlich der onLoad-Funktion
|
// Ihr JavaScript-Code hier, einschließlich der onLoad-Funktion
|
||||||
overlay = document.getElementById("overlay");
|
overlay = document.getElementById("overlay");
|
||||||
@@ -45,16 +145,32 @@ function initSettingInputs() {
|
|||||||
|
|
||||||
function onOpen(event) {
|
function onOpen(event) {
|
||||||
console.log("Connection opened");
|
console.log("Connection opened");
|
||||||
|
setTraceUI(false, null, "Verbunden – Trace inaktiv");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClose(event) {
|
function onClose(event) {
|
||||||
console.log("Connection closed");
|
console.log("Connection closed");
|
||||||
setTimeout(initWebSocket, 1000);
|
setTimeout(initWebSocket, 1000);
|
||||||
overlay.style.display = "flex";
|
overlay.style.display = "flex";
|
||||||
|
|
||||||
|
// Falls Trace noch aktiv war: lokal finalisieren
|
||||||
|
if (traceActive) {
|
||||||
|
const note = "Trace beendet (Verbindung getrennt)";
|
||||||
|
if (traceUseFsAccess && traceWriter) {
|
||||||
|
traceWriteQueue.then(() => traceWriter.close()).catch(console.error);
|
||||||
|
traceWriter = null;
|
||||||
|
} else if (traceMemParts.length) {
|
||||||
|
const blob = new Blob(traceMemParts, { type: "text/plain" });
|
||||||
|
triggerBlobDownload(traceFileName || "cantrace.log", blob);
|
||||||
|
traceMemParts = [];
|
||||||
|
}
|
||||||
|
setTraceUI(false, null, note);
|
||||||
|
showNotification(note, "warning");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendButton(event) {
|
async function sendButton(event) {
|
||||||
var targetElement = event.target;
|
const targetElement = event.target;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
targetElement.classList.contains("confirm") &&
|
targetElement.classList.contains("confirm") &&
|
||||||
@@ -62,7 +178,46 @@ function sendButton(event) {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
websocket_sendevent("btn-" + targetElement.id, targetElement.value);
|
const wsid = targetElement.dataset.wsid || targetElement.id; // z.B. "trace-start"
|
||||||
|
const val = targetElement.value || "";
|
||||||
|
|
||||||
|
// File-Ziel *vor* dem WS-Start öffnen (nur bei trace-start; wegen User-Gesture!)
|
||||||
|
if (wsid === "trace-start") {
|
||||||
|
const mode = val || "raw";
|
||||||
|
traceFileName = genTraceFileName(mode);
|
||||||
|
|
||||||
|
// Anzeige schon mal leeren
|
||||||
|
traceClear();
|
||||||
|
setTraceUI(false, null, "Trace wird gestartet…");
|
||||||
|
|
||||||
|
traceUseFsAccess = false;
|
||||||
|
traceWriter = null;
|
||||||
|
traceEncoder = null;
|
||||||
|
traceWriteQueue = Promise.resolve();
|
||||||
|
traceMemParts = []; // Fallback-Puffer leeren
|
||||||
|
|
||||||
|
if (window.showSaveFilePicker) {
|
||||||
|
try {
|
||||||
|
const fh = await showSaveFilePicker({
|
||||||
|
suggestedName: traceFileName,
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
description: "Text Log",
|
||||||
|
accept: { "text/plain": [".log", ".txt"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
traceWriter = await fh.createWritable();
|
||||||
|
traceEncoder = new TextEncoder();
|
||||||
|
traceUseFsAccess = true;
|
||||||
|
} catch (e) {
|
||||||
|
// Nutzer hat evtl. abgebrochen → Fallback in RAM
|
||||||
|
traceUseFsAccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket_sendevent("btn-" + wsid, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessage(event) {
|
function onMessage(event) {
|
||||||
@@ -101,6 +256,58 @@ function onMessage(event) {
|
|||||||
fillValuesToHTML(result);
|
fillValuesToHTML(result);
|
||||||
overlay.style.display = "none";
|
overlay.style.display = "none";
|
||||||
}
|
}
|
||||||
|
// --- Trace: Start ---
|
||||||
|
else if (data.startsWith("STARTTRACE;")) {
|
||||||
|
const kv = parseKv(data.slice(11)); // mode=..., ts=...
|
||||||
|
const mode = kv.mode || "?";
|
||||||
|
setTraceUI(true, mode, `Trace gestartet (${mode})`);
|
||||||
|
|
||||||
|
// Fallback: wenn kein FS-Access → in RAM sammeln
|
||||||
|
// (sonst haben wir traceWriter bereits im Klick vorbereitet)
|
||||||
|
}
|
||||||
|
// --- Trace: Lines (ggf. mehrere in einer WS-Nachricht) ---
|
||||||
|
else if (data.startsWith("TRACELINE;")) {
|
||||||
|
const payload = data.replace(/TRACELINE;/g, ""); // reiner Text inkl. '\n'
|
||||||
|
traceAppend(payload);
|
||||||
|
if (traceUseFsAccess && traceWriter) {
|
||||||
|
writeToFs(payload);
|
||||||
|
} else {
|
||||||
|
traceMemParts.push(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Trace: Stop/Summary ---
|
||||||
|
else if (data.startsWith("STOPTRACE;")) {
|
||||||
|
const kv = parseKv(data.slice(10));
|
||||||
|
const msg = `Trace beendet (${kv.mode || "?"}), Zeilen=${
|
||||||
|
kv.lines || "0"
|
||||||
|
}, Drops=${kv.drops || "0"}${kv.reason ? ", Grund=" + kv.reason : ""}`;
|
||||||
|
|
||||||
|
// Datei finalisieren
|
||||||
|
if (traceUseFsAccess && traceWriter) {
|
||||||
|
traceWriteQueue.then(() => traceWriter.close()).catch(console.error);
|
||||||
|
traceWriter = null;
|
||||||
|
} else if (traceMemParts.length) {
|
||||||
|
const blob = new Blob(traceMemParts, { type: "text/plain" });
|
||||||
|
triggerBlobDownload(traceFileName || "cantrace.log", blob);
|
||||||
|
traceMemParts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setTraceUI(false, null, msg);
|
||||||
|
showNotification(msg, "info");
|
||||||
|
}
|
||||||
|
// --- Busy/Fehler/Ack ---
|
||||||
|
else if (data.startsWith("TRACEBUSY;")) {
|
||||||
|
const kv = parseKv(data.slice(10));
|
||||||
|
const owner = kv.owner ? " (Owner #" + kv.owner + ")" : "";
|
||||||
|
showNotification("Trace bereits aktiv" + owner, "warning");
|
||||||
|
} else if (data.startsWith("TRACEERROR;")) {
|
||||||
|
const kv = parseKv(data.slice(11));
|
||||||
|
showNotification("Trace-Fehler: " + (kv.msg || "unbekannt"), "danger");
|
||||||
|
} else if (data.startsWith("TRACEACK;")) {
|
||||||
|
// optional
|
||||||
|
const kv = parseKv(data.slice(9));
|
||||||
|
console.log("TRACEACK", kv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMapping(mappingString) {
|
function createMapping(mappingString) {
|
||||||
|
@@ -1 +1 @@
|
|||||||
1.04
|
1.05
|
@@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file can.h
|
|
||||||
*
|
|
||||||
* @brief Header file for Controller Area Network (CAN) functionality in the ChainLube application.
|
|
||||||
*
|
|
||||||
* This file provides functions and structures related to Controller Area Network (CAN)
|
|
||||||
* communication for the ChainLube project. It includes functions for initializing CAN,
|
|
||||||
* processing CAN messages, and retrieving wheel speed from CAN data.
|
|
||||||
*
|
|
||||||
* @author Marcel Peterkau
|
|
||||||
* @date 09.01.2024
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _CAN_H_
|
|
||||||
#define _CAN_H_
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <mcp_can.h>
|
|
||||||
#include <SPI.h>
|
|
||||||
#include "common.h"
|
|
||||||
#include "globals.h"
|
|
||||||
#include "dtc.h"
|
|
||||||
#include "debugger.h"
|
|
||||||
|
|
||||||
// CAN frame structure definition
|
|
||||||
struct can_frame
|
|
||||||
{
|
|
||||||
unsigned long can_id;
|
|
||||||
uint8_t can_dlc;
|
|
||||||
uint8_t data[8] __attribute__((aligned(8)));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function prototypes
|
|
||||||
void Init_CAN();
|
|
||||||
uint32_t Process_CAN_WheelSpeed();
|
|
||||||
|
|
||||||
#endif
|
|
78
Software/include/can_hal.h
Normal file
78
Software/include/can_hal.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <mcp_can.h>
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
// ==== Board-Pin ====
|
||||||
|
// Falls nicht bereits global definiert:
|
||||||
|
#ifndef GPIO_CS_CAN
|
||||||
|
#define GPIO_CS_CAN 5
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ==== Öffentliche, einzige Instanz ====
|
||||||
|
extern MCP_CAN CAN0;
|
||||||
|
|
||||||
|
// ==== Init-Config ====
|
||||||
|
struct CanHalConfig {
|
||||||
|
uint8_t baud = CAN_500KBPS; // laut Lib
|
||||||
|
uint8_t clock = MCP_16MHZ; // 8/16 MHz je nach Quarz
|
||||||
|
uint16_t listenOnlyProbeMs = 0; // optionaler kurzer Hörtest
|
||||||
|
uint16_t modeSettleMs = 10; // Wartezeit für Mode-Set (Retry-Fenster)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==== Universeller Filter-Descriptor ====
|
||||||
|
struct CanFilter {
|
||||||
|
uint32_t id; // 11-bit oder 29-bit Roh-ID
|
||||||
|
bool ext; // false = STD(11-bit), true = EXT(29-bit)
|
||||||
|
};
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Trace / Logging Types
|
||||||
|
// =====================
|
||||||
|
struct CanLogFrame {
|
||||||
|
uint32_t ts_ms;
|
||||||
|
uint32_t id;
|
||||||
|
bool ext;
|
||||||
|
bool rx; // true = RX, false = TX
|
||||||
|
uint8_t dlc;
|
||||||
|
uint8_t data[8];
|
||||||
|
};
|
||||||
|
|
||||||
|
using CanTraceSink = void (*)(const CanLogFrame& f);
|
||||||
|
|
||||||
|
// ==== API ====
|
||||||
|
|
||||||
|
// 1) Einmalige Hardware-Initialisierung + integrierter Loopback-Selftest.
|
||||||
|
// - begin()
|
||||||
|
// - LOOPBACK senden/echo prüfen (ohne Bus)
|
||||||
|
// - optional: ListenOnly-Probe (nur Heuristik)
|
||||||
|
// - Default: Filter/Masks weit offen, NORMAL-Mode
|
||||||
|
// Rückgabe: true = bereit; false = Fehler (kein CAN verwenden)
|
||||||
|
bool CAN_HAL_Init(const CanHalConfig& cfg);
|
||||||
|
|
||||||
|
// Ist die HAL bereit (nach Init)?
|
||||||
|
bool CAN_HAL_IsReady();
|
||||||
|
|
||||||
|
// Bestätigter Moduswechsel (CONFIG/NORMAL/LISTENONLY/LOOPBACK)
|
||||||
|
// true = erfolgreich; setzt bei Misserfolg DTC_CAN_TRANSCEIVER_FAILED
|
||||||
|
bool CAN_HAL_SetMode(uint8_t mode);
|
||||||
|
|
||||||
|
// Masken/Filter
|
||||||
|
bool CAN_HAL_SetMask(uint8_t bank, bool ext, uint32_t rawMask);
|
||||||
|
bool CAN_HAL_SetStdMask11(uint8_t bank, uint16_t mask11);
|
||||||
|
void CAN_HAL_ClearFilters();
|
||||||
|
bool CAN_HAL_AddFilter(const CanFilter& f);
|
||||||
|
bool CAN_HAL_SetFilters(const CanFilter* list, size_t count);
|
||||||
|
|
||||||
|
// Non-blocking IO
|
||||||
|
bool CAN_HAL_Read(unsigned long& id, uint8_t& len, uint8_t data[8]); // true = Frame gelesen
|
||||||
|
uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t* data); // CAN_OK bei Erfolg
|
||||||
|
|
||||||
|
// Diagnose/Utilities
|
||||||
|
uint8_t CAN_HAL_GetErrorFlags(); // Intern: getError()
|
||||||
|
void CAN_HAL_GetErrorCounters(uint8_t& tec, uint8_t& rec); // TX/RX Error Counter
|
||||||
|
|
||||||
|
// Trace / Sniffer
|
||||||
|
void CAN_HAL_SetTraceSink(CanTraceSink sink);
|
||||||
|
void CAN_HAL_EnableRawSniffer(bool enable);
|
||||||
|
bool CAN_HAL_IsRawSnifferEnabled();
|
11
Software/include/can_native.h
Normal file
11
Software/include/can_native.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "can_hal.h"
|
||||||
|
|
||||||
|
// Initialisiert den Native-CAN-Profilpfad (setzt Masken/Filter und NORMAL-Mode).
|
||||||
|
// Voraussetzung: CAN_HAL_Init(...) hat zuvor true geliefert.
|
||||||
|
bool Init_CAN_Native();
|
||||||
|
|
||||||
|
// Liest Frames non-blocking, extrahiert Hinterradgeschwindigkeit je nach Bike,
|
||||||
|
// integriert mm über dt und liefert die seit letztem Aufruf addierten Millimeter.
|
||||||
|
uint32_t Process_CAN_Native_WheelSpeed();
|
15
Software/include/can_obd2.h
Normal file
15
Software/include/can_obd2.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Initialisiert das OBD2-CAN-Profil:
|
||||||
|
// - setzt Masken/Filter für 0x7E8..0x7EF (ECU-Antworten)
|
||||||
|
// - Normal-Mode sicherstellen
|
||||||
|
// Voraussetzung: CAN_HAL_Init(...) hat zuvor true geliefert.
|
||||||
|
bool Init_CAN_OBD2();
|
||||||
|
|
||||||
|
// Polling-Prozess für OBD2 über CAN (non-blocking):
|
||||||
|
// - sendet zyklisch Requests (0x7DF) auf PID 0x0D (Fahrzeuggeschwindigkeit)
|
||||||
|
// - verarbeitet Antworten 0x7E8..0x7EF
|
||||||
|
// - integriert Millimeter über dt
|
||||||
|
// Rückgabe: seit letztem Aufruf addierte Millimeter (uint32_t)
|
||||||
|
uint32_t Process_CAN_OBD2_Speed();
|
@@ -68,6 +68,7 @@
|
|||||||
// -> 6.90µl / Pulse
|
// -> 6.90µl / Pulse
|
||||||
#define DEFAULT_PUMP_DOSE 7
|
#define DEFAULT_PUMP_DOSE 7
|
||||||
|
|
||||||
|
// --- System status enum with sentinel ---
|
||||||
typedef enum eSystem_Status
|
typedef enum eSystem_Status
|
||||||
{
|
{
|
||||||
sysStat_Startup,
|
sysStat_Startup,
|
||||||
@@ -76,7 +77,8 @@ typedef enum eSystem_Status
|
|||||||
sysStat_Wash,
|
sysStat_Wash,
|
||||||
sysStat_Purge,
|
sysStat_Purge,
|
||||||
sysStat_Error,
|
sysStat_Error,
|
||||||
sysStat_Shutdown
|
sysStat_Shutdown,
|
||||||
|
SYSSTAT_COUNT // <- sentinel (must be last)
|
||||||
} tSystem_Status;
|
} tSystem_Status;
|
||||||
|
|
||||||
// Enum for different sources of speed input
|
// Enum for different sources of speed input
|
||||||
@@ -89,13 +91,10 @@ typedef enum SpeedSource_e
|
|||||||
SOURCE_GPS,
|
SOURCE_GPS,
|
||||||
SOURCE_CAN,
|
SOURCE_CAN,
|
||||||
SOURCE_OBD2_KLINE,
|
SOURCE_OBD2_KLINE,
|
||||||
SOURCE_OBD2_CAN
|
SOURCE_OBD2_CAN,
|
||||||
|
SPEEDSOURCE_COUNT // <- sentinel (must be last)
|
||||||
} SpeedSource_t;
|
} SpeedSource_t;
|
||||||
|
|
||||||
// String representation of SpeedSource enum
|
|
||||||
extern const char *SpeedSourceString[];
|
|
||||||
extern const size_t SpeedSourceString_Elements;
|
|
||||||
|
|
||||||
// Enum for GPS baud rates
|
// Enum for GPS baud rates
|
||||||
typedef enum GPSBaudRate_e
|
typedef enum GPSBaudRate_e
|
||||||
{
|
{
|
||||||
@@ -104,23 +103,30 @@ typedef enum GPSBaudRate_e
|
|||||||
BAUD_19200,
|
BAUD_19200,
|
||||||
BAUD_38400,
|
BAUD_38400,
|
||||||
BAUD_57600,
|
BAUD_57600,
|
||||||
BAUD_115200
|
BAUD_115200,
|
||||||
|
GPSBAUDRATE_COUNT // <- sentinel (must be last)
|
||||||
} GPSBaudRate_t;
|
} GPSBaudRate_t;
|
||||||
|
|
||||||
// String representation of GPSBaudRate enum
|
|
||||||
extern const char *GPSBaudRateString[];
|
|
||||||
extern const size_t GPSBaudRateString_Elements;
|
|
||||||
|
|
||||||
// Enum for CAN bus sources
|
// Enum for CAN bus sources
|
||||||
typedef enum CANSource_e
|
typedef enum CANSource_e
|
||||||
{
|
{
|
||||||
KTM_890_ADV_R_2021,
|
KTM_890_ADV_R_2021,
|
||||||
KTM_1290_SD_R_2023
|
KTM_1290_SD_R_2023,
|
||||||
|
TRIUMPH_SPEED_TWIN_1200_RS_2025,
|
||||||
|
CANSOURCE_COUNT // <- sentinel (must be last)
|
||||||
} CANSource_t;
|
} CANSource_t;
|
||||||
|
|
||||||
// String representation of CANSource enum
|
// String tables (kept internal to the module)
|
||||||
extern const char *CANSourceString[];
|
extern const char * const SystemStatusString[SYSSTAT_COUNT];
|
||||||
extern const size_t CANSourceString_Elements;
|
extern const char * const SpeedSourceString[SPEEDSOURCE_COUNT];
|
||||||
|
extern const char * const GPSBaudRateString[GPSBAUDRATE_COUNT];
|
||||||
|
extern const char * const CANSourceString[CANSOURCE_COUNT];
|
||||||
|
|
||||||
|
// Safe getters (centralized bounds check)
|
||||||
|
const char* ToString(SpeedSource_t v);
|
||||||
|
const char* ToString(GPSBaudRate_t v);
|
||||||
|
const char* ToString(CANSource_t v);
|
||||||
|
const char* ToString(tSystem_Status v);
|
||||||
|
|
||||||
#define STARTUP_DELAY 2500
|
#define STARTUP_DELAY 2500
|
||||||
#define SHUTDOWN_DELAY_MS 2500
|
#define SHUTDOWN_DELAY_MS 2500
|
||||||
|
@@ -1,27 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* @file config.h
|
* @file config.h
|
||||||
|
* @brief Configuration structures and EEPROM API for ChainLube firmware.
|
||||||
*
|
*
|
||||||
* @brief Header file for configuration settings and EEPROM operations in the ChainLube application.
|
* Defines EEPROM layout versions, configuration and persistence data structures,
|
||||||
|
* and the public functions for storing, loading, formatting and validating
|
||||||
|
* configuration/persistence records.
|
||||||
*
|
*
|
||||||
* This file defines configuration settings for the ChainLube project, including default values,
|
* Notes:
|
||||||
* EEPROM structures, and functions for EEPROM operations. It also defines enums for different sources
|
* - The system always boots with defaults in RAM; EEPROM is optional.
|
||||||
* of speed input, GPS baud rates, and CAN bus sources. Additionally, it includes functions for EEPROM handling
|
* - DTC handling for EEPROM availability and integrity is centralized in EEPROM_Process().
|
||||||
* such as storing, retrieving, and formatting configuration data.
|
|
||||||
*
|
|
||||||
* @author Marcel Peterkau
|
|
||||||
* @date 09.01.2024
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef _CONFIG_H_
|
#ifndef _CONFIG_H_
|
||||||
#define _CONFIG_H_
|
#define _CONFIG_H_
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <stdint.h>
|
||||||
#include <Wire.h>
|
|
||||||
#include <I2C_eeprom.h>
|
#include <I2C_eeprom.h>
|
||||||
#include "dtc.h"
|
#include "dtc.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
#define EEPROM_STRUCTURE_REVISION 4 // Increment this version when changing EEPROM structures
|
// Increment when EEPROM structure changes
|
||||||
|
#define EEPROM_STRUCTURE_REVISION 4
|
||||||
|
|
||||||
#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
|
||||||
@@ -29,9 +28,14 @@
|
|||||||
#define EEPROM_SIZE_BYTES I2C_DEVICESIZE_24LC256
|
#define EEPROM_SIZE_BYTES I2C_DEVICESIZE_24LC256
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EEPROM request state machine codes.
|
||||||
|
*
|
||||||
|
* Used by globals.requestEEAction to schedule EEPROM operations.
|
||||||
|
*/
|
||||||
typedef enum EERequest_e
|
typedef enum EERequest_e
|
||||||
{
|
{
|
||||||
EE_IDLE,
|
EE_IDLE = 0,
|
||||||
EE_CFG_SAVE,
|
EE_CFG_SAVE,
|
||||||
EE_CFG_LOAD,
|
EE_CFG_LOAD,
|
||||||
EE_CFG_FORMAT,
|
EE_CFG_FORMAT,
|
||||||
@@ -39,11 +43,13 @@ typedef enum EERequest_e
|
|||||||
EE_PDS_LOAD,
|
EE_PDS_LOAD,
|
||||||
EE_PDS_FORMAT,
|
EE_PDS_FORMAT,
|
||||||
EE_FORMAT_ALL,
|
EE_FORMAT_ALL,
|
||||||
EE_ALL_SAVE
|
EE_ALL_SAVE,
|
||||||
|
EE_REINITIALIZE
|
||||||
} EERequest_t;
|
} EERequest_t;
|
||||||
|
|
||||||
// Structure for persistence data stored in EEPROM
|
/**
|
||||||
|
* @brief Wear-levelled persistence data block.
|
||||||
|
*/
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint16_t writeCycleCounter;
|
uint16_t writeCycleCounter;
|
||||||
@@ -54,7 +60,9 @@ typedef struct
|
|||||||
uint32_t checksum;
|
uint32_t checksum;
|
||||||
} persistenceData_t;
|
} persistenceData_t;
|
||||||
|
|
||||||
// Structure for configuration settings stored in EEPROM
|
/**
|
||||||
|
* @brief User configuration stored in EEPROM.
|
||||||
|
*/
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint8_t EEPROM_Version;
|
uint8_t EEPROM_Version;
|
||||||
@@ -85,7 +93,9 @@ typedef struct
|
|||||||
uint32_t checksum;
|
uint32_t checksum;
|
||||||
} LubeConfig_t;
|
} LubeConfig_t;
|
||||||
|
|
||||||
// Default configuration settings
|
/**
|
||||||
|
* @brief Factory defaults for configuration (in RAM).
|
||||||
|
*/
|
||||||
const LubeConfig_t LubeConfig_defaults = {
|
const LubeConfig_t LubeConfig_defaults = {
|
||||||
0, 8000, 4000, 320, DEFAULT_PUMP_DOSE, 30, 1, 150, 70, 18, 2000, 25, 500, 10, SOURCE_IMPULSE,
|
0, 8000, 4000, 320, DEFAULT_PUMP_DOSE, 30, 1, 150, 70, 18, 2000, 25, 500, 10, SOURCE_IMPULSE,
|
||||||
BAUD_115200,
|
BAUD_115200,
|
||||||
@@ -100,21 +110,31 @@ const LubeConfig_t LubeConfig_defaults = {
|
|||||||
true,
|
true,
|
||||||
0};
|
0};
|
||||||
|
|
||||||
|
/* ==== Public API ==== */
|
||||||
|
|
||||||
|
// Initialization & main process
|
||||||
void InitEEPROM();
|
void InitEEPROM();
|
||||||
void EEPROM_Process();
|
void EEPROM_Process();
|
||||||
|
|
||||||
|
// Config & persistence access
|
||||||
void StoreConfig_EEPROM();
|
void StoreConfig_EEPROM();
|
||||||
void GetConfig_EEPROM();
|
void GetConfig_EEPROM();
|
||||||
void StorePersistence_EEPROM();
|
void StorePersistence_EEPROM();
|
||||||
void GetPersistence_EEPROM();
|
void GetPersistence_EEPROM();
|
||||||
void FormatConfig_EEPROM();
|
void FormatConfig_EEPROM();
|
||||||
void FormatPersistence_EEPROM();
|
void FormatPersistence_EEPROM();
|
||||||
|
void MovePersistencePage_EEPROM(boolean reset);
|
||||||
|
|
||||||
|
// Utilities
|
||||||
uint32_t Checksum_EEPROM(uint8_t const *data, size_t len);
|
uint32_t Checksum_EEPROM(uint8_t const *data, size_t len);
|
||||||
void dumpEEPROM(uint16_t memoryAddress, uint16_t length);
|
void dumpEEPROM(uint16_t memoryAddress, uint16_t length);
|
||||||
void MovePersistencePage_EEPROM(boolean reset);
|
|
||||||
uint32_t ConfigSanityCheck(bool autocorrect = false);
|
uint32_t ConfigSanityCheck(bool autocorrect = false);
|
||||||
bool validateWiFiString(char *string, size_t size);
|
bool validateWiFiString(char *string, size_t size);
|
||||||
|
|
||||||
|
/* ==== Externals ==== */
|
||||||
|
|
||||||
extern LubeConfig_t LubeConfig;
|
extern LubeConfig_t LubeConfig;
|
||||||
extern persistenceData_t PersistenceData;
|
extern persistenceData_t PersistenceData;
|
||||||
extern uint16_t eePersistenceMarker;
|
extern uint16_t eePersistenceAddress;
|
||||||
|
|
||||||
#endif // _CONFIG_H_
|
#endif // _CONFIG_H_
|
||||||
|
@@ -18,39 +18,63 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
|
#ifndef FW_VERSION_MAJOR
|
||||||
|
#define FW_VERSION_MAJOR 0
|
||||||
|
#endif
|
||||||
|
#ifndef FW_VERSION_MINOR
|
||||||
|
#define FW_VERSION_MINOR 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef REQ_FLASH_MAJOR
|
||||||
|
#define REQ_FLASH_MAJOR 0
|
||||||
|
#endif
|
||||||
|
#ifndef REQ_FLASH_MINOR
|
||||||
|
#define REQ_FLASH_MINOR 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef GIT_REV
|
||||||
|
#define GIT_REV "nogit"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef FW_VERSION_STR
|
||||||
|
#define FW_VERSION_STR "0.00"
|
||||||
|
#endif
|
||||||
|
#ifndef REQ_FLASH_STR
|
||||||
|
#define REQ_FLASH_STR "0.00"
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct Globals_s
|
typedef struct Globals_s
|
||||||
{
|
{
|
||||||
tSystem_Status systemStatus = sysStat_Startup; /**< Current system status */
|
tSystem_Status systemStatus = sysStat_Startup; /**< Current system status */
|
||||||
tSystem_Status resumeStatus = sysStat_Startup; /**< Status to resume after rain mode */
|
tSystem_Status resumeStatus = sysStat_Startup; /**< Status to resume after rain mode */
|
||||||
char systemStatustxt[16] = ""; /**< Text representation of system status */
|
uint16_t purgePulses = 0; /**< Number of purge pulses */
|
||||||
uint16_t purgePulses = 0; /**< Number of purge pulses */
|
EERequest_t requestEEAction = EE_IDLE; /**< EEPROM-related request */
|
||||||
EERequest_t requestEEAction = EE_IDLE; /**< EEPROM-related request */
|
char DeviceName[33]; /**< Device name */
|
||||||
char DeviceName[33]; /**< Device name */
|
char FlashVersion[10]; /**< Flash version */
|
||||||
char FlashVersion[10]; /**< Flash version */
|
uint16_t eePersistenceAddress; /**< EEPROM persistence address */
|
||||||
uint16_t eePersistanceAdress; /**< EEPROM persistence address */
|
uint8_t TankPercentage; /**< Tank percentage */
|
||||||
uint8_t TankPercentage; /**< Tank percentage */
|
bool hasDTC; /**< Flag indicating the presence of Diagnostic Trouble Codes (DTC) */
|
||||||
bool hasDTC; /**< Flag indicating the presence of Diagnostic Trouble Codes (DTC) */
|
bool measurementActive; /**< Flag indicating active measurement */
|
||||||
bool measurementActive; /**< Flag indicating active measurement */
|
uint32_t measuredPulses; /**< Number of measured pulses */
|
||||||
uint32_t measuredPulses; /**< Number of measured pulses */
|
|
||||||
bool toggle_wifi;
|
bool toggle_wifi;
|
||||||
uint16_t isr_debug;
|
uint16_t isr_debug;
|
||||||
} Globals_t;
|
} Globals_t;
|
||||||
|
|
||||||
extern Globals_t globals; /**< Global variable struct */
|
extern Globals_t globals; /**< Global variable struct */
|
||||||
|
|
||||||
typedef struct Constants_s
|
typedef struct Constants_s
|
||||||
{
|
{
|
||||||
uint8_t FW_Version_major; /**< Firmware version major number */
|
uint8_t FW_Version_major; /**< Firmware version major number */
|
||||||
uint8_t FW_Version_minor; /**< Firmware version minor number */
|
uint8_t FW_Version_minor; /**< Firmware version minor number */
|
||||||
uint8_t Required_Flash_Version_major; /**< Required flash version major number */
|
uint8_t Required_Flash_Version_major; /**< Required flash version major number */
|
||||||
uint8_t Required_Flash_Version_minor; /**< Required flash version minor number */
|
uint8_t Required_Flash_Version_minor; /**< Required flash version minor number */
|
||||||
char GitHash[11]; /**< Git hash string */
|
char GitHash[11]; /**< Git hash string */
|
||||||
} Constants_t;
|
} Constants_t;
|
||||||
|
|
||||||
const Constants_t constants PROGMEM = {
|
const Constants_t constants PROGMEM = {
|
||||||
1,4, // Firmware_Version
|
FW_VERSION_MAJOR, FW_VERSION_MINOR, // Firmware_Version
|
||||||
1,4, // Required Flash Version
|
REQ_FLASH_MAJOR, REQ_FLASH_MINOR, // Required Flash Version
|
||||||
GIT_REV // Git-Hash-String
|
QUOTE(GIT_REV) // Git-Hash-String
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
#ifndef _OBD2_CAN_H_
|
|
||||||
#define _OBD2_CAN_H_
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
// === Funktionen ===
|
|
||||||
void Init_OBD2_CAN();
|
|
||||||
uint32_t Process_OBD2_CAN_Speed();
|
|
||||||
|
|
||||||
#endif
|
|
@@ -46,4 +46,6 @@ void Webserver_Shutdown();
|
|||||||
void Websocket_PushLiveDebug(String Message);
|
void Websocket_PushLiveDebug(String Message);
|
||||||
void Websocket_PushNotification(String Message, NotificationType_t type);
|
void Websocket_PushNotification(String Message, NotificationType_t type);
|
||||||
|
|
||||||
|
void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note);
|
||||||
|
|
||||||
#endif // _WEBUI_H_
|
#endif // _WEBUI_H_
|
||||||
|
@@ -19,8 +19,16 @@ board = d1_mini
|
|||||||
framework = arduino
|
framework = arduino
|
||||||
upload_speed = 921600
|
upload_speed = 921600
|
||||||
|
|
||||||
|
custom_firmware_version = 1.07
|
||||||
|
|
||||||
|
; --- C++17 erzwingen (für if constexpr etc.) ---
|
||||||
|
; Entferne evtl. voreingestelltes -std=gnu++11/14 aus dem Core:
|
||||||
|
build_unflags =
|
||||||
|
-std=gnu++11
|
||||||
|
-std=gnu++14
|
||||||
|
; Setze C++17 für alle Envs:
|
||||||
build_flags =
|
build_flags =
|
||||||
!python codegen/git_rev_macro.py
|
-std=gnu++17
|
||||||
-DWIFI_SSID_CLIENT=${wifi_cred.wifi_ssid_client}
|
-DWIFI_SSID_CLIENT=${wifi_cred.wifi_ssid_client}
|
||||||
-DWIFI_PASSWORD_CLIENT=${wifi_cred.wifi_password_client}
|
-DWIFI_PASSWORD_CLIENT=${wifi_cred.wifi_password_client}
|
||||||
-DADMIN_PASSWORD=${wifi_cred.admin_password}
|
-DADMIN_PASSWORD=${wifi_cred.admin_password}
|
||||||
@@ -33,7 +41,7 @@ build_flags =
|
|||||||
-DFEATURE_ENABLE_OLED
|
-DFEATURE_ENABLE_OLED
|
||||||
|
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
post:codegen/prepare_littlefs.py
|
post:codegen/prepare_littlefs.py
|
||||||
pre:codegen/run_pre.py
|
pre:codegen/run_pre.py
|
||||||
|
|
||||||
@@ -41,7 +49,7 @@ monitor_filters = esp8266_exception_decoder
|
|||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
|
|
||||||
lib_ldf_mode = deep
|
lib_ldf_mode = deep
|
||||||
lib_deps =
|
lib_deps =
|
||||||
olikraus/U8g2 @ ^2.36.5
|
olikraus/U8g2 @ ^2.36.5
|
||||||
adafruit/Adafruit NeoPixel @ ^1.15.1
|
adafruit/Adafruit NeoPixel @ ^1.15.1
|
||||||
sstaub/Ticker @ ^4.4.0
|
sstaub/Ticker @ ^4.4.0
|
||||||
@@ -95,7 +103,6 @@ build_flags =
|
|||||||
-DPCB_REV=${this.custom_pcb_revision}
|
-DPCB_REV=${this.custom_pcb_revision}
|
||||||
board_build.ldscript = eagle.flash.4m1m.ld
|
board_build.ldscript = eagle.flash.4m1m.ld
|
||||||
|
|
||||||
|
|
||||||
[env:pcb_rev_1-2_serial]
|
[env:pcb_rev_1-2_serial]
|
||||||
extends = env
|
extends = env
|
||||||
custom_pcb_revision = 2
|
custom_pcb_revision = 2
|
||||||
|
@@ -1,184 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file can.cpp
|
|
||||||
*
|
|
||||||
* @brief Implementation file for CAN-related functions in the ChainLube application.
|
|
||||||
*
|
|
||||||
* This file contains the implementation of functions related to CAN (Controller Area Network)
|
|
||||||
* communication within the ChainLube application. It includes the initialization of the CAN module,
|
|
||||||
* setup of masks and filters, and processing of CAN messages. Additionally, a debug message function
|
|
||||||
* is included if CAN debugging is enabled.
|
|
||||||
*
|
|
||||||
* @author Your Name
|
|
||||||
* @date Date
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "can.h"
|
|
||||||
|
|
||||||
MCP_CAN CAN0(GPIO_CS_CAN);
|
|
||||||
#ifdef CAN_DEBUG_MESSAGE
|
|
||||||
#define MAX_DEBUG_RETRIES 100
|
|
||||||
void sendCANDebugMessage();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initializes the CAN module, sets masks, and filters based on the configured CAN source.
|
|
||||||
*
|
|
||||||
* This function initializes the CAN module, sets masks and filters based on the configured CAN source
|
|
||||||
* in the application settings, and sets the CAN module in normal mode for communication.
|
|
||||||
*/
|
|
||||||
void Init_CAN()
|
|
||||||
{
|
|
||||||
if (CAN0.begin(MCP_STDEXT, CAN_500KBPS, MCP_16MHZ) != CAN_OK)
|
|
||||||
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
|
||||||
|
|
||||||
CAN0.init_Mask(0, 0, 0x07FF0000); // Init first mask...
|
|
||||||
CAN0.init_Mask(1, 0, 0x07FF0000); // Init second mask...
|
|
||||||
|
|
||||||
switch (LubeConfig.CANSource)
|
|
||||||
{
|
|
||||||
case KTM_890_ADV_R_2021:
|
|
||||||
CAN0.init_Filt(0, 0, 0x012D0000); // Init first filter...
|
|
||||||
break;
|
|
||||||
case KTM_1290_SD_R_2023:
|
|
||||||
CAN0.init_Filt(0, 0, 0x012D0000); // Init first filter...
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
CAN0.setMode(MCP_NORMAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Processes CAN messages to determine the wheel speed based on the configured CAN source.
|
|
||||||
*
|
|
||||||
* This function reads incoming CAN messages and extracts the rear wheel speed information.
|
|
||||||
* The wheel speed is then converted to millimeters per second based on the configured CAN source.
|
|
||||||
* The function also monitors the CAN signal for potential issues and triggers diagnostic trouble codes (DTCs).
|
|
||||||
*
|
|
||||||
* @return The calculated distance traveled in millimeters since the last call.
|
|
||||||
*/
|
|
||||||
uint32_t Process_CAN_WheelSpeed()
|
|
||||||
{
|
|
||||||
#define FACTOR_RWP_KMH_890ADV 18 // Divider to convert Raw Data to km/h
|
|
||||||
#define FACTOR_RWP_KMH_1290SD 18 // Divider to convert Raw Data to km/h
|
|
||||||
can_frame canMsg;
|
|
||||||
static uint32_t lastRecTimestamp = 0;
|
|
||||||
uint16_t RearWheelSpeed_raw;
|
|
||||||
uint32_t milimeters_to_add = 0;
|
|
||||||
uint32_t RWP_millimeter_per_second = 0;
|
|
||||||
|
|
||||||
if (CAN0.readMsgBuf(&canMsg.can_id, &canMsg.can_dlc, canMsg.data) == CAN_OK)
|
|
||||||
{
|
|
||||||
|
|
||||||
switch (LubeConfig.CANSource)
|
|
||||||
{
|
|
||||||
case KTM_890_ADV_R_2021:
|
|
||||||
// raw / FACTOR_RWP_KMH_890ADV -> km/h * 100000 -> cm/h / 3600 -> cm/s
|
|
||||||
// raw * 500 -> cm/s
|
|
||||||
RearWheelSpeed_raw = (uint16_t)canMsg.data[5] << 8 | canMsg.data[6];
|
|
||||||
RWP_millimeter_per_second = (((uint32_t)RearWheelSpeed_raw * 1000000) / FACTOR_RWP_KMH_890ADV) / 3600;
|
|
||||||
break;
|
|
||||||
case KTM_1290_SD_R_2023:
|
|
||||||
// raw / FACTOR_RWP_KMH_1290SD -> km/h * 100000 -> cm/h / 3600 -> cm/s
|
|
||||||
// raw * 500 -> cm/s
|
|
||||||
RearWheelSpeed_raw = (uint16_t)canMsg.data[5] << 8 | canMsg.data[6];
|
|
||||||
RWP_millimeter_per_second = (((uint32_t)RearWheelSpeed_raw * 1000000) / FACTOR_RWP_KMH_1290SD) / 3600;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t timesincelast = millis() - lastRecTimestamp;
|
|
||||||
lastRecTimestamp = millis();
|
|
||||||
|
|
||||||
milimeters_to_add = (RWP_millimeter_per_second * timesincelast) / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastRecTimestamp > 1000)
|
|
||||||
{
|
|
||||||
MaintainDTC(DTC_NO_CAN_SIGNAL, (millis() > lastRecTimestamp + 10000 ? true : false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef CAN_DEBUG_MESSAGE
|
|
||||||
static uint32_t previousMillis = 0;
|
|
||||||
|
|
||||||
if (millis() - previousMillis >= 1000)
|
|
||||||
{
|
|
||||||
sendCANDebugMessage();
|
|
||||||
previousMillis = millis();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return milimeters_to_add;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef CAN_DEBUG_MESSAGE
|
|
||||||
/**
|
|
||||||
* @brief Sends periodic CAN debug messages for monitoring and diagnostics.
|
|
||||||
*
|
|
||||||
* This function sends periodic CAN debug messages containing various system information for monitoring and diagnostics.
|
|
||||||
* The information includes system status, timestamps, tank percentage, DTC flags, and other relevant data.
|
|
||||||
* The debug messages are sent with a configurable multiplexer to broadcast different types of information in each cycle.
|
|
||||||
*/
|
|
||||||
void sendCANDebugMessage()
|
|
||||||
{
|
|
||||||
#define MAX_DEBUG_MULTIPLEXER 6
|
|
||||||
static uint16_t DebugSendFailTimeout = 0;
|
|
||||||
static uint8_t debugMultiplexer = 0;
|
|
||||||
byte data[8] = {debugMultiplexer, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
|
||||||
uint32_t millisValue = millis();
|
|
||||||
|
|
||||||
switch (debugMultiplexer)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
memcpy(&data[1], &millisValue, sizeof(millisValue));
|
|
||||||
memcpy(&data[5], &globals.purgePulses, sizeof(globals.purgePulses));
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
data[1] = (uint8_t)globals.systemStatus;
|
|
||||||
data[2] = (uint8_t)globals.resumeStatus;
|
|
||||||
data[3] = (uint8_t)globals.requestEEAction;
|
|
||||||
data[4] = globals.TankPercentage;
|
|
||||||
data[5] = (0x01 & globals.hasDTC) | ((0x01 & globals.measurementActive) << 1);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
memcpy(&data[1], &globals.eePersistanceAdress, sizeof(globals.eePersistanceAdress));
|
|
||||||
memcpy(&data[3], &PersistenceData.tankRemain_microL, sizeof(PersistenceData.tankRemain_microL));
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
memcpy(&data[1], &PersistenceData.writeCycleCounter, sizeof(PersistenceData.writeCycleCounter));
|
|
||||||
memcpy(&data[3], &PersistenceData.TravelDistance_highRes_mm, sizeof(PersistenceData.TravelDistance_highRes_mm));
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
memcpy(&data[1], &PersistenceData.odometer_mm, sizeof(PersistenceData.odometer_mm));
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
memcpy(&data[1], &PersistenceData.odometer, sizeof(PersistenceData.odometer));
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
memcpy(&data[1], &PersistenceData.checksum, sizeof(PersistenceData.checksum));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugMultiplexer++;
|
|
||||||
debugMultiplexer = debugMultiplexer > MAX_DEBUG_MULTIPLEXER ? 0 : debugMultiplexer;
|
|
||||||
|
|
||||||
if (DebugSendFailTimeout < MAX_DEBUG_RETRIES)
|
|
||||||
{
|
|
||||||
byte sndStat = CAN0.sendMsgBuf(0x7FF, 0, 8, data);
|
|
||||||
if (sndStat != CAN_OK)
|
|
||||||
{
|
|
||||||
Debug_pushMessage("failed sending CAN-DbgMsg: %d, %d\n", sndStat, DebugSendFailTimeout);
|
|
||||||
DebugSendFailTimeout++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (DebugSendFailTimeout == MAX_DEBUG_RETRIES)
|
|
||||||
{
|
|
||||||
Debug_pushMessage("disable CAN-DbgMsg due to timeout\n");
|
|
||||||
DebugSendFailTimeout++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
458
Software/src/can_hal.cpp
Normal file
458
Software/src/can_hal.cpp
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
#include "can_hal.h"
|
||||||
|
#include "dtc.h"
|
||||||
|
#include <string.h> // memcpy, memcmp
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Interner Zustand/Helper
|
||||||
|
// =====================
|
||||||
|
MCP_CAN CAN0(GPIO_CS_CAN);
|
||||||
|
|
||||||
|
static bool s_ready = false;
|
||||||
|
static uint8_t s_nextFiltSlot = 0; // 0..5 (MCP2515 hat 6 Filter-Slots)
|
||||||
|
static uint16_t s_modeSettleMs = 10; // Default aus Config
|
||||||
|
|
||||||
|
// Trace-Hook
|
||||||
|
static CanTraceSink s_traceSink = nullptr;
|
||||||
|
|
||||||
|
// RAW-Sniffer-Steuerung (Filter offen + Restore der vorherigen Konfig)
|
||||||
|
static bool s_rawSnifferEnabled = false;
|
||||||
|
|
||||||
|
// Spiegel der "Normal"-Konfiguration (damit wir nach RAW wiederherstellen können)
|
||||||
|
static uint16_t s_savedStdMask[2] = {0x000, 0x000};
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
uint32_t id;
|
||||||
|
bool ext;
|
||||||
|
} s_savedFilt[6];
|
||||||
|
static uint8_t s_savedFiltCount = 0;
|
||||||
|
|
||||||
|
// 11-bit: Lib erwartet (value << 16)
|
||||||
|
static inline uint32_t _std_to_hw(uint16_t v11) { return ((uint32_t)v11) << 16; }
|
||||||
|
|
||||||
|
// „Bestätigter“ Mode-Wechsel mithilfe der Lib-Funktion setMode(newMode)
|
||||||
|
static bool _trySetMode(uint8_t mode, uint16_t settleMs)
|
||||||
|
{
|
||||||
|
const uint32_t t0 = millis();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (CAN0.setMode(mode) == CAN_OK)
|
||||||
|
return true;
|
||||||
|
delay(1);
|
||||||
|
} while ((millis() - t0) < settleMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOOPBACK-Selftest (ohne Bus)
|
||||||
|
static bool _selftest_loopback(uint16_t windowMs)
|
||||||
|
{
|
||||||
|
if (!_trySetMode(MCP_LOOPBACK, s_modeSettleMs))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const unsigned long tid = 0x123;
|
||||||
|
uint8_t tx[8] = {0xA5, 0x5A, 0x11, 0x22, 0x33, 0x44, 0x77, 0x88};
|
||||||
|
if (CAN0.sendMsgBuf(tid, 0, 8, tx) != CAN_OK)
|
||||||
|
{
|
||||||
|
(void)_trySetMode(MCP_NORMAL, s_modeSettleMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool got = false;
|
||||||
|
const uint32_t t0 = millis();
|
||||||
|
while ((millis() - t0) < windowMs)
|
||||||
|
{
|
||||||
|
if (CAN0.checkReceive() == CAN_MSGAVAIL)
|
||||||
|
{
|
||||||
|
unsigned long rid;
|
||||||
|
uint8_t len, rx[8];
|
||||||
|
if (CAN0.readMsgBuf(&rid, &len, rx) == CAN_OK)
|
||||||
|
{
|
||||||
|
if (rid == tid && len == 8 && memcmp(tx, rx, 8) == 0)
|
||||||
|
{
|
||||||
|
got = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)_trySetMode(MCP_NORMAL, s_modeSettleMs);
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: kurzer ListenOnly-Hörtest (nur Heuristik, keine DTC-Änderung)
|
||||||
|
static void _probe_listen_only(uint16_t ms)
|
||||||
|
{
|
||||||
|
if (ms == 0)
|
||||||
|
return;
|
||||||
|
if (!_trySetMode(MCP_LISTENONLY, s_modeSettleMs))
|
||||||
|
return;
|
||||||
|
const uint32_t t0 = millis();
|
||||||
|
while ((millis() - t0) < ms)
|
||||||
|
{
|
||||||
|
if (CAN0.checkReceive() == CAN_MSGAVAIL)
|
||||||
|
break;
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
(void)_trySetMode(MCP_NORMAL, s_modeSettleMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offen konfigurieren (RAW-Sniffer)
|
||||||
|
static bool _apply_open_filters()
|
||||||
|
{
|
||||||
|
if (!_trySetMode(MODE_CONFIG, s_modeSettleMs))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Masken 0 -> alles durchlassen
|
||||||
|
CAN0.init_Mask(0, 0, _std_to_hw(0x000));
|
||||||
|
CAN0.init_Mask(1, 0, _std_to_hw(0x000));
|
||||||
|
// Filter egal
|
||||||
|
for (uint8_t i = 0; i < 6; ++i)
|
||||||
|
{
|
||||||
|
CAN0.init_Filt(i, 0, _std_to_hw(0x000));
|
||||||
|
}
|
||||||
|
s_nextFiltSlot = 0;
|
||||||
|
|
||||||
|
return _trySetMode(MCP_NORMAL, s_modeSettleMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gespeicherte Normal-Konfiguration anwenden
|
||||||
|
static bool _apply_saved_filters()
|
||||||
|
{
|
||||||
|
if (!_trySetMode(MODE_CONFIG, s_modeSettleMs))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
CAN0.init_Mask(0, 0, _std_to_hw(s_savedStdMask[0]));
|
||||||
|
CAN0.init_Mask(1, 0, _std_to_hw(s_savedStdMask[1]));
|
||||||
|
|
||||||
|
// Erst alle Filter neutralisieren
|
||||||
|
for (uint8_t i = 0; i < 6; ++i)
|
||||||
|
{
|
||||||
|
CAN0.init_Filt(i, 0, _std_to_hw(0x000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dann gespeicherte Filter wieder setzen
|
||||||
|
s_nextFiltSlot = 0;
|
||||||
|
for (uint8_t i = 0; i < s_savedFiltCount && s_nextFiltSlot < 6; ++i)
|
||||||
|
{
|
||||||
|
const auto &F = s_savedFilt[i];
|
||||||
|
const uint32_t hwId = F.ext ? F.id : _std_to_hw((uint16_t)F.id);
|
||||||
|
CAN0.init_Filt(s_nextFiltSlot++, F.ext ? 1 : 0, hwId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _trySetMode(MCP_NORMAL, s_modeSettleMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Öffentliche API
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
void CAN_HAL_SetTraceSink(CanTraceSink sink)
|
||||||
|
{
|
||||||
|
s_traceSink = sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAN_HAL_EnableRawSniffer(bool enable)
|
||||||
|
{
|
||||||
|
if (enable == s_rawSnifferEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (enable)
|
||||||
|
{
|
||||||
|
// Auf RAW öffnen
|
||||||
|
if (_apply_open_filters())
|
||||||
|
{
|
||||||
|
s_rawSnifferEnabled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Falls es nicht klappt, lieber Defekt melden als im Zwischending zu bleiben
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Gespeicherte "Normal"-Konfiguration wieder aktivieren
|
||||||
|
if (_apply_saved_filters())
|
||||||
|
{
|
||||||
|
s_rawSnifferEnabled = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_IsRawSnifferEnabled()
|
||||||
|
{
|
||||||
|
return s_rawSnifferEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_Init(const CanHalConfig &cfg)
|
||||||
|
{
|
||||||
|
s_ready = false;
|
||||||
|
s_modeSettleMs = cfg.modeSettleMs ? cfg.modeSettleMs : 10;
|
||||||
|
s_traceSink = nullptr;
|
||||||
|
s_rawSnifferEnabled = false;
|
||||||
|
|
||||||
|
// 1) SPI/MCP starten (STDEXT ist robust gegen Fehlpfade in Lib-Forks)
|
||||||
|
if (CAN0.begin(MCP_STDEXT, cfg.baud, cfg.clock) != CAN_OK)
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Loopback-Selftest (ohne Bus)
|
||||||
|
if (!_selftest_loopback(20))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Optional Listen-Only-Probe (nur Info)
|
||||||
|
_probe_listen_only(cfg.listenOnlyProbeMs);
|
||||||
|
|
||||||
|
// 4) Default: Filter/Masks offen, Mode NORMAL
|
||||||
|
if (!_apply_open_filters())
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiale "Normal"-Spiegelung: alles offen (bis die App später echte Filter setzt)
|
||||||
|
s_savedStdMask[0] = 0x000;
|
||||||
|
s_savedStdMask[1] = 0x000;
|
||||||
|
s_savedFiltCount = 0;
|
||||||
|
for (uint8_t i = 0; i < 6; ++i)
|
||||||
|
{
|
||||||
|
s_savedFilt[i].id = 0;
|
||||||
|
s_savedFilt[i].ext = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, false);
|
||||||
|
s_ready = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_IsReady() { return s_ready; }
|
||||||
|
|
||||||
|
bool CAN_HAL_SetMode(uint8_t mode)
|
||||||
|
{
|
||||||
|
const bool ok = _trySetMode(mode, s_modeSettleMs);
|
||||||
|
if (!ok)
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_SetMask(uint8_t bank, bool ext, uint32_t rawMask)
|
||||||
|
{
|
||||||
|
if (bank > 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!CAN_HAL_SetMode(MODE_CONFIG))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const bool ok = (CAN0.init_Mask(bank, ext ? 1 : 0, rawMask) == CAN_OK);
|
||||||
|
|
||||||
|
// Spiegeln (nur STD-11 Spiegel führen wir – ext-Masken selten; bei ext ignorieren)
|
||||||
|
if (!ext)
|
||||||
|
{
|
||||||
|
const uint16_t m11 = (uint16_t)(rawMask >> 16);
|
||||||
|
s_savedStdMask[bank] = m11;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CAN_HAL_SetMode(MCP_NORMAL))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_SetStdMask11(uint8_t bank, uint16_t mask11)
|
||||||
|
{
|
||||||
|
return CAN_HAL_SetMask(bank, false, _std_to_hw(mask11));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAN_HAL_ClearFilters()
|
||||||
|
{
|
||||||
|
if (!CAN_HAL_SetMode(MODE_CONFIG))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CAN0.init_Mask(0, 0, _std_to_hw(0x000));
|
||||||
|
CAN0.init_Mask(1, 0, _std_to_hw(0x000));
|
||||||
|
for (uint8_t i = 0; i < 6; ++i)
|
||||||
|
{
|
||||||
|
CAN0.init_Filt(i, 0, _std_to_hw(0x000));
|
||||||
|
}
|
||||||
|
s_nextFiltSlot = 0;
|
||||||
|
|
||||||
|
// Spiegel auch zurücksetzen
|
||||||
|
s_savedStdMask[0] = 0x000;
|
||||||
|
s_savedStdMask[1] = 0x000;
|
||||||
|
s_savedFiltCount = 0;
|
||||||
|
for (uint8_t i = 0; i < 6; ++i)
|
||||||
|
{
|
||||||
|
s_savedFilt[i].id = 0;
|
||||||
|
s_savedFilt[i].ext = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CAN_HAL_SetMode(MCP_NORMAL))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_AddFilter(const CanFilter &f)
|
||||||
|
{
|
||||||
|
if (s_nextFiltSlot >= 6)
|
||||||
|
return false;
|
||||||
|
if (!CAN_HAL_SetMode(MODE_CONFIG))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t hwId = f.ext ? f.id : _std_to_hw((uint16_t)f.id);
|
||||||
|
const uint8_t slot = s_nextFiltSlot++;
|
||||||
|
const bool ok = (CAN0.init_Filt(slot, f.ext ? 1 : 0, hwId) == CAN_OK);
|
||||||
|
|
||||||
|
// Spiegeln
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
if (s_savedFiltCount < 6)
|
||||||
|
{
|
||||||
|
s_savedFilt[s_savedFiltCount].id = f.ext ? f.id : (uint32_t)((uint16_t)f.id);
|
||||||
|
s_savedFilt[s_savedFiltCount].ext = f.ext;
|
||||||
|
++s_savedFiltCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CAN_HAL_SetMode(MCP_NORMAL))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_SetFilters(const CanFilter *list, size_t count)
|
||||||
|
{
|
||||||
|
if (!CAN_HAL_SetMode(MODE_CONFIG))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slots zurücksetzen
|
||||||
|
s_nextFiltSlot = 0;
|
||||||
|
for (uint8_t i = 0; i < 6; ++i)
|
||||||
|
{
|
||||||
|
CAN0.init_Filt(i, 0, _std_to_hw(0x000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setzen
|
||||||
|
for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i)
|
||||||
|
{
|
||||||
|
const auto &f = list[i];
|
||||||
|
const uint32_t hwId = f.ext ? f.id : _std_to_hw((uint16_t)f.id);
|
||||||
|
CAN0.init_Filt(s_nextFiltSlot++, f.ext ? 1 : 0, hwId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spiegel aktualisieren
|
||||||
|
s_savedFiltCount = 0;
|
||||||
|
for (size_t i = 0; i < count && i < 6; ++i)
|
||||||
|
{
|
||||||
|
s_savedFilt[i].id = list[i].ext ? list[i].id : (uint32_t)((uint16_t)list[i].id);
|
||||||
|
s_savedFilt[i].ext = list[i].ext;
|
||||||
|
++s_savedFiltCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CAN_HAL_SetMode(MCP_NORMAL))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CAN_HAL_Read(unsigned long &id, uint8_t &len, uint8_t data[8])
|
||||||
|
{
|
||||||
|
if (CAN0.checkReceive() != CAN_MSGAVAIL)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK)
|
||||||
|
{
|
||||||
|
// Echte Lese-Fehler -> vermutlich SPI/Controller-Problem
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP_CAN schreibt Flags in das ID-Wort:
|
||||||
|
// bit31 = EXT, bit30 = RTR, Rest = rohe ID (11 oder 29 Bit)
|
||||||
|
const bool ext = (id & 0x80000000UL) != 0;
|
||||||
|
const bool rtr = (id & 0x40000000UL) != 0; // aktuell nur informativ
|
||||||
|
|
||||||
|
// "Saubere" ID für Aufrufer herstellen
|
||||||
|
const uint32_t clean_id = ext ? (id & 0x1FFFFFFFUL) : (id & 0x7FFUL);
|
||||||
|
id = clean_id;
|
||||||
|
|
||||||
|
// Trace-Hook (RX)
|
||||||
|
if (s_traceSink)
|
||||||
|
{
|
||||||
|
CanLogFrame f{};
|
||||||
|
f.ts_ms = millis();
|
||||||
|
f.id = clean_id;
|
||||||
|
f.ext = ext;
|
||||||
|
f.rx = true;
|
||||||
|
f.dlc = len;
|
||||||
|
if (len)
|
||||||
|
memcpy(f.data, data, len);
|
||||||
|
s_traceSink(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Wenn du RTR-Frames speziell behandeln willst, könntest du hier
|
||||||
|
// (rtr==true) markieren/loggen oder len=0 erzwingen. Für jetzt: einfach durchreichen.
|
||||||
|
(void)rtr;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t *data)
|
||||||
|
{
|
||||||
|
// Senden
|
||||||
|
uint8_t st = CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast<uint8_t *>(data));
|
||||||
|
|
||||||
|
// Trace-Hook (TX) nur bei Erfolg loggen – optional: immer loggen
|
||||||
|
if (st == CAN_OK && s_traceSink)
|
||||||
|
{
|
||||||
|
CanLogFrame f{};
|
||||||
|
f.ts_ms = millis();
|
||||||
|
f.id = id;
|
||||||
|
f.ext = ext;
|
||||||
|
f.rx = false;
|
||||||
|
f.dlc = len;
|
||||||
|
if (len)
|
||||||
|
memcpy(f.data, data, len);
|
||||||
|
s_traceSink(f);
|
||||||
|
}
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Diagnose/Utilities ====
|
||||||
|
|
||||||
|
uint8_t CAN_HAL_GetErrorFlags()
|
||||||
|
{
|
||||||
|
// getError() liefert MCP_EFLG Snapshot (Lib-abhängig)
|
||||||
|
return CAN0.getError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAN_HAL_GetErrorCounters(uint8_t &tec, uint8_t &rec)
|
||||||
|
{
|
||||||
|
tec = CAN0.errorCountTX();
|
||||||
|
rec = CAN0.errorCountRX();
|
||||||
|
}
|
228
Software/src/can_native.cpp
Normal file
228
Software/src/can_native.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// can_native.cpp – Mehrmodell-Setup (Integer-only), Triumph nutzt NUR Kanal B (W23)
|
||||||
|
|
||||||
|
#include "can_native.h"
|
||||||
|
#include "globals.h" // enthält LubeConfig.CANSource
|
||||||
|
#include "dtc.h"
|
||||||
|
#include "debugger.h"
|
||||||
|
|
||||||
|
// ====================== Gemeinsame Konstanten / Helpers ======================
|
||||||
|
|
||||||
|
// KTM-Faktoren: raw/FACTOR -> km/h
|
||||||
|
static constexpr uint16_t FACTOR_RWP_KMH_890ADV = 18;
|
||||||
|
static constexpr uint16_t FACTOR_RWP_KMH_1290SD = 18;
|
||||||
|
|
||||||
|
// Triumph 0x208: Fit ≈ 0.0073 km/h/LSB -> exakt 73/10000 km/h/LSB
|
||||||
|
// mm/s = km/h * 1_000_000 / 3600 -> 73/36 mm/s pro LSB (bei EINEM 16-Bit-Wert)
|
||||||
|
static constexpr uint16_t TRI_MMPS_NUM = 73;
|
||||||
|
static constexpr uint16_t TRI_MMPS_DEN_SINGLE = 36; // EIN Kanal (W23)
|
||||||
|
|
||||||
|
// Gemeinsamer Integrations-/Alive-Status
|
||||||
|
static uint32_t s_lastIntegrateMs = 0;
|
||||||
|
static uint32_t s_lastRxMs = 0; // für DTC_NO_CAN_SIGNAL
|
||||||
|
static uint32_t s_lastSpeed_mmps = 0; // aktuelle Geschwindigkeit [mm/s]
|
||||||
|
|
||||||
|
// mm = (mm/s * ms) / 1000
|
||||||
|
static inline uint32_t integrate_mm(uint32_t v_mmps, uint32_t dt_ms)
|
||||||
|
{
|
||||||
|
return (uint64_t)v_mmps * dt_ms / 1000ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================== Modell-Decoder (Integer) =========================
|
||||||
|
|
||||||
|
// --- KTM: 11-bit ID 0x12D, Speed in data[5..6] (BE), raw/FACTOR -> km/h -> mm/s
|
||||||
|
static uint32_t dec_ktm_rearwheel_mmps(uint8_t dlc, const uint8_t data[8], uint8_t bikeVariant /*0=890,1=1290*/)
|
||||||
|
{
|
||||||
|
if (dlc < 7) return 0; // benötigt data[5], data[6]
|
||||||
|
const uint16_t raw = (uint16_t(data[5]) << 8) | data[6];
|
||||||
|
|
||||||
|
uint16_t factor = FACTOR_RWP_KMH_890ADV;
|
||||||
|
if (bikeVariant == 1) factor = FACTOR_RWP_KMH_1290SD;
|
||||||
|
|
||||||
|
// mm/s = (raw/factor) * 1_000_000 / 3600 -> reine Integer-Mathe:
|
||||||
|
const uint32_t num = (uint32_t)raw * 1000000UL;
|
||||||
|
const uint32_t kmh_times1e6 = num / factor;
|
||||||
|
return kmh_times1e6 / 3600UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Triumph: 11-bit ID 0x208, NUR Kanal B = W23 (B2..B3, Little-Endian)
|
||||||
|
static uint32_t dec_triumph_0x208_w23_mmps(uint8_t dlc, const uint8_t data[8], uint8_t /*unused*/)
|
||||||
|
{
|
||||||
|
if (dlc < 4) return 0;
|
||||||
|
|
||||||
|
// W23 = (B2) + 256*(B3), LE
|
||||||
|
const uint16_t W23 = (uint16_t)data[2] | ((uint16_t)data[3] << 8);
|
||||||
|
|
||||||
|
if (W23 == 0) return 0;
|
||||||
|
|
||||||
|
// mm/s = (W23 * 73) / 36 — rundendes Integer-Divide
|
||||||
|
return ( (uint32_t)W23 * TRI_MMPS_NUM + (TRI_MMPS_DEN_SINGLE/2) ) / TRI_MMPS_DEN_SINGLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ Modell-Registry ================================
|
||||||
|
struct ModelSpec
|
||||||
|
{
|
||||||
|
// Erwartete 11-bit CAN-ID, min DLC, ob Extended (false=Standard)
|
||||||
|
uint16_t can_id;
|
||||||
|
uint8_t min_dlc;
|
||||||
|
bool ext;
|
||||||
|
|
||||||
|
// Decoder-Funktion → mm/s (Integer). bikeVariant: optionale Untervariante.
|
||||||
|
uint32_t (*decode_mmps)(uint8_t dlc, const uint8_t data[8], uint8_t bikeVariant);
|
||||||
|
|
||||||
|
// Optionaler Untervarianten-Index (z.B. 0=890ADV, 1=1290SD)
|
||||||
|
uint8_t bikeVariant;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Konkrete Modelle (einfach erweiterbar)
|
||||||
|
static constexpr uint16_t ID_KTM_REAR_WHEEL = 0x12D;
|
||||||
|
static constexpr uint16_t ID_TRIUMPH_SPEED = 0x208;
|
||||||
|
|
||||||
|
static uint32_t trampoline_ktm_890(uint8_t dlc, const uint8_t data[8], uint8_t) {
|
||||||
|
return dec_ktm_rearwheel_mmps(dlc, data, 0);
|
||||||
|
}
|
||||||
|
static uint32_t trampoline_ktm_1290(uint8_t dlc, const uint8_t data[8], uint8_t) {
|
||||||
|
return dec_ktm_rearwheel_mmps(dlc, data, 1);
|
||||||
|
}
|
||||||
|
static uint32_t trampoline_triumph_w23(uint8_t dlc, const uint8_t data[8], uint8_t) {
|
||||||
|
return dec_triumph_0x208_w23_mmps(dlc, data, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSpec(): mappt LubeConfig.CANSource → ModelSpec
|
||||||
|
static bool getSpec(ModelSpec &out)
|
||||||
|
{
|
||||||
|
switch (LubeConfig.CANSource)
|
||||||
|
{
|
||||||
|
case KTM_890_ADV_R_2021:
|
||||||
|
out = { ID_KTM_REAR_WHEEL, 7, false, trampoline_ktm_890, 0 };
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case KTM_1290_SD_R_2023:
|
||||||
|
out = { ID_KTM_REAR_WHEEL, 7, false, trampoline_ktm_1290, 1 };
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case TRIUMPH_SPEED_TWIN_1200_RS_2025:
|
||||||
|
// Triumph nutzt NUR W23 (Hinterrad-Kanal B)
|
||||||
|
out = { ID_TRIUMPH_SPEED, 4, false, trampoline_triumph_w23, 0 };
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false; // unbekannt → optional generisch behandeln
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Initialisierung ==============================
|
||||||
|
bool Init_CAN_Native()
|
||||||
|
{
|
||||||
|
// HAL bereitstellen
|
||||||
|
if (!CAN_HAL_IsReady())
|
||||||
|
{
|
||||||
|
CanHalConfig cfg;
|
||||||
|
cfg.baud = CAN_500KBPS;
|
||||||
|
cfg.clock = MCP_16MHZ;
|
||||||
|
cfg.listenOnlyProbeMs = 50;
|
||||||
|
|
||||||
|
if (!CAN_HAL_Init(cfg))
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
||||||
|
Debug_pushMessage("CAN(Native): HAL init failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec laden
|
||||||
|
ModelSpec spec;
|
||||||
|
const bool haveSpec = getSpec(spec);
|
||||||
|
|
||||||
|
// Masken/Filter
|
||||||
|
CAN_HAL_SetStdMask11(0, 0x7FF);
|
||||||
|
CAN_HAL_SetStdMask11(1, 0x7FF);
|
||||||
|
|
||||||
|
if (haveSpec)
|
||||||
|
{
|
||||||
|
CanFilter flist[1] = { { spec.can_id, spec.ext } };
|
||||||
|
CAN_HAL_SetFilters(flist, 1);
|
||||||
|
Debug_pushMessage("CAN(Native): Filter set (ID=0x%03X, minDLC=%u)\n", spec.can_id, spec.min_dlc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: beide IDs aktivieren (KTM+Triumph), falls Quelle unbekannt
|
||||||
|
CanFilter flist[2] = { { ID_KTM_REAR_WHEEL, false }, { ID_TRIUMPH_SPEED, false } };
|
||||||
|
CAN_HAL_SetFilters(flist, 2);
|
||||||
|
Debug_pushMessage("CAN(Native): Fallback filters (KTM=0x%03X, TRI=0x%03X)\n", ID_KTM_REAR_WHEEL, ID_TRIUMPH_SPEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
CAN_HAL_SetMode(MCP_NORMAL);
|
||||||
|
|
||||||
|
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, false);
|
||||||
|
|
||||||
|
s_lastIntegrateMs = millis();
|
||||||
|
s_lastRxMs = 0;
|
||||||
|
s_lastSpeed_mmps = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Verarbeitung ================================
|
||||||
|
uint32_t Process_CAN_Native_WheelSpeed()
|
||||||
|
{
|
||||||
|
const uint32_t now = millis();
|
||||||
|
ModelSpec spec;
|
||||||
|
const bool haveSpec = getSpec(spec);
|
||||||
|
|
||||||
|
// Frames non-blocking verarbeiten
|
||||||
|
for (uint8_t i = 0; i < 6; ++i) // kleine Obergrenze gegen Busy-Loops
|
||||||
|
{
|
||||||
|
unsigned long id;
|
||||||
|
uint8_t dlc;
|
||||||
|
uint8_t buf[8];
|
||||||
|
if (!CAN_HAL_Read(id, dlc, buf))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (haveSpec)
|
||||||
|
{
|
||||||
|
if (id == spec.can_id && dlc >= spec.min_dlc)
|
||||||
|
{
|
||||||
|
s_lastSpeed_mmps = spec.decode_mmps(dlc, buf, spec.bikeVariant);
|
||||||
|
s_lastRxMs = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: KTM prüfen
|
||||||
|
if (id == ID_KTM_REAR_WHEEL && dlc >= 7)
|
||||||
|
{
|
||||||
|
s_lastSpeed_mmps = dec_ktm_rearwheel_mmps(dlc, buf, 0);
|
||||||
|
s_lastRxMs = now;
|
||||||
|
}
|
||||||
|
// Fallback: Triumph prüfen (nur W23)
|
||||||
|
else if (id == ID_TRIUMPH_SPEED && dlc >= 4)
|
||||||
|
{
|
||||||
|
s_lastSpeed_mmps = dec_triumph_0x208_w23_mmps(dlc, buf, 0);
|
||||||
|
s_lastRxMs = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAN-Heartbeat / DTC
|
||||||
|
if (s_lastRxMs != 0)
|
||||||
|
{
|
||||||
|
const bool stale = (now - s_lastRxMs) > 10000UL;
|
||||||
|
MaintainDTC(DTC_NO_CAN_SIGNAL, stale);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
static uint32_t t0 = now;
|
||||||
|
if (now - t0 > 1000UL)
|
||||||
|
MaintainDTC(DTC_NO_CAN_SIGNAL, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration Strecke (mm)
|
||||||
|
if (s_lastIntegrateMs == 0) s_lastIntegrateMs = now;
|
||||||
|
const uint32_t dt_ms = now - s_lastIntegrateMs;
|
||||||
|
s_lastIntegrateMs = now;
|
||||||
|
|
||||||
|
const bool speedStale = (s_lastRxMs == 0) || ((now - s_lastRxMs) > 600UL);
|
||||||
|
const uint32_t v_mmps = speedStale ? 0u : s_lastSpeed_mmps;
|
||||||
|
|
||||||
|
return integrate_mm(v_mmps, dt_ms);
|
||||||
|
}
|
294
Software/src/can_obd2.cpp
Normal file
294
Software/src/can_obd2.cpp
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
#include "can_obd2.h"
|
||||||
|
#include "can_hal.h"
|
||||||
|
#include "dtc.h"
|
||||||
|
#include "debugger.h"
|
||||||
|
#include "globals.h"
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
// Trace-Sink aus webui.cpp (o.ä.)
|
||||||
|
extern void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note);
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Konfiguration (anpassbar)
|
||||||
|
// =======================
|
||||||
|
|
||||||
|
// Abfrageintervall für Speed (PID 0x0D)
|
||||||
|
#ifndef OBD2_QUERY_INTERVAL_MS
|
||||||
|
#define OBD2_QUERY_INTERVAL_MS 100 // 10 Hz
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Antwort-Timeout auf eine einzelne Anfrage
|
||||||
|
#ifndef OBD2_RESP_TIMEOUT_MS
|
||||||
|
#define OBD2_RESP_TIMEOUT_MS 120 // etwas großzügiger für reale ECUs
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Wenn so lange keine valide Antwort kam, gilt die Geschwindigkeit als stale -> v=0
|
||||||
|
#ifndef OBD2_STALE_MS
|
||||||
|
#define OBD2_STALE_MS 600 // 0,6 s
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Begrenzung, wie viele RX-Frames pro Aufruf maximal gezogen werden
|
||||||
|
#ifndef OBD2_MAX_READS_PER_CALL
|
||||||
|
#define OBD2_MAX_READS_PER_CALL 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Optionales Debug-Rate-Limit
|
||||||
|
#ifndef OBD2_DEBUG_INTERVAL_MS
|
||||||
|
#define OBD2_DEBUG_INTERVAL_MS 1000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Max. Delta-Zeit fürs Weg-Integrationsglied (Ausreißer-Klemme)
|
||||||
|
#ifndef OBD2_MAX_DT_MS
|
||||||
|
#define OBD2_MAX_DT_MS 200
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Erlaube einmaligen Fallback von funktionaler (0x7DF) auf physische Adresse (0x7E0)
|
||||||
|
#ifndef OBD2_ALLOW_PHYSICAL_FALLBACK
|
||||||
|
#define OBD2_ALLOW_PHYSICAL_FALLBACK 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// OBD-II IDs (11-bit)
|
||||||
|
// =======================
|
||||||
|
static constexpr uint16_t OBD_REQ_ID_FUNCTIONAL = 0x7DF; // Broadcast-Request
|
||||||
|
static constexpr uint16_t OBD_REQ_ID_PHYSICAL = 0x7E0; // Engine ECU (Antwort 0x7E8)
|
||||||
|
static constexpr uint16_t OBD_RESP_MIN = 0x7E8; // ECUs antworten 0x7E8..0x7EF
|
||||||
|
static constexpr uint16_t OBD_RESP_MAX = 0x7EF;
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Interner Status
|
||||||
|
// =======================
|
||||||
|
enum class ObdState : uint8_t
|
||||||
|
{
|
||||||
|
Idle = 0,
|
||||||
|
Waiting = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
static ObdState s_state = ObdState::Idle;
|
||||||
|
static uint32_t s_lastQueryTime = 0;
|
||||||
|
static uint32_t s_requestDeadline = 0;
|
||||||
|
|
||||||
|
static uint32_t s_lastRespTime = 0;
|
||||||
|
static uint32_t s_lastIntegrateMs = 0;
|
||||||
|
static uint32_t s_lastSpeedMMps = 0;
|
||||||
|
|
||||||
|
static uint32_t s_lastDbgMs = 0;
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Hilfsfunktionen
|
||||||
|
// =======================
|
||||||
|
static inline bool isResponseId(unsigned long id)
|
||||||
|
{
|
||||||
|
return (id >= OBD_RESP_MIN) && (id <= OBD_RESP_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t kmh_to_mmps(uint16_t kmh)
|
||||||
|
{
|
||||||
|
return (uint32_t)kmh * 1000000UL / 3600UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void maybeDebug(uint32_t now, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
#if 1
|
||||||
|
if (now - s_lastDbgMs < OBD2_DEBUG_INTERVAL_MS)
|
||||||
|
return;
|
||||||
|
s_lastDbgMs = now;
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
Debug_pushMessage(fmt, ap); // nimmt va_list
|
||||||
|
va_end(ap);
|
||||||
|
#else
|
||||||
|
(void)now;
|
||||||
|
(void)fmt;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Öffentliche API
|
||||||
|
// =======================
|
||||||
|
bool Init_CAN_OBD2()
|
||||||
|
{
|
||||||
|
// 1) HAL bereitstellen (Selftest inklusive). Nur initialisieren, wenn noch nicht ready.
|
||||||
|
if (!CAN_HAL_IsReady())
|
||||||
|
{
|
||||||
|
CanHalConfig cfg;
|
||||||
|
cfg.baud = CAN_500KBPS;
|
||||||
|
cfg.clock = MCP_16MHZ;
|
||||||
|
cfg.listenOnlyProbeMs = 50;
|
||||||
|
|
||||||
|
if (!CAN_HAL_Init(cfg))
|
||||||
|
{
|
||||||
|
// Hardware/Selftest failed → OBD2-CAN nicht nutzbar
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
|
||||||
|
Debug_pushMessage("CAN(OBD2): HAL init failed\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Filter/Masken für 0x7E8..0x7EF
|
||||||
|
CAN_HAL_SetStdMask11(0, 0x7F0);
|
||||||
|
CAN_HAL_SetStdMask11(1, 0x7F0);
|
||||||
|
|
||||||
|
CanFilter flist[8] = {
|
||||||
|
{0x7E8, false},
|
||||||
|
{0x7E9, false},
|
||||||
|
{0x7EA, false},
|
||||||
|
{0x7EB, false},
|
||||||
|
{0x7EC, false},
|
||||||
|
{0x7ED, false},
|
||||||
|
{0x7EE, false},
|
||||||
|
{0x7EF, false},
|
||||||
|
};
|
||||||
|
CAN_HAL_SetFilters(flist, 8);
|
||||||
|
|
||||||
|
CAN_HAL_SetMode(MCP_NORMAL);
|
||||||
|
|
||||||
|
// 3) DTC-Startzustände
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, false);
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true); // bis erste Antwort kommt
|
||||||
|
|
||||||
|
// 4) Zeitbasen resetten
|
||||||
|
const uint32_t now = millis();
|
||||||
|
s_state = ObdState::Idle;
|
||||||
|
s_lastQueryTime = now;
|
||||||
|
s_requestDeadline = 0;
|
||||||
|
s_lastRespTime = 0;
|
||||||
|
s_lastIntegrateMs = now;
|
||||||
|
s_lastSpeedMMps = 0;
|
||||||
|
s_lastDbgMs = 0;
|
||||||
|
|
||||||
|
Debug_pushMessage("CAN(OBD2): Filters set (7E8..7EF), NORMAL mode\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Process_CAN_OBD2_Speed()
|
||||||
|
{
|
||||||
|
const uint32_t now = millis();
|
||||||
|
|
||||||
|
// 1) Anfrage senden (nur wenn Idle und Intervall um)
|
||||||
|
if (s_state == ObdState::Idle && (now - s_lastQueryTime) >= OBD2_QUERY_INTERVAL_MS)
|
||||||
|
{
|
||||||
|
uint8_t req[8] = {0x02, 0x01, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00}; // Mode 01, PID 0x0D (Speed)
|
||||||
|
|
||||||
|
// Trace: geplanter Request (functional)
|
||||||
|
TRACE_OnObdFrame(OBD_REQ_ID_FUNCTIONAL, /*rx=*/false, req, 8, "req 01 0D (functional)");
|
||||||
|
|
||||||
|
uint8_t st = CAN_HAL_Send(OBD_REQ_ID_FUNCTIONAL, /*ext=*/false, 8, req);
|
||||||
|
s_lastQueryTime = now;
|
||||||
|
|
||||||
|
if (st == CAN_OK)
|
||||||
|
{
|
||||||
|
s_state = ObdState::Waiting;
|
||||||
|
s_requestDeadline = now + OBD2_RESP_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if OBD2_ALLOW_PHYSICAL_FALLBACK
|
||||||
|
// einmalig physisch versuchen (0x7E0 → Antwort 0x7E8)
|
||||||
|
TRACE_OnObdFrame(OBD_REQ_ID_PHYSICAL, /*rx=*/false, req, 8, "req 01 0D (physical)");
|
||||||
|
st = CAN_HAL_Send(OBD_REQ_ID_PHYSICAL, /*ext=*/false, 8, req);
|
||||||
|
s_lastQueryTime = now;
|
||||||
|
if (st == CAN_OK)
|
||||||
|
{
|
||||||
|
s_state = ObdState::Waiting;
|
||||||
|
s_requestDeadline = now + OBD2_RESP_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// Senden fehlgeschlagen -> harter Timeout-DTC
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
|
||||||
|
maybeDebug(now, "OBD2-CAN send failed (%u)\n", st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Non-blocking Receive: wenige Frames pro Tick ziehen
|
||||||
|
for (uint8_t i = 0; i < OBD2_MAX_READS_PER_CALL; ++i)
|
||||||
|
{
|
||||||
|
unsigned long rxId;
|
||||||
|
uint8_t len;
|
||||||
|
uint8_t rx[8];
|
||||||
|
if (!CAN_HAL_Read(rxId, len, rx))
|
||||||
|
break;
|
||||||
|
if (!isResponseId(rxId))
|
||||||
|
continue;
|
||||||
|
if (len < 3)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Erwartete Formate:
|
||||||
|
// - Einfache Antwort: 0x41 0x0D <A> ...
|
||||||
|
// - Mit Längen-Byte: 0x03/0x04 0x41 0x0D <A> ...
|
||||||
|
uint8_t modeResp = 0, pid = 0, speedKmh = 0;
|
||||||
|
if ((rx[0] == 0x03 || rx[0] == 0x04) && len >= 4 && rx[1] == 0x41 && rx[2] == 0x0D)
|
||||||
|
{
|
||||||
|
modeResp = rx[1];
|
||||||
|
pid = rx[2];
|
||||||
|
speedKmh = rx[3];
|
||||||
|
}
|
||||||
|
else if (rx[0] == 0x41 && rx[1] == 0x0D && len >= 3)
|
||||||
|
{
|
||||||
|
modeResp = rx[0];
|
||||||
|
pid = rx[1];
|
||||||
|
speedKmh = rx[2];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Nicht das gesuchte PID → optional trotzdem loggen
|
||||||
|
TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, "other OBD resp");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modeResp == 0x41 && pid == 0x0D)
|
||||||
|
{
|
||||||
|
// Valide Antwort
|
||||||
|
s_lastSpeedMMps = kmh_to_mmps(speedKmh);
|
||||||
|
s_lastRespTime = now;
|
||||||
|
s_state = ObdState::Idle;
|
||||||
|
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, false);
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, false);
|
||||||
|
|
||||||
|
char note[40];
|
||||||
|
snprintf(note, sizeof(note), "speed=%ukmh", (unsigned)speedKmh);
|
||||||
|
TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, note);
|
||||||
|
|
||||||
|
maybeDebug(now, "OBD2 speed: %u km/h (%lu mm/s)\n",
|
||||||
|
(unsigned)speedKmh, (unsigned long)s_lastSpeedMMps);
|
||||||
|
break; // eine valide Antwort pro Zyklus reicht
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ist zwar OBD-II Antwort, aber nicht unser PID – optional loggen
|
||||||
|
TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, "other OBD resp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Offene Anfrage: Timeout prüfen
|
||||||
|
if (s_state == ObdState::Waiting && (int32_t)(now - s_requestDeadline) >= 0)
|
||||||
|
{
|
||||||
|
// Keine passende Antwort erhalten
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true);
|
||||||
|
s_state = ObdState::Idle;
|
||||||
|
TRACE_OnObdFrame(0x000, /*rx=*/true, nullptr, 0, "timeout 01 0D");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Integration (mm) über dt
|
||||||
|
uint32_t add_mm = 0;
|
||||||
|
if (s_lastIntegrateMs == 0)
|
||||||
|
s_lastIntegrateMs = now;
|
||||||
|
|
||||||
|
uint32_t raw_dt = now - s_lastIntegrateMs;
|
||||||
|
if (raw_dt > OBD2_MAX_DT_MS) raw_dt = OBD2_MAX_DT_MS; // Ausreißer klemmen
|
||||||
|
const uint32_t dt_ms = raw_dt;
|
||||||
|
s_lastIntegrateMs = now;
|
||||||
|
|
||||||
|
// Stale-Schutz: wenn lange keine Antwort -> v=0
|
||||||
|
const bool stale = (s_lastRespTime == 0) || ((now - s_lastRespTime) > OBD2_STALE_MS);
|
||||||
|
const uint32_t v_mmps = stale ? 0 : s_lastSpeedMMps;
|
||||||
|
|
||||||
|
// mm = (mm/s * ms) / 1000
|
||||||
|
add_mm = (uint64_t)v_mmps * dt_ms / 1000ULL;
|
||||||
|
|
||||||
|
return add_mm;
|
||||||
|
}
|
@@ -1,7 +1,27 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
|
#define ARR_LEN(a) (sizeof(a)/sizeof((a)[0]))
|
||||||
|
|
||||||
|
static_assert(ARR_LEN(SystemStatusString) == SYSSTAT_COUNT, "SystemStatusString size mismatch");
|
||||||
|
static_assert(ARR_LEN(SpeedSourceString) == SPEEDSOURCE_COUNT, "SpeedSourceString size mismatch");
|
||||||
|
static_assert(ARR_LEN(GPSBaudRateString) == GPSBAUDRATE_COUNT, "GPSBaudRateString size mismatch");
|
||||||
|
static_assert(ARR_LEN(CANSourceString) == CANSOURCE_COUNT, "CANSourceString size mismatch");
|
||||||
|
|
||||||
|
static const char kUnknownStr[] = "Unknown";
|
||||||
|
|
||||||
|
// ---- System status string table ----
|
||||||
|
const char *const SystemStatusString[SYSSTAT_COUNT] = {
|
||||||
|
"Startup",
|
||||||
|
"Normal",
|
||||||
|
"Rain",
|
||||||
|
"Wash",
|
||||||
|
"Purge",
|
||||||
|
"Error",
|
||||||
|
"Shutdown",
|
||||||
|
};
|
||||||
|
|
||||||
// String representation of SpeedSource enum
|
// String representation of SpeedSource enum
|
||||||
const char *SpeedSourceString[] = {
|
const char *const SpeedSourceString[SPEEDSOURCE_COUNT] = {
|
||||||
#ifdef FEATURE_ENABLE_TIMER
|
#ifdef FEATURE_ENABLE_TIMER
|
||||||
"Timer",
|
"Timer",
|
||||||
#endif
|
#endif
|
||||||
@@ -12,10 +32,8 @@ const char *SpeedSourceString[] = {
|
|||||||
"OBD2 (CAN)",
|
"OBD2 (CAN)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const size_t SpeedSourceString_Elements = sizeof(SpeedSourceString) / sizeof(SpeedSourceString[0]);
|
|
||||||
|
|
||||||
// String representation of GPSBaudRate enum
|
// String representation of GPSBaudRate enum
|
||||||
const char *GPSBaudRateString[] = {
|
const char *const GPSBaudRateString[GPSBAUDRATE_COUNT] = {
|
||||||
"4800",
|
"4800",
|
||||||
"9600",
|
"9600",
|
||||||
"19200",
|
"19200",
|
||||||
@@ -24,12 +42,50 @@ const char *GPSBaudRateString[] = {
|
|||||||
"115200",
|
"115200",
|
||||||
};
|
};
|
||||||
|
|
||||||
const size_t GPSBaudRateString_Elements = sizeof(GPSBaudRateString) / sizeof(GPSBaudRateString[0]);
|
|
||||||
|
|
||||||
// String representation of CANSource enum
|
// String representation of CANSource enum
|
||||||
const char *CANSourceString[] = {
|
const char *const CANSourceString[CANSOURCE_COUNT] = {
|
||||||
"KTM 890 Adventure R (2021)",
|
"KTM 890 Adventure R (2021)",
|
||||||
"KTM 1290 Superduke R (2023)",
|
"KTM 1290 Superduke R (2023)",
|
||||||
|
"Triumph Speed Twin 1200 RS (2025)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const size_t CANSourceString_Elements = sizeof(CANSourceString) / sizeof(CANSourceString[0]);
|
// ---- Centralized, safe getters ----
|
||||||
|
|
||||||
|
// ---- Local helper for range check ----
|
||||||
|
static inline bool in_range(int v, int max_exclusive)
|
||||||
|
{
|
||||||
|
return (v >= 0) && (v < max_exclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Safe getter ----
|
||||||
|
const char *ToString(tSystem_Status v)
|
||||||
|
{
|
||||||
|
const int i = static_cast<int>(v);
|
||||||
|
return in_range(i, static_cast<int>(SYSSTAT_COUNT))
|
||||||
|
? SystemStatusString[i]
|
||||||
|
: kUnknownStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ToString(SpeedSource_t v)
|
||||||
|
{
|
||||||
|
const int i = static_cast<int>(v);
|
||||||
|
return in_range(i, static_cast<int>(SPEEDSOURCE_COUNT))
|
||||||
|
? SpeedSourceString[i]
|
||||||
|
: kUnknownStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ToString(GPSBaudRate_t v)
|
||||||
|
{
|
||||||
|
const int i = static_cast<int>(v);
|
||||||
|
return in_range(i, static_cast<int>(GPSBAUDRATE_COUNT))
|
||||||
|
? GPSBaudRateString[i]
|
||||||
|
: kUnknownStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *ToString(CANSource_t v)
|
||||||
|
{
|
||||||
|
const int i = static_cast<int>(v);
|
||||||
|
return in_range(i, static_cast<int>(CANSOURCE_COUNT))
|
||||||
|
? CANSourceString[i]
|
||||||
|
: kUnknownStr;
|
||||||
|
}
|
||||||
|
@@ -1,56 +1,181 @@
|
|||||||
/**
|
/**
|
||||||
* @file config.cpp
|
* @file config.cpp
|
||||||
* @brief Implementation of EEPROM and configuration-related functions.
|
* @brief EEPROM handling and configuration storage for the ChainLube firmware.
|
||||||
*
|
*
|
||||||
* This file contains functions for managing EEPROM storage and handling configuration data.
|
* Responsibilities:
|
||||||
* It includes the definitions of configuration structures, EEPROM access, and utility functions.
|
* - Bring-up of the external I²C EEPROM
|
||||||
|
* - Robust availability checks with optional bus recovery
|
||||||
|
* - Central processing of EEPROM requests (save/load/format/move page)
|
||||||
|
* - CRC32 utilities and debug dump helpers
|
||||||
|
*
|
||||||
|
* Design notes:
|
||||||
|
* - The device boots with sane in-RAM defaults so the system stays operable
|
||||||
|
* even when EEPROM is missing. Actual lube execution is gated by DTCs elsewhere.
|
||||||
|
* - The DTC DTC_NO_EEPROM_FOUND is set/cleared only in EEPROM_Process(), never here ad-hoc.
|
||||||
|
* - Background recovery is non-blocking and driven by millis().
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "debugger.h"
|
#include "debugger.h"
|
||||||
#include "globals.h"
|
#include "globals.h"
|
||||||
|
|
||||||
// Instance of I2C_eeprom for EEPROM access
|
// Recovery edge flag: set when availability changes 0 -> 1
|
||||||
|
static bool eeRecoveredOnce = false;
|
||||||
|
// Non-blocking retry scheduling
|
||||||
|
static uint32_t eeNextTryMs = 0;
|
||||||
|
static uint32_t eeRetryIntervalMs = 2000; // ms between background attempts
|
||||||
|
|
||||||
|
// I²C EEPROM instance
|
||||||
I2C_eeprom ee(0x50, EEPROM_SIZE_BYTES);
|
I2C_eeprom ee(0x50, EEPROM_SIZE_BYTES);
|
||||||
|
|
||||||
// Configuration and persistence data structures
|
// Configuration and persistence data
|
||||||
LubeConfig_t LubeConfig;
|
LubeConfig_t LubeConfig;
|
||||||
persistenceData_t PersistenceData;
|
persistenceData_t PersistenceData;
|
||||||
|
|
||||||
// EEPROM version identifier
|
// EEPROM structure version (bumped when layout changes)
|
||||||
const uint16_t eeVersion = EEPROM_STRUCTURE_REVISION;
|
const uint16_t eeVersion = EEPROM_STRUCTURE_REVISION;
|
||||||
|
|
||||||
// Flag indicating whether EEPROM is available
|
// Latched availability flag
|
||||||
boolean eeAvailable = false;
|
static bool eeAvailable = false;
|
||||||
|
|
||||||
// Offsets within EEPROM for LubeConfig and PersistenceData
|
// EEPROM layout offsets
|
||||||
const uint16_t startofLubeConfig = 16;
|
const uint16_t startofLubeConfig = 16;
|
||||||
const uint16_t startofPersistence = 16 + sizeof(LubeConfig) + (sizeof(LubeConfig) % 16);
|
const uint16_t startofPersistence = 16 + sizeof(LubeConfig) + (sizeof(LubeConfig) % 16);
|
||||||
|
|
||||||
// Function prototype to check EEPROM availability
|
// availability probe
|
||||||
boolean checkEEPROMavailable();
|
bool EEPROM_Available(bool recover = false, uint8_t attempts = 3, uint16_t delay_ms = 25);
|
||||||
|
|
||||||
|
// Robust EEPROM handling (internal helpers)
|
||||||
|
void I2C_BusReset();
|
||||||
|
bool TryRecoverEEPROM(uint8_t attempts = 5, uint16_t delay_ms = 50);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initializes EEPROM and checks its availability.
|
* @brief Initialize I²C and EEPROM driver, load in-RAM defaults.
|
||||||
*
|
*
|
||||||
* This function initializes the EEPROM using the I2C_eeprom instance and checks if it's available.
|
* Loads defaults into RAM to keep the application operational.
|
||||||
|
* Availability is checked but no DTC is set here—EEPROM_Process() is the single place
|
||||||
|
* that sets/clears DTC_NO_EEPROM_FOUND.
|
||||||
*/
|
*/
|
||||||
void InitEEPROM()
|
void InitEEPROM()
|
||||||
{
|
{
|
||||||
LubeConfig = LubeConfig_defaults;
|
LubeConfig = LubeConfig_defaults;
|
||||||
PersistenceData = {0};
|
PersistenceData = {0};
|
||||||
|
|
||||||
|
Wire.begin();
|
||||||
ee.begin();
|
ee.begin();
|
||||||
checkEEPROMavailable();
|
|
||||||
|
eeAvailable = ee.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Processes EEPROM actions based on the request from the global state.
|
* @brief Try to free a stuck I²C bus and enforce a STOP condition.
|
||||||
*
|
*
|
||||||
* This function processes EEPROM actions based on the request from the global state.
|
* Pulses SCL up to 9 times to release a held SDA, then issues a STOP (SDA ↑ while SCL ↑).
|
||||||
* It performs actions such as saving, loading, and formatting EEPROM data for both configuration and persistence.
|
* Finally returns control to Wire.
|
||||||
|
*/
|
||||||
|
void I2C_BusReset()
|
||||||
|
{
|
||||||
|
pinMode(SCL, OUTPUT_OPEN_DRAIN);
|
||||||
|
pinMode(SDA, INPUT_PULLUP);
|
||||||
|
|
||||||
|
for (int i = 0; i < 9; i++)
|
||||||
|
{
|
||||||
|
digitalWrite(SCL, LOW);
|
||||||
|
delayMicroseconds(5);
|
||||||
|
digitalWrite(SCL, HIGH);
|
||||||
|
delayMicroseconds(5);
|
||||||
|
}
|
||||||
|
pinMode(SDA, OUTPUT_OPEN_DRAIN);
|
||||||
|
digitalWrite(SDA, LOW);
|
||||||
|
delayMicroseconds(5);
|
||||||
|
digitalWrite(SCL, HIGH);
|
||||||
|
delayMicroseconds(5);
|
||||||
|
digitalWrite(SDA, HIGH);
|
||||||
|
delayMicroseconds(5);
|
||||||
|
|
||||||
|
pinMode(SCL, INPUT);
|
||||||
|
pinMode(SDA, INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Attempt to recover EEPROM connectivity.
|
||||||
|
*
|
||||||
|
* Sequence per attempt:
|
||||||
|
* - I²C bus reset
|
||||||
|
* - Wire.begin()
|
||||||
|
* - ee.begin()
|
||||||
|
* - short settle delay
|
||||||
|
*
|
||||||
|
* On first successful probe (0->1) the eeRecoveredOnce flag is raised.
|
||||||
|
*
|
||||||
|
* @param attempts Number of attempts
|
||||||
|
* @param delay_ms Delay between attempts (after ee.begin())
|
||||||
|
* @return true if EEPROM is reachable after recovery, false otherwise
|
||||||
|
*/
|
||||||
|
bool TryRecoverEEPROM(uint8_t attempts, uint16_t delay_ms)
|
||||||
|
{
|
||||||
|
for (uint8_t n = 0; n < attempts; n++)
|
||||||
|
{
|
||||||
|
I2C_BusReset();
|
||||||
|
|
||||||
|
// ESP8266 core: Wire.end() is not available; re-begin is sufficient
|
||||||
|
Wire.begin();
|
||||||
|
delay(2);
|
||||||
|
|
||||||
|
ee.begin();
|
||||||
|
delay(delay_ms);
|
||||||
|
|
||||||
|
if (ee.isConnected())
|
||||||
|
{
|
||||||
|
if (!eeAvailable)
|
||||||
|
eeRecoveredOnce = true; // edge 0 -> 1
|
||||||
|
eeAvailable = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eeAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Central EEPROM task: background recovery, DTC handling, and request dispatch.
|
||||||
|
*
|
||||||
|
* Called periodically from the main loop. Non-blocking by design.
|
||||||
|
* - Schedules gentle recovery tries based on millis()
|
||||||
|
* - Sets DTC_NO_EEPROM_FOUND when unavailable
|
||||||
|
* - On successful recovery edge, clears DTC and reloads CFG/PDS exactly once
|
||||||
|
* - Executes requested actions (save/load/format/move)
|
||||||
*/
|
*/
|
||||||
void EEPROM_Process()
|
void EEPROM_Process()
|
||||||
{
|
{
|
||||||
|
// Background recovery (single soft attempt per interval)
|
||||||
|
const uint32_t now = millis();
|
||||||
|
if (!EEPROM_Available() && now >= eeNextTryMs)
|
||||||
|
{
|
||||||
|
(void)TryRecoverEEPROM(1, 10);
|
||||||
|
eeNextTryMs = now + eeRetryIntervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Central DTC handling
|
||||||
|
if (!EEPROM_Available())
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_NO_EEPROM_FOUND, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recovery edge: clear DTC and reload persisted data exactly once
|
||||||
|
if (EEPROM_Available() && eeRecoveredOnce)
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_NO_EEPROM_FOUND, false);
|
||||||
|
GetConfig_EEPROM();
|
||||||
|
GetPersistence_EEPROM();
|
||||||
|
eeRecoveredOnce = false;
|
||||||
|
Debug_pushMessage("EEPROM recovered – reloaded CFG/PDS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request dispatcher
|
||||||
switch (globals.requestEEAction)
|
switch (globals.requestEEAction)
|
||||||
{
|
{
|
||||||
case EE_CFG_SAVE:
|
case EE_CFG_SAVE:
|
||||||
@@ -58,33 +183,39 @@ void EEPROM_Process()
|
|||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
Debug_pushMessage("Stored EEPROM CFG\n");
|
Debug_pushMessage("Stored EEPROM CFG\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_CFG_LOAD:
|
case EE_CFG_LOAD:
|
||||||
GetConfig_EEPROM();
|
GetConfig_EEPROM();
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
Debug_pushMessage("Loaded EEPROM CFG\n");
|
Debug_pushMessage("Loaded EEPROM CFG\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_CFG_FORMAT:
|
case EE_CFG_FORMAT:
|
||||||
FormatConfig_EEPROM();
|
FormatConfig_EEPROM();
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
GetConfig_EEPROM();
|
GetConfig_EEPROM();
|
||||||
Debug_pushMessage("Formatted EEPROM CFG\n");
|
Debug_pushMessage("Formatted EEPROM CFG\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_PDS_SAVE:
|
case EE_PDS_SAVE:
|
||||||
StorePersistence_EEPROM();
|
StorePersistence_EEPROM();
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
Debug_pushMessage("Stored EEPROM PDS\n");
|
Debug_pushMessage("Stored EEPROM PDS\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_PDS_LOAD:
|
case EE_PDS_LOAD:
|
||||||
GetPersistence_EEPROM();
|
GetPersistence_EEPROM();
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
Debug_pushMessage("Loaded EEPROM PDS\n");
|
Debug_pushMessage("Loaded EEPROM PDS\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_PDS_FORMAT:
|
case EE_PDS_FORMAT:
|
||||||
FormatPersistence_EEPROM();
|
FormatPersistence_EEPROM();
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
GetPersistence_EEPROM();
|
GetPersistence_EEPROM();
|
||||||
Debug_pushMessage("Formatted EEPROM PDS\n");
|
Debug_pushMessage("Formatted EEPROM PDS\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_FORMAT_ALL:
|
case EE_FORMAT_ALL:
|
||||||
FormatConfig_EEPROM();
|
FormatConfig_EEPROM();
|
||||||
FormatPersistence_EEPROM();
|
FormatPersistence_EEPROM();
|
||||||
@@ -93,73 +224,100 @@ void EEPROM_Process()
|
|||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
Debug_pushMessage("Formatted EEPROM ALL\n");
|
Debug_pushMessage("Formatted EEPROM ALL\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EE_ALL_SAVE:
|
case EE_ALL_SAVE:
|
||||||
StorePersistence_EEPROM();
|
StorePersistence_EEPROM();
|
||||||
StoreConfig_EEPROM();
|
StoreConfig_EEPROM();
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
Debug_pushMessage("Stored EEPROM ALL\n");
|
Debug_pushMessage("Stored EEPROM ALL\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EE_REINITIALIZE:
|
||||||
|
{
|
||||||
|
// quick burst of attempts
|
||||||
|
const bool ok = TryRecoverEEPROM(5, 20);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
// Edge & reload are handled by the block above
|
||||||
|
Debug_pushMessage("EEPROM reinitialize OK\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_NO_EEPROM_FOUND, true);
|
||||||
|
Debug_pushMessage("EEPROM reinitialize FAILED\n");
|
||||||
|
}
|
||||||
|
globals.requestEEAction = EE_IDLE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case EE_IDLE:
|
case EE_IDLE:
|
||||||
default:
|
default:
|
||||||
globals.requestEEAction = EE_IDLE;
|
globals.requestEEAction = EE_IDLE;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Stores the configuration data in EEPROM.
|
* @brief Store configuration to EEPROM (with CRC and sanity report).
|
||||||
*
|
*
|
||||||
* This function calculates the checksum for the configuration data, updates it, and stores it in EEPROM.
|
* Writes only if EEPROM is available. On completion, DTC_EEPROM_CFG_SANITY is
|
||||||
* It also performs a sanity check on the configuration and raises a diagnostic trouble code (DTC) if needed.
|
* raised if any config fields are out of plausible bounds (bitmask payload).
|
||||||
*/
|
*/
|
||||||
void StoreConfig_EEPROM()
|
void StoreConfig_EEPROM()
|
||||||
{
|
{
|
||||||
LubeConfig.checksum = 0;
|
LubeConfig.checksum = 0;
|
||||||
LubeConfig.checksum = Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig));
|
LubeConfig.checksum = Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig));
|
||||||
|
|
||||||
if (!checkEEPROMavailable())
|
if (!EEPROM_Available())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ee.updateBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig));
|
ee.updateBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig));
|
||||||
|
|
||||||
uint32_t ConfigSanityCheckResult = ConfigSanityCheck(false);
|
const uint32_t sanity = ConfigSanityCheck(false);
|
||||||
|
if (sanity > 0)
|
||||||
if (ConfigSanityCheckResult > 0)
|
|
||||||
{
|
{
|
||||||
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, ConfigSanityCheckResult);
|
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Retrieves the configuration data from EEPROM.
|
* @brief Load configuration from EEPROM and validate.
|
||||||
*
|
*
|
||||||
* This function reads the configuration data from EEPROM, performs a checksum validation,
|
* On CRC failure: raise DTC_EEPROM_CFG_BAD and fall back to in-RAM defaults (no writes).
|
||||||
* and conducts a sanity check on the configuration. It raises a diagnostic trouble code (DTC) if needed.
|
* On CRC OK: run sanity with autocorrect=true and raise DTC_EEPROM_CFG_SANITY with bitmask if needed.
|
||||||
*/
|
*/
|
||||||
void GetConfig_EEPROM()
|
void GetConfig_EEPROM()
|
||||||
{
|
{
|
||||||
if (!checkEEPROMavailable())
|
if (!EEPROM_Available())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ee.readBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig));
|
ee.readBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig));
|
||||||
|
|
||||||
uint32_t checksum = LubeConfig.checksum;
|
const uint32_t checksum = LubeConfig.checksum;
|
||||||
LubeConfig.checksum = 0;
|
LubeConfig.checksum = 0;
|
||||||
|
|
||||||
MaintainDTC(DTC_EEPROM_CFG_BAD, (Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)) != checksum));
|
const bool badCrc = (Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)) != checksum);
|
||||||
|
MaintainDTC(DTC_EEPROM_CFG_BAD, badCrc);
|
||||||
|
|
||||||
|
if (badCrc) {
|
||||||
|
// Don’t keep corrupted data in RAM
|
||||||
|
LubeConfig = LubeConfig_defaults;
|
||||||
|
LubeConfig.EEPROM_Version = EEPROM_STRUCTURE_REVISION; // explicit in-RAM version
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC OK → restore checksum and sanitize (with autocorrect)
|
||||||
LubeConfig.checksum = checksum;
|
LubeConfig.checksum = checksum;
|
||||||
|
|
||||||
uint32_t ConfigSanityCheckResult = ConfigSanityCheck(false);
|
const uint32_t sanity = ConfigSanityCheck(true);
|
||||||
|
MaintainDTC(DTC_EEPROM_CFG_SANITY, (sanity > 0), sanity);
|
||||||
MaintainDTC(DTC_EEPROM_CFG_SANITY, (ConfigSanityCheckResult > 0), ConfigSanityCheckResult);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Stores the persistence data in EEPROM.
|
* @brief Store persistence record to EEPROM (wear-levelled page).
|
||||||
*
|
*
|
||||||
* This function increments the write cycle counter, performs a checksum calculation on the persistence data,
|
* Increments the write-cycle counter and moves the page if close to the limit.
|
||||||
* and stores it in EEPROM. It also handles EEPROM page movement when needed.
|
* Writes only if EEPROM is available.
|
||||||
*/
|
*/
|
||||||
void StorePersistence_EEPROM()
|
void StorePersistence_EEPROM()
|
||||||
{
|
{
|
||||||
@@ -171,103 +329,108 @@ void StorePersistence_EEPROM()
|
|||||||
PersistenceData.checksum = 0;
|
PersistenceData.checksum = 0;
|
||||||
PersistenceData.checksum = Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData));
|
PersistenceData.checksum = Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData));
|
||||||
|
|
||||||
if (!checkEEPROMavailable())
|
if (!EEPROM_Available())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ee.updateBlock(globals.eePersistanceAdress, (uint8_t *)&PersistenceData, sizeof(PersistenceData));
|
ee.updateBlock(globals.eePersistenceAddress, (uint8_t *)&PersistenceData, sizeof(PersistenceData));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Retrieves the persistence data from EEPROM.
|
* @brief Load persistence record, validating address range and CRC.
|
||||||
*
|
*
|
||||||
* This function reads the EEPROM to get the start address of the persistence data.
|
* If the stored start address is out of range, the persistence partition is reset,
|
||||||
* If the start address is out of range, it resets and stores defaults. Otherwise,
|
* formatted, and DTC_EEPROM_PDSADRESS_BAD is raised.
|
||||||
* it reads from EEPROM and checks if the data is correct.
|
* Otherwise, the record is read and checked; on CRC failure DTC_EEPROM_PDS_BAD is raised
|
||||||
|
* and the in-RAM persistence data is reset to a safe default (no writes performed here).
|
||||||
*/
|
*/
|
||||||
void GetPersistence_EEPROM()
|
void GetPersistence_EEPROM()
|
||||||
{
|
{
|
||||||
if (!checkEEPROMavailable())
|
if (!EEPROM_Available())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ee.readBlock(0, (uint8_t *)&globals.eePersistanceAdress, sizeof(globals.eePersistanceAdress));
|
// Read wear-level start address
|
||||||
// if we got the StartAdress of Persistance and it's out of Range - we Reset it and store defaults
|
ee.readBlock(0, (uint8_t *)&globals.eePersistenceAddress, sizeof(globals.eePersistenceAddress));
|
||||||
// otherwise we Read from eeprom and check if everything is correct
|
|
||||||
if (globals.eePersistanceAdress < startofPersistence || globals.eePersistanceAdress > ee.getDeviceSize())
|
const uint16_t addr = globals.eePersistenceAddress;
|
||||||
|
const uint16_t need = sizeof(PersistenceData);
|
||||||
|
const uint16_t dev = ee.getDeviceSize();
|
||||||
|
|
||||||
|
// Strict range check: addr must be within partition and block must fit into device
|
||||||
|
if (addr < startofPersistence || (uint32_t)addr + (uint32_t)need > (uint32_t)dev)
|
||||||
{
|
{
|
||||||
MovePersistencePage_EEPROM(true);
|
MovePersistencePage_EEPROM(true);
|
||||||
FormatPersistence_EEPROM();
|
FormatPersistence_EEPROM();
|
||||||
MaintainDTC(DTC_EEPROM_PDSADRESS_BAD, true);
|
MaintainDTC(DTC_EEPROM_PDSADRESS_BAD, true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// Safe to read the record
|
||||||
|
ee.readBlock(addr, (uint8_t *)&PersistenceData, sizeof(PersistenceData));
|
||||||
|
|
||||||
|
const uint32_t checksum = PersistenceData.checksum;
|
||||||
|
PersistenceData.checksum = 0;
|
||||||
|
|
||||||
|
const bool badCrc = (Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)) != checksum);
|
||||||
|
MaintainDTC(DTC_EEPROM_PDS_BAD, badCrc);
|
||||||
|
|
||||||
|
if (badCrc)
|
||||||
{
|
{
|
||||||
ee.readBlock(globals.eePersistanceAdress, (uint8_t *)&PersistenceData, sizeof(PersistenceData));
|
// Do not keep corrupted data in RAM; leave DTC set, no EEPROM writes here
|
||||||
|
PersistenceData = {0};
|
||||||
uint32_t checksum = PersistenceData.checksum;
|
return;
|
||||||
PersistenceData.checksum = 0;
|
|
||||||
|
|
||||||
MaintainDTC(DTC_EEPROM_PDS_BAD, (Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)) != checksum));
|
|
||||||
|
|
||||||
PersistenceData.checksum = checksum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRC ok -> restore checksum into the struct kept in RAM
|
||||||
|
PersistenceData.checksum = checksum;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Formats the configuration partition in EEPROM.
|
* @brief Reset the configuration partition to defaults and write it.
|
||||||
*
|
|
||||||
* This function resets the configuration data to defaults and stores it in EEPROM.
|
|
||||||
*/
|
*/
|
||||||
void FormatConfig_EEPROM()
|
void FormatConfig_EEPROM()
|
||||||
{
|
{
|
||||||
Debug_pushMessage("Formatting Config-Partition\n");
|
Debug_pushMessage("Formatting Config partition\n");
|
||||||
LubeConfig = LubeConfig_defaults;
|
LubeConfig = LubeConfig_defaults;
|
||||||
LubeConfig.EEPROM_Version = eeVersion;
|
LubeConfig.EEPROM_Version = eeVersion;
|
||||||
StoreConfig_EEPROM();
|
StoreConfig_EEPROM();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Formats the persistence partition in EEPROM.
|
* @brief Reset the persistence partition and write an empty record.
|
||||||
*
|
|
||||||
* This function resets the persistence data to defaults and stores it in EEPROM.
|
|
||||||
*/
|
*/
|
||||||
void FormatPersistence_EEPROM()
|
void FormatPersistence_EEPROM()
|
||||||
{
|
{
|
||||||
Debug_pushMessage("Formatting Persistance-Partition\n");
|
Debug_pushMessage("Formatting Persistence partition\n");
|
||||||
PersistenceData = {0};
|
PersistenceData = {0};
|
||||||
// memset(&PersistenceData, 0, sizeof(PersistenceData));
|
|
||||||
StorePersistence_EEPROM();
|
StorePersistence_EEPROM();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Moves the persistence page in EEPROM.
|
* @brief Advance the persistence page (wear levelling) and store the new start address.
|
||||||
*
|
*
|
||||||
* This function adjusts the persistence page address and resets the write cycle counter.
|
* When end-of-device (or reset=true), wrap back to startofPersistence.
|
||||||
|
* Requires EEPROM availability.
|
||||||
*
|
*
|
||||||
* @param reset If true, the function resets the persistence page address to the start of the partition.
|
* @param reset If true, force wrap to the start of the partition.
|
||||||
*/
|
*/
|
||||||
void MovePersistencePage_EEPROM(boolean reset)
|
void MovePersistencePage_EEPROM(boolean reset)
|
||||||
{
|
{
|
||||||
if (!checkEEPROMavailable())
|
if (!EEPROM_Available())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
globals.eePersistanceAdress += sizeof(PersistenceData);
|
globals.eePersistenceAddress += sizeof(PersistenceData);
|
||||||
PersistenceData.writeCycleCounter = 0;
|
PersistenceData.writeCycleCounter = 0;
|
||||||
|
|
||||||
// Check if we reached the end of the EEPROM and start over at the beginning
|
if ((globals.eePersistenceAddress + sizeof(PersistenceData)) > ee.getDeviceSize() || reset)
|
||||||
if ((globals.eePersistanceAdress + sizeof(PersistenceData)) > ee.getDeviceSize() || reset)
|
|
||||||
{
|
{
|
||||||
globals.eePersistanceAdress = startofPersistence;
|
globals.eePersistenceAddress = startofPersistence;
|
||||||
}
|
}
|
||||||
|
|
||||||
ee.updateBlock(0, (uint8_t *)&globals.eePersistanceAdress, sizeof(globals.eePersistanceAdress));
|
ee.updateBlock(0, (uint8_t *)&globals.eePersistenceAddress, sizeof(globals.eePersistenceAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Calculate CRC-32 checksum for a block of data.
|
* @brief Compute CRC-32 (poly 0xEDB88320) over a byte buffer.
|
||||||
*
|
|
||||||
* This function implements the CRC-32 algorithm.
|
|
||||||
*
|
|
||||||
* @param data Pointer to the data block.
|
|
||||||
* @param len Length of the data block in bytes.
|
|
||||||
* @return CRC-32 checksum.
|
|
||||||
*/
|
*/
|
||||||
uint32_t Checksum_EEPROM(uint8_t const *data, size_t len)
|
uint32_t Checksum_EEPROM(uint8_t const *data, size_t len)
|
||||||
{
|
{
|
||||||
@@ -275,55 +438,43 @@ uint32_t Checksum_EEPROM(uint8_t const *data, size_t len)
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
uint32_t crc = 0xFFFFFFFF;
|
uint32_t crc = 0xFFFFFFFF;
|
||||||
uint32_t mask;
|
|
||||||
|
|
||||||
while (len--)
|
while (len--)
|
||||||
{
|
{
|
||||||
crc ^= *data++;
|
crc ^= *data++;
|
||||||
|
|
||||||
for (uint8_t k = 0; k < 8; k++)
|
for (uint8_t k = 0; k < 8; k++)
|
||||||
{
|
crc = (crc >> 1) ^ (0xEDB88320 & (-(int32_t)(crc & 1)));
|
||||||
mask = -(crc & 1);
|
|
||||||
crc = (crc >> 1) ^ (0xEDB88320 & mask);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ~crc;
|
return ~crc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Dump a portion of EEPROM contents for debugging.
|
* @brief Print a hex/ASCII dump of a region of the EEPROM for debugging.
|
||||||
*
|
*
|
||||||
* This function prints the contents of a specified portion of EEPROM in a formatted way.
|
* Output format:
|
||||||
*
|
* Address 00 01 02 ... 0F ASCII
|
||||||
* @param memoryAddress Starting address in EEPROM.
|
* 0x00000: XX XX ... .....
|
||||||
* @param length Number of bytes to dump.
|
|
||||||
*/
|
*/
|
||||||
void dumpEEPROM(uint16_t memoryAddress, uint16_t length)
|
void dumpEEPROM(uint16_t memoryAddress, uint16_t length)
|
||||||
{
|
{
|
||||||
#define BLOCK_TO_LENGTH 16
|
#define BLOCK_TO_LENGTH 16
|
||||||
|
|
||||||
if (!checkEEPROMavailable())
|
if (!EEPROM_Available())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
char ascii_buf[BLOCK_TO_LENGTH + 1];
|
char ascii_buf[BLOCK_TO_LENGTH + 1];
|
||||||
sprintf(ascii_buf, "%*s", BLOCK_TO_LENGTH, "ASCII");
|
sprintf(ascii_buf, "%*s", BLOCK_TO_LENGTH, "ASCII");
|
||||||
|
|
||||||
// Print column headers
|
|
||||||
Debug_pushMessage(PSTR("\nAddress "));
|
Debug_pushMessage(PSTR("\nAddress "));
|
||||||
for (int x = 0; x < BLOCK_TO_LENGTH; x++)
|
for (int x = 0; x < BLOCK_TO_LENGTH; x++)
|
||||||
Debug_pushMessage("%3d", x);
|
Debug_pushMessage("%3d", x);
|
||||||
|
|
||||||
// Align address and length to BLOCK_TO_LENGTH boundaries
|
memoryAddress = (memoryAddress / BLOCK_TO_LENGTH) * BLOCK_TO_LENGTH;
|
||||||
memoryAddress = memoryAddress / BLOCK_TO_LENGTH * BLOCK_TO_LENGTH;
|
length = ((length + BLOCK_TO_LENGTH - 1) / BLOCK_TO_LENGTH) * BLOCK_TO_LENGTH;
|
||||||
length = (length + BLOCK_TO_LENGTH - 1) / BLOCK_TO_LENGTH * BLOCK_TO_LENGTH;
|
|
||||||
|
|
||||||
// Iterate through the specified portion of EEPROM
|
|
||||||
for (unsigned int i = 0; i < length; i++)
|
for (unsigned int i = 0; i < length; i++)
|
||||||
{
|
{
|
||||||
int blockpoint = memoryAddress % BLOCK_TO_LENGTH;
|
const int blockpoint = memoryAddress % BLOCK_TO_LENGTH;
|
||||||
|
|
||||||
// Print ASCII representation header for each block
|
|
||||||
if (blockpoint == 0)
|
if (blockpoint == 0)
|
||||||
{
|
{
|
||||||
ascii_buf[BLOCK_TO_LENGTH] = 0;
|
ascii_buf[BLOCK_TO_LENGTH] = 0;
|
||||||
@@ -331,55 +482,54 @@ void dumpEEPROM(uint16_t memoryAddress, uint16_t length)
|
|||||||
Debug_pushMessage("\n0x%05X:", memoryAddress);
|
Debug_pushMessage("\n0x%05X:", memoryAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and print each byte
|
|
||||||
ascii_buf[blockpoint] = ee.readByte(memoryAddress);
|
ascii_buf[blockpoint] = ee.readByte(memoryAddress);
|
||||||
Debug_pushMessage(" %02X", ascii_buf[blockpoint]);
|
Debug_pushMessage(" %02X", ascii_buf[blockpoint]);
|
||||||
|
|
||||||
// Replace non-printable characters with dots in ASCII representation
|
|
||||||
if (ascii_buf[blockpoint] < 0x20 || ascii_buf[blockpoint] > 0x7E)
|
if (ascii_buf[blockpoint] < 0x20 || ascii_buf[blockpoint] > 0x7E)
|
||||||
ascii_buf[blockpoint] = '.';
|
ascii_buf[blockpoint] = '.';
|
||||||
|
|
||||||
memoryAddress++;
|
memoryAddress++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print a new line at the end of the dump
|
|
||||||
Debug_pushMessage("\n");
|
Debug_pushMessage("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check if EEPROM is available and connected.
|
* @brief Unified availability probe with optional recovery.
|
||||||
*
|
*
|
||||||
* This function checks if the EEPROM is available and connected. If not, it triggers
|
* Fast path returns the latched availability flag. If not available,
|
||||||
* a diagnostic trouble code (DTC) indicating the absence of EEPROM.
|
* performs a direct probe and, optionally, a recovery sequence.
|
||||||
*
|
*
|
||||||
|
* @param recover If true, attempt recovery when not available (default: false).
|
||||||
|
* @param attempts Recovery attempts (default: 3).
|
||||||
|
* @param delay_ms Delay between attempts in ms (default: 25).
|
||||||
* @return true if EEPROM is available, false otherwise.
|
* @return true if EEPROM is available, false otherwise.
|
||||||
*/
|
*/
|
||||||
boolean checkEEPROMavailable()
|
bool EEPROM_Available(bool recover, uint8_t attempts, uint16_t delay_ms)
|
||||||
{
|
{
|
||||||
// Check if EEPROM is connected
|
if (eeAvailable)
|
||||||
if (!ee.isConnected())
|
return true;
|
||||||
|
|
||||||
|
if (ee.isConnected())
|
||||||
{
|
{
|
||||||
// Trigger DTC for no EEPROM found
|
eeAvailable = true;
|
||||||
MaintainDTC(DTC_NO_EEPROM_FOUND, true);
|
eeRecoveredOnce = true; // edge 0 -> 1
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear DTC for no EEPROM found since it's available now
|
if (recover)
|
||||||
MaintainDTC(DTC_NO_EEPROM_FOUND, false);
|
{
|
||||||
|
return TryRecoverEEPROM(attempts, delay_ms);
|
||||||
|
}
|
||||||
|
|
||||||
// EEPROM is available
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Perform sanity check on configuration settings.
|
* @brief Validate config fields; return bitmask of invalid entries.
|
||||||
*
|
*
|
||||||
* This function checks the validity of various configuration settings and returns a bitmask
|
* If autocorrect is true, invalid fields are reset to default values.
|
||||||
* indicating which settings need to be reset. If autocorrect is enabled, it resets the settings
|
* Each bit in the returned mask identifies a specific field-group that was out-of-bounds.
|
||||||
* to their default values.
|
|
||||||
*
|
|
||||||
* @param autocorrect If true, automatically correct invalid settings by resetting to defaults.
|
|
||||||
* @return A bitmask indicating which settings need to be reset.
|
|
||||||
*/
|
*/
|
||||||
uint32_t ConfigSanityCheck(bool autocorrect)
|
uint32_t ConfigSanityCheck(bool autocorrect)
|
||||||
{
|
{
|
||||||
@@ -465,21 +615,21 @@ uint32_t ConfigSanityCheck(bool autocorrect)
|
|||||||
LubeConfig.BleedingPulses = LubeConfig_defaults.BleedingPulses;
|
LubeConfig.BleedingPulses = LubeConfig_defaults.BleedingPulses;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(LubeConfig.SpeedSource >= 0) || !(LubeConfig.SpeedSource < SpeedSourceString_Elements))
|
if (!(LubeConfig.SpeedSource >= 0) || !(LubeConfig.SpeedSource < SPEEDSOURCE_COUNT))
|
||||||
{
|
{
|
||||||
SET_BIT(setting_reset_bits, 11);
|
SET_BIT(setting_reset_bits, 11);
|
||||||
if (autocorrect)
|
if (autocorrect)
|
||||||
LubeConfig.SpeedSource = LubeConfig_defaults.SpeedSource;
|
LubeConfig.SpeedSource = LubeConfig_defaults.SpeedSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(LubeConfig.GPSBaudRate >= 0) || !(LubeConfig.GPSBaudRate < GPSBaudRateString_Elements))
|
if (!(LubeConfig.GPSBaudRate >= 0) || !(LubeConfig.GPSBaudRate < GPSBAUDRATE_COUNT))
|
||||||
{
|
{
|
||||||
SET_BIT(setting_reset_bits, 12);
|
SET_BIT(setting_reset_bits, 12);
|
||||||
if (autocorrect)
|
if (autocorrect)
|
||||||
LubeConfig.GPSBaudRate = LubeConfig_defaults.GPSBaudRate;
|
LubeConfig.GPSBaudRate = LubeConfig_defaults.GPSBaudRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(LubeConfig.CANSource >= 0) || !(LubeConfig.CANSource < CANSourceString_Elements))
|
if (!(LubeConfig.CANSource >= 0) || !(LubeConfig.CANSource < CANSOURCE_COUNT))
|
||||||
{
|
{
|
||||||
SET_BIT(setting_reset_bits, 13);
|
SET_BIT(setting_reset_bits, 13);
|
||||||
if (autocorrect)
|
if (autocorrect)
|
||||||
@@ -513,22 +663,17 @@ uint32_t ConfigSanityCheck(bool autocorrect)
|
|||||||
if (autocorrect)
|
if (autocorrect)
|
||||||
strncpy(LubeConfig.wifi_client_password, LubeConfig_defaults.wifi_client_password, sizeof(LubeConfig.wifi_client_password));
|
strncpy(LubeConfig.wifi_client_password, LubeConfig_defaults.wifi_client_password, sizeof(LubeConfig.wifi_client_password));
|
||||||
}
|
}
|
||||||
// Return the bitmask indicating which settings need to be reset
|
|
||||||
return setting_reset_bits;
|
return setting_reset_bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Validates whether a given string contains only characters allowed in WiFi SSIDs and passwords.
|
* @brief Validate that a string contains only characters allowed for Wi‑Fi SSIDs/passwords.
|
||||||
*
|
*
|
||||||
* This function checks each character in the provided string to ensure
|
* Allowed: A‑Z, a‑z, 0‑9 and the printable ASCII punctuation: ! " # $ % & ' ( ) * + , - . / : ;
|
||||||
* that it contains only characters allowed in WiFi SSIDs and passwords.
|
* < = > ? @ [ \ ] ^ _ ` { | } ~
|
||||||
* It considers characters from 'A' to 'Z', 'a' to 'z', '0' to '9', as well as
|
|
||||||
* the following special characters: ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
|
|
||||||
*
|
*
|
||||||
* @param string Pointer to the string to be validated.
|
* @return true if valid (or empty), false otherwise.
|
||||||
* @param size Size of the string including the null-terminator.
|
|
||||||
* @return true if the string contains only allowed characters or is NULL,
|
|
||||||
* false otherwise.
|
|
||||||
*/
|
*/
|
||||||
bool validateWiFiString(char *string, size_t size)
|
bool validateWiFiString(char *string, size_t size)
|
||||||
{
|
{
|
||||||
@@ -539,10 +684,8 @@ bool validateWiFiString(char *string, size_t size)
|
|||||||
{
|
{
|
||||||
char c = string[i];
|
char c = string[i];
|
||||||
if (c == '\0')
|
if (c == '\0')
|
||||||
{
|
return true; // reached end with valid chars
|
||||||
// Reached the end of the string, all characters were valid WiFi characters.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||||
(c >= '0' && c <= '9') || c == '!' || c == '"' || c == '#' ||
|
(c >= '0' && c <= '9') || c == '!' || c == '"' || c == '#' ||
|
||||||
c == '$' || c == '%' || c == '&' || c == '\'' || c == '(' ||
|
c == '$' || c == '%' || c == '&' || c == '\'' || c == '(' ||
|
||||||
@@ -552,11 +695,9 @@ bool validateWiFiString(char *string, size_t size)
|
|||||||
c == '\\' || c == ']' || c == '^' || c == '_' || c == '`' ||
|
c == '\\' || c == ']' || c == '^' || c == '_' || c == '`' ||
|
||||||
c == '{' || c == '|' || c == '}' || c == '~'))
|
c == '{' || c == '|' || c == '}' || c == '~'))
|
||||||
{
|
{
|
||||||
// Found a character that is not a valid WiFi character.
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the loop completes without finding a null terminator, the string is invalid.
|
// No NUL within buffer: treat as invalid
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
|
|
||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Startup"));
|
|
||||||
LEDControl_SetBasic(LED_STARTUP_NORMAL, LED_PATTERN_BLINK);
|
LEDControl_SetBasic(LED_STARTUP_NORMAL, LED_PATTERN_BLINK);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
globals.resumeStatus = sysStat_Startup;
|
globals.resumeStatus = sysStat_Startup;
|
||||||
@@ -72,7 +71,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
case sysStat_Normal:
|
case sysStat_Normal:
|
||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Normal"));
|
|
||||||
LEDControl_SetBasic(LED_NORMAL_COLOR, LED_PATTERN_ON);
|
LEDControl_SetBasic(LED_NORMAL_COLOR, LED_PATTERN_ON);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
globals.resumeStatus = sysStat_Normal;
|
globals.resumeStatus = sysStat_Normal;
|
||||||
@@ -89,7 +87,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
case sysStat_Rain:
|
case sysStat_Rain:
|
||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Rain"));
|
|
||||||
LEDControl_SetBasic(LED_RAIN_COLOR, LED_PATTERN_ON);
|
LEDControl_SetBasic(LED_RAIN_COLOR, LED_PATTERN_ON);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
globals.resumeStatus = sysStat_Rain;
|
globals.resumeStatus = sysStat_Rain;
|
||||||
@@ -107,7 +104,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
washModeRemainDistance = LubeConfig.WashMode_Distance;
|
washModeRemainDistance = LubeConfig.WashMode_Distance;
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Wash"));
|
|
||||||
LEDControl_SetBasic(LED_WASH_COLOR, LED_PATTERN_BREATH);
|
LEDControl_SetBasic(LED_WASH_COLOR, LED_PATTERN_BREATH);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
}
|
}
|
||||||
@@ -134,7 +130,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
globals.purgePulses = LubeConfig.BleedingPulses;
|
globals.purgePulses = LubeConfig.BleedingPulses;
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Purge"));
|
|
||||||
LEDControl_SetBasic(LED_PURGE_COLOR, LED_PATTERN_BLINK);
|
LEDControl_SetBasic(LED_PURGE_COLOR, LED_PATTERN_BLINK);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
}
|
}
|
||||||
@@ -161,7 +156,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
|
|
||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Error"));
|
|
||||||
LEDControl_SetBasic(LED_ERROR_COLOR, LED_PATTERN_BLINK_FAST);
|
LEDControl_SetBasic(LED_ERROR_COLOR, LED_PATTERN_BLINK_FAST);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
}
|
}
|
||||||
@@ -173,7 +167,6 @@ void RunLubeApp(uint32_t add_milimeters)
|
|||||||
|
|
||||||
if (lastSystemStatus != globals.systemStatus)
|
if (lastSystemStatus != globals.systemStatus)
|
||||||
{
|
{
|
||||||
strcpy_P(globals.systemStatustxt, PSTR("Shutdown"));
|
|
||||||
LEDControl_SetBasic(LED_SHUTDOWN_COLOR, LED_PATTERN_BREATH_REVERSE);
|
LEDControl_SetBasic(LED_SHUTDOWN_COLOR, LED_PATTERN_BREATH_REVERSE);
|
||||||
lastSystemStatus = globals.systemStatus;
|
lastSystemStatus = globals.systemStatus;
|
||||||
}
|
}
|
||||||
|
@@ -33,12 +33,12 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "globals.h"
|
#include "globals.h"
|
||||||
#include "debugger.h"
|
#include "debugger.h"
|
||||||
#include "can.h"
|
|
||||||
#include "gps.h"
|
#include "gps.h"
|
||||||
#include "dtc.h"
|
#include "dtc.h"
|
||||||
#include "led_colors.h"
|
#include "led_colors.h"
|
||||||
#include "obd2_kline.h"
|
#include "obd2_kline.h"
|
||||||
#include "obd2_can.h"
|
#include "can_obd2.h"
|
||||||
|
#include "can_native.h"
|
||||||
#include "buttoncontrol.h"
|
#include "buttoncontrol.h"
|
||||||
#include "button_actions.h"
|
#include "button_actions.h"
|
||||||
#include "ledcontrol.h"
|
#include "ledcontrol.h"
|
||||||
@@ -102,6 +102,9 @@ void setup()
|
|||||||
// Initialize and clear Diagnostic Trouble Code (DTC) storage
|
// Initialize and clear Diagnostic Trouble Code (DTC) storage
|
||||||
ClearAllDTC();
|
ClearAllDTC();
|
||||||
|
|
||||||
|
Wire.begin();
|
||||||
|
|
||||||
|
|
||||||
#ifdef FEATURE_ENABLE_WIFI_CLIENT
|
#ifdef FEATURE_ENABLE_WIFI_CLIENT
|
||||||
// Configure WiFi settings for client mode if enabled
|
// Configure WiFi settings for client mode if enabled
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
@@ -142,8 +145,8 @@ void setup()
|
|||||||
switch (LubeConfig.SpeedSource)
|
switch (LubeConfig.SpeedSource)
|
||||||
{
|
{
|
||||||
case SOURCE_CAN:
|
case SOURCE_CAN:
|
||||||
Init_CAN();
|
Init_CAN_Native();
|
||||||
wheelSpeedcapture = &Process_CAN_WheelSpeed;
|
wheelSpeedcapture = &Process_CAN_Native_WheelSpeed;
|
||||||
Serial.print("\nCAN-Init done");
|
Serial.print("\nCAN-Init done");
|
||||||
break;
|
break;
|
||||||
case SOURCE_GPS:
|
case SOURCE_GPS:
|
||||||
@@ -163,8 +166,8 @@ void setup()
|
|||||||
Serial.print("\nOBD2-KLine-Init done");
|
Serial.print("\nOBD2-KLine-Init done");
|
||||||
break;
|
break;
|
||||||
case SOURCE_OBD2_CAN:
|
case SOURCE_OBD2_CAN:
|
||||||
Init_OBD2_CAN();
|
Init_CAN_OBD2();
|
||||||
wheelSpeedcapture = &Process_OBD2_CAN_Speed;
|
wheelSpeedcapture = &Process_CAN_OBD2_Speed;
|
||||||
Serial.print("\nOBD2-CAN-Init done");
|
Serial.print("\nOBD2-CAN-Init done");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -398,7 +401,7 @@ void Display_Process()
|
|||||||
DistRemain = DistRemain - (PersistenceData.TravelDistance_highRes_mm / 1000);
|
DistRemain = DistRemain - (PersistenceData.TravelDistance_highRes_mm / 1000);
|
||||||
|
|
||||||
// Display relevant information on the OLED screen based on system status
|
// Display relevant information on the OLED screen based on system status
|
||||||
u8x8.printf(PSTR("Mode: %10s\n"), globals.systemStatustxt);
|
u8x8.printf(PSTR("Mode: %10s\n"), ToString(globals.systemStatus));
|
||||||
if (globals.systemStatus == sysStat_Error)
|
if (globals.systemStatus == sysStat_Error)
|
||||||
{
|
{
|
||||||
// Display the last Diagnostic Trouble Code (DTC) in case of an error
|
// Display the last Diagnostic Trouble Code (DTC) in case of an error
|
||||||
@@ -412,7 +415,8 @@ void Display_Process()
|
|||||||
u8x8.printf(PSTR("WiFi: %10s\n"), (WiFi.getMode() == WIFI_AP ? "AP" : WiFi.getMode() == WIFI_OFF ? "OFF"
|
u8x8.printf(PSTR("WiFi: %10s\n"), (WiFi.getMode() == WIFI_AP ? "AP" : WiFi.getMode() == WIFI_OFF ? "OFF"
|
||||||
: WiFi.getMode() == WIFI_STA ? "CLIENT"
|
: WiFi.getMode() == WIFI_STA ? "CLIENT"
|
||||||
: "UNKNOWN"));
|
: "UNKNOWN"));
|
||||||
u8x8.printf(PSTR("Source: %8s\n"), SpeedSourceString[LubeConfig.SpeedSource]);
|
u8x8.printf(PSTR("Source: %8s\n"), ToString(LubeConfig.SpeedSource));
|
||||||
|
|
||||||
u8x8.printf("%s\n", WiFi.localIP().toString().c_str());
|
u8x8.printf("%s\n", WiFi.localIP().toString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,84 +0,0 @@
|
|||||||
#include "obd2_can.h"
|
|
||||||
#include <mcp_can.h>
|
|
||||||
#include <SPI.h>
|
|
||||||
#include "common.h"
|
|
||||||
#include "globals.h"
|
|
||||||
#include "dtc.h"
|
|
||||||
#include "debugger.h"
|
|
||||||
|
|
||||||
// === Setup: MCP2515 CS-Pin definieren ===
|
|
||||||
#define OBD2_CAN_CS_PIN 10
|
|
||||||
#define OBD2_OBD_REQUEST_ID 0x7DF
|
|
||||||
#define OBD2_OBD_RESPONSE_ID 0x7E8
|
|
||||||
|
|
||||||
MCP_CAN OBD_CAN(OBD2_CAN_CS_PIN);
|
|
||||||
|
|
||||||
static uint32_t lastQueryTime = 0;
|
|
||||||
static uint32_t lastRecvTime = 0;
|
|
||||||
static uint32_t lastSpeedMMperSec = 0;
|
|
||||||
|
|
||||||
#define OBD2_QUERY_INTERVAL 500 // alle 500ms
|
|
||||||
|
|
||||||
void Init_OBD2_CAN()
|
|
||||||
{
|
|
||||||
if (OBD_CAN.begin(MCP_STD, CAN_500KBPS, MCP_16MHZ) != CAN_OK)
|
|
||||||
{
|
|
||||||
Serial.println("OBD2 CAN Init FAILED!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OBD_CAN.setMode(MCP_NORMAL);
|
|
||||||
delay(100);
|
|
||||||
Serial.println("OBD2 CAN Init OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t Process_OBD2_CAN_Speed()
|
|
||||||
{
|
|
||||||
if (millis() - lastQueryTime < OBD2_QUERY_INTERVAL)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
lastQueryTime = millis();
|
|
||||||
|
|
||||||
// Anfrage: 01 0D → Geschwindigkeit
|
|
||||||
byte obdRequest[8] = {0x02, 0x01, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00};
|
|
||||||
byte sendStat = OBD_CAN.sendMsgBuf(OBD2_OBD_REQUEST_ID, 0, 8, obdRequest);
|
|
||||||
|
|
||||||
if (sendStat != CAN_OK)
|
|
||||||
{
|
|
||||||
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
|
|
||||||
Debug_pushMessage("OBD2_CAN: send failed (%d)\n", sendStat);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long rxId;
|
|
||||||
byte len = 0;
|
|
||||||
byte rxBuf[8];
|
|
||||||
uint32_t timeout = millis() + 100;
|
|
||||||
|
|
||||||
while (millis() < timeout)
|
|
||||||
{
|
|
||||||
if (OBD_CAN.checkReceive() == CAN_MSGAVAIL)
|
|
||||||
{
|
|
||||||
OBD_CAN.readMsgBuf(&rxId, &len, rxBuf);
|
|
||||||
if ((rxId & 0xFFF8) == OBD2_OBD_RESPONSE_ID && rxBuf[1] == 0x0D)
|
|
||||||
{
|
|
||||||
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, false); // alles ok
|
|
||||||
|
|
||||||
uint8_t speed_kmh = rxBuf[3];
|
|
||||||
uint32_t speed_mm_per_sec = (uint32_t)speed_kmh * 1000000 / 3600;
|
|
||||||
uint32_t dt = millis() - lastRecvTime;
|
|
||||||
lastRecvTime = millis();
|
|
||||||
lastSpeedMMperSec = speed_mm_per_sec;
|
|
||||||
|
|
||||||
Debug_pushMessage("OBD2_CAN: %d km/h (%lu mm/s)\n", speed_kmh, speed_mm_per_sec);
|
|
||||||
return (speed_mm_per_sec * dt) / 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keine Antwort erhalten
|
|
||||||
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true);
|
|
||||||
Debug_pushMessage("OBD2_CAN: no response within timeout\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@@ -13,11 +13,13 @@
|
|||||||
|
|
||||||
#include "webui.h"
|
#include "webui.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "can_hal.h" // <-- für CanLogFrame, Trace-Sink
|
||||||
#include <memory> // std::unique_ptr
|
#include <memory> // std::unique_ptr
|
||||||
#include <cstring> // strlen, strncpy, memcpy
|
#include <cstring> // strlen, strncpy, memcpy
|
||||||
#include <algorithm> // std::clamp
|
#include <algorithm> // std::clamp
|
||||||
|
|
||||||
AsyncWebServer webServer(80);
|
AsyncWebServer webServer(80);
|
||||||
|
AsyncWebSocket webSocket("/ws");
|
||||||
|
|
||||||
const char *PARAM_MESSAGE = "message";
|
const char *PARAM_MESSAGE = "message";
|
||||||
|
|
||||||
@@ -52,8 +54,6 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
|
|||||||
void WebServerEEJSON_Callback(AsyncWebServerRequest *request);
|
void WebServerEEJSON_Callback(AsyncWebServerRequest *request);
|
||||||
void GetFlashVersion(char *buff, size_t buff_size);
|
void GetFlashVersion(char *buff, size_t buff_size);
|
||||||
|
|
||||||
AsyncWebSocket webSocket("/ws");
|
|
||||||
|
|
||||||
void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
|
void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
|
||||||
void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len);
|
void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len);
|
||||||
void Websocket_RefreshClientData_DTCs(uint32_t client_id);
|
void Websocket_RefreshClientData_DTCs(uint32_t client_id);
|
||||||
@@ -65,7 +65,10 @@ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierB
|
|||||||
int findIndexByString(const char *searchString, const char *const *array, int arraySize);
|
int findIndexByString(const char *searchString, const char *const *array, int arraySize);
|
||||||
|
|
||||||
// ---------- small helpers (safety) ----------
|
// ---------- small helpers (safety) ----------
|
||||||
static inline const char *nz(const char *p) { return p ? p : ""; }
|
static inline const char *nz(const char *p)
|
||||||
|
{
|
||||||
|
return p ? p : "";
|
||||||
|
}
|
||||||
|
|
||||||
static inline String tableStr(const char *const *tbl, int idx, int size)
|
static inline String tableStr(const char *const *tbl, int idx, int size)
|
||||||
{
|
{
|
||||||
@@ -92,6 +95,290 @@ static inline bool validIndex(int idx, int size)
|
|||||||
return idx >= 0 && idx < size;
|
return idx >= 0 && idx < size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WebSocket-basierter Trace
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
enum class TraceMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Raw,
|
||||||
|
Obd
|
||||||
|
};
|
||||||
|
|
||||||
|
static TraceMode g_traceMode = TraceMode::None;
|
||||||
|
static uint32_t g_traceOwnerId = 0; // WS-Client-ID des Starters
|
||||||
|
static uint32_t g_traceStartMs = 0;
|
||||||
|
static uint32_t g_traceLines = 0;
|
||||||
|
static uint32_t g_traceDrops = 0;
|
||||||
|
|
||||||
|
// Aktueller WS-Client während WS_EVT_DATA (für HandleMessage)
|
||||||
|
static AsyncWebSocketClient *g_wsCurrentClient = nullptr;
|
||||||
|
|
||||||
|
// Ringpuffer (verlusttolerant)
|
||||||
|
// ---- Dynamischer Ringpuffer, um BSS klein zu halten ----
|
||||||
|
#ifndef TRACE_FMT_BUFSZ
|
||||||
|
#define TRACE_FMT_BUFSZ 128 // Puffer für ASCII-Zeile (unabhängig vom Ring)
|
||||||
|
#endif
|
||||||
|
#ifndef TRACE_DEFAULT_LINES
|
||||||
|
#define TRACE_DEFAULT_LINES 64 // 64 x 128 = 8KB
|
||||||
|
#endif
|
||||||
|
#ifndef TRACE_DEFAULT_LINE_MAX
|
||||||
|
#define TRACE_DEFAULT_LINE_MAX 128
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static char *g_ring = nullptr; // contiguous: lines * lineSize
|
||||||
|
static uint16_t g_ringLines = 0;
|
||||||
|
static uint16_t g_lineSize = 0;
|
||||||
|
static uint16_t g_head = 0, g_tail = 0;
|
||||||
|
|
||||||
|
static inline bool ring_alloc(uint16_t lines, uint16_t lineSize)
|
||||||
|
{
|
||||||
|
size_t bytes = (size_t)lines * (size_t)lineSize;
|
||||||
|
g_ring = (char *)malloc(bytes);
|
||||||
|
if (!g_ring)
|
||||||
|
return false;
|
||||||
|
g_ringLines = lines;
|
||||||
|
g_lineSize = lineSize;
|
||||||
|
g_head = g_tail = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static inline void ring_free()
|
||||||
|
{
|
||||||
|
if (g_ring)
|
||||||
|
free(g_ring);
|
||||||
|
g_ring = nullptr;
|
||||||
|
g_ringLines = 0;
|
||||||
|
g_lineSize = 0;
|
||||||
|
g_head = g_tail = 0;
|
||||||
|
}
|
||||||
|
static inline bool ring_empty() { return g_head == g_tail; }
|
||||||
|
static inline bool ring_full() { return (uint16_t)(g_head + 1) == g_tail; }
|
||||||
|
|
||||||
|
static inline char *ring_slot(uint16_t idx) { return g_ring + ((idx % g_ringLines) * g_lineSize); }
|
||||||
|
|
||||||
|
static inline void ring_push_line(const char *s)
|
||||||
|
{
|
||||||
|
if (!g_ring)
|
||||||
|
return;
|
||||||
|
if (ring_full())
|
||||||
|
{
|
||||||
|
g_tail++;
|
||||||
|
g_traceDrops++;
|
||||||
|
}
|
||||||
|
char *dst = ring_slot(g_head);
|
||||||
|
strncpy(dst, s, g_lineSize - 1);
|
||||||
|
dst[g_lineSize - 1] = '\0';
|
||||||
|
g_head++;
|
||||||
|
}
|
||||||
|
static inline const char *ring_front() { return ring_empty() ? "" : ring_slot(g_tail); }
|
||||||
|
static inline void ring_pop()
|
||||||
|
{
|
||||||
|
if (!ring_empty())
|
||||||
|
g_tail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: direkt senden, falls kein Ring (zu wenig RAM)
|
||||||
|
static inline void trace_emit_line_direct_or_ring(const char *s)
|
||||||
|
{
|
||||||
|
if (g_ring)
|
||||||
|
{ // wenn Ring existiert -> dort ablegen
|
||||||
|
ring_push_line(s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// sonst: direkt an den Owner senden (Best-Effort)
|
||||||
|
if (g_traceOwnerId && webSocket.availableForWrite(g_traceOwnerId))
|
||||||
|
{
|
||||||
|
String payload;
|
||||||
|
payload.reserve(strlen(s) + 16);
|
||||||
|
payload += "TRACELINE;";
|
||||||
|
payload += s;
|
||||||
|
payload += "\n";
|
||||||
|
webSocket.text(g_traceOwnerId, payload);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_traceDrops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASCII-Formatter (ID 3-stellig 11-bit, 8-stellig 29-bit)
|
||||||
|
static void TRACE_FormatLine(char *dst, size_t n, const CanLogFrame &f, const char *note)
|
||||||
|
{
|
||||||
|
int off = snprintf(dst, n, "%lu %s 0x%0*lX %u ",
|
||||||
|
(unsigned long)f.ts_ms,
|
||||||
|
f.rx ? "RX" : "TX",
|
||||||
|
f.ext ? 8 : 3, (unsigned long)f.id,
|
||||||
|
f.dlc);
|
||||||
|
for (uint8_t i = 0; i < f.dlc && off < (int)n - 3; i++)
|
||||||
|
off += snprintf(dst + off, n - off, "%02X ", f.data[i]);
|
||||||
|
if (note && *note)
|
||||||
|
snprintf(dst + off, n - off, "; %s", note);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinks ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// RAW (wird vom HAL bei RX/TX gerufen)
|
||||||
|
static void TRACE_SinkRaw(const CanLogFrame &f)
|
||||||
|
{
|
||||||
|
if (g_traceMode != TraceMode::Raw || g_traceOwnerId == 0)
|
||||||
|
return;
|
||||||
|
char buf[TRACE_FMT_BUFSZ];
|
||||||
|
TRACE_FormatLine(buf, sizeof(buf), f, nullptr);
|
||||||
|
trace_emit_line_direct_or_ring(buf);
|
||||||
|
g_traceLines++;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* OBD-Trace-Hook:
|
||||||
|
* Wird von can_obd2.cpp aufgerufen, um einzelne OBD-Frames (ASCII) an den
|
||||||
|
* WebSocket-Trace zu übergeben. Implementierung liegt in webui.cpp.
|
||||||
|
*
|
||||||
|
* @param id CAN-ID (11-bit bei OBD)
|
||||||
|
* @param rx true=Empfangen, false=Gesendet
|
||||||
|
* @param d Datenpointer (kann nullptr sein, wenn dlc==0)
|
||||||
|
* @param dlc Datenlänge (0..8)
|
||||||
|
* @param note optionale Zusatznotiz (z.B. "Mode01 PID 0x0D")
|
||||||
|
*/
|
||||||
|
void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note)
|
||||||
|
{
|
||||||
|
if (g_traceMode != TraceMode::Obd || g_traceOwnerId == 0)
|
||||||
|
return;
|
||||||
|
CanLogFrame f{};
|
||||||
|
f.ts_ms = millis();
|
||||||
|
f.id = id;
|
||||||
|
f.ext = false;
|
||||||
|
f.rx = rx;
|
||||||
|
f.dlc = dlc;
|
||||||
|
if (d && dlc)
|
||||||
|
memcpy(f.data, d, dlc);
|
||||||
|
char buf[TRACE_FMT_BUFSZ];
|
||||||
|
TRACE_FormatLine(buf, sizeof(buf), f, note);
|
||||||
|
trace_emit_line_direct_or_ring(buf);
|
||||||
|
g_traceLines++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner noch da?
|
||||||
|
static inline bool trace_owner_online()
|
||||||
|
{
|
||||||
|
return (g_traceOwnerId != 0) && webSocket.hasClient(g_traceOwnerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pump: gebündelt senden, Backpressure beachten
|
||||||
|
static void TRACE_PumpWs()
|
||||||
|
{
|
||||||
|
if (!g_ring)
|
||||||
|
return; // bei direktem Senden gibt's keinen Ring zu pumpen
|
||||||
|
if (g_traceMode == TraceMode::None || g_traceOwnerId == 0)
|
||||||
|
return;
|
||||||
|
if (!trace_owner_online())
|
||||||
|
return;
|
||||||
|
if (!webSocket.availableForWrite(g_traceOwnerId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
String payload;
|
||||||
|
payload.reserve(2048);
|
||||||
|
int sent = 0;
|
||||||
|
|
||||||
|
// mehrere Zeilen in eine WS-Nachricht
|
||||||
|
while (!ring_empty() && sent < 32)
|
||||||
|
{
|
||||||
|
payload += "TRACELINE;";
|
||||||
|
payload += ring_front();
|
||||||
|
payload += "\n";
|
||||||
|
ring_pop();
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.length())
|
||||||
|
webSocket.text(g_traceOwnerId, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TRACE_StopWs(const char *reason)
|
||||||
|
{
|
||||||
|
if (g_traceMode == TraceMode::None)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Hooks lösen
|
||||||
|
CAN_HAL_SetTraceSink(nullptr);
|
||||||
|
CAN_HAL_EnableRawSniffer(false);
|
||||||
|
|
||||||
|
// Abschlussinfo an Owner (falls noch online)
|
||||||
|
if (trace_owner_online())
|
||||||
|
{
|
||||||
|
String end = "STOPTRACE;mode=";
|
||||||
|
end += (g_traceMode == TraceMode::Raw ? "raw" : "obd");
|
||||||
|
end += ";lines=";
|
||||||
|
end += String(g_traceLines);
|
||||||
|
end += ";drops=";
|
||||||
|
end += String(g_traceDrops);
|
||||||
|
if (reason)
|
||||||
|
{
|
||||||
|
end += ";reason=";
|
||||||
|
end += reason;
|
||||||
|
}
|
||||||
|
webSocket.text(g_traceOwnerId, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ring freigeben
|
||||||
|
ring_free();
|
||||||
|
// Reset
|
||||||
|
g_traceMode = TraceMode::None;
|
||||||
|
g_traceOwnerId = 0;
|
||||||
|
g_traceStartMs = 0;
|
||||||
|
g_traceLines = 0;
|
||||||
|
g_traceDrops = 0;
|
||||||
|
g_head = g_tail = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void TRACE_StartWs(TraceMode m, uint32_t ownerId)
|
||||||
|
{
|
||||||
|
// Falls schon aktiv → erst stoppen (sauber)
|
||||||
|
if (g_traceMode != TraceMode::None)
|
||||||
|
{
|
||||||
|
TRACE_StopWs("restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_traceMode = m;
|
||||||
|
g_traceOwnerId = ownerId;
|
||||||
|
g_traceStartMs = millis();
|
||||||
|
g_traceLines = 0;
|
||||||
|
g_traceDrops = 0;
|
||||||
|
|
||||||
|
// vorhandenen Ring sicherheitshalber freigeben
|
||||||
|
ring_free();
|
||||||
|
|
||||||
|
// versuchen: 64x128 (8KB)
|
||||||
|
if (!ring_alloc(TRACE_DEFAULT_LINES, TRACE_DEFAULT_LINE_MAX))
|
||||||
|
{
|
||||||
|
// Fallback: 48x112 ~ 5.3KB
|
||||||
|
if (!ring_alloc(48, 112))
|
||||||
|
{
|
||||||
|
// Minimal: 32x96 ~ 3KB
|
||||||
|
(void)ring_alloc(32, 96); // wenn das auch scheitert -> g_ring bleibt nullptr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m == TraceMode::Raw)
|
||||||
|
{
|
||||||
|
CAN_HAL_SetTraceSink(TRACE_SinkRaw);
|
||||||
|
CAN_HAL_EnableRawSniffer(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ // Obd
|
||||||
|
CAN_HAL_SetTraceSink(nullptr);
|
||||||
|
CAN_HAL_EnableRawSniffer(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String hdr = "STARTTRACE;mode=";
|
||||||
|
hdr += (m == TraceMode::Raw ? "raw" : "obd");
|
||||||
|
hdr += ";ts=";
|
||||||
|
hdr += String(g_traceStartMs);
|
||||||
|
webSocket.text(ownerId, hdr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WebUI
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initializes the web-based user interface (WebUI) for the ChainLube application.
|
* @brief Initializes the web-based user interface (WebUI) for the ChainLube application.
|
||||||
*
|
*
|
||||||
@@ -139,10 +426,8 @@ void initWebUI()
|
|||||||
{ request->redirect("/index.htm"); });
|
{ request->redirect("/index.htm"); });
|
||||||
webServer.onNotFound(WebserverNotFound_Callback);
|
webServer.onNotFound(WebserverNotFound_Callback);
|
||||||
webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback);
|
webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback);
|
||||||
webServer.on(
|
webServer.on("/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback);
|
||||||
"/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback);
|
webServer.on("/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);
|
||||||
webServer.on(
|
|
||||||
"/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);
|
|
||||||
|
|
||||||
// Start the web server
|
// Start the web server
|
||||||
webServer.begin();
|
webServer.begin();
|
||||||
@@ -218,6 +503,29 @@ void Webserver_Process()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trace pumpen (sicher, backpressure-aware)
|
||||||
|
TRACE_PumpWs();
|
||||||
|
|
||||||
|
// Watchdog: Owner weg → Stop nach 10s (zusätzlich zu Disconnect-Stop)
|
||||||
|
static uint32_t ownerMissingSince = 0;
|
||||||
|
if (g_traceMode != TraceMode::None)
|
||||||
|
{
|
||||||
|
if (!trace_owner_online())
|
||||||
|
{
|
||||||
|
if (ownerMissingSince == 0)
|
||||||
|
ownerMissingSince = millis();
|
||||||
|
if (millis() - ownerMissingSince > 10000)
|
||||||
|
{
|
||||||
|
TRACE_StopWs("owner-timeout");
|
||||||
|
ownerMissingSince = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ownerMissingSince = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,6 +543,11 @@ void Webserver_Process()
|
|||||||
*/
|
*/
|
||||||
void Webserver_Shutdown()
|
void Webserver_Shutdown()
|
||||||
{
|
{
|
||||||
|
// Bei Shutdown Trace beenden
|
||||||
|
if (g_traceMode != TraceMode::None)
|
||||||
|
{
|
||||||
|
TRACE_StopWs("shutdown");
|
||||||
|
}
|
||||||
if (webSocket.count() > 0)
|
if (webSocket.count() > 0)
|
||||||
webSocket.closeAll();
|
webSocket.closeAll();
|
||||||
webServer.end();
|
webServer.end();
|
||||||
@@ -298,7 +611,6 @@ void GetFlashVersion(char *buff, size_t buff_size)
|
|||||||
*/
|
*/
|
||||||
void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
|
void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!index)
|
if (!index)
|
||||||
{
|
{
|
||||||
Debug_pushMessage("Update\n");
|
Debug_pushMessage("Update\n");
|
||||||
@@ -338,88 +650,145 @@ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
void WebserverEERestore_Callback(AsyncWebServerRequest *request,
|
||||||
* @brief Callback function for handling EEPROM restore via the web server.
|
const String &filename,
|
||||||
*
|
size_t index,
|
||||||
* This function is invoked during the EEPROM restore process when a new EEPROM file
|
uint8_t *data,
|
||||||
* is received. It handles the restore process by reading the data from the received file,
|
size_t len,
|
||||||
* deserializing the JSON data, and updating the configuration and persistence data accordingly.
|
bool final)
|
||||||
* If the restore is successful, it triggers a system shutdown.
|
|
||||||
*
|
|
||||||
* @param request Pointer to the AsyncWebServerRequest object.
|
|
||||||
* @param filename The name of the file being restored.
|
|
||||||
* @param index The index of the file being restored.
|
|
||||||
* @param data Pointer to the data buffer.
|
|
||||||
* @param len The length of the data buffer.
|
|
||||||
* @param final Boolean indicating if this is the final chunk of data.
|
|
||||||
*/
|
|
||||||
void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
|
|
||||||
{
|
{
|
||||||
|
constexpr size_t kBufCap = 1536;
|
||||||
|
|
||||||
bool ee_done = false;
|
bool ee_done = false;
|
||||||
static bool validext = false;
|
static bool validext = false;
|
||||||
static char *buffer = NULL;
|
static char *buffer = nullptr;
|
||||||
static uint32_t read_ptr = 0;
|
static uint32_t read_ptr = 0;
|
||||||
DeserializationError error;
|
DeserializationError error;
|
||||||
|
|
||||||
|
// kleines Helferlein zum sicheren Kopieren & Terminieren
|
||||||
|
auto safe_copy = [](char *dst, size_t dst_sz, const char *src)
|
||||||
|
{
|
||||||
|
if (!dst || dst_sz == 0)
|
||||||
|
return;
|
||||||
|
if (!src)
|
||||||
|
{
|
||||||
|
dst[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
strncpy(dst, src, dst_sz - 1);
|
||||||
|
dst[dst_sz - 1] = '\0';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grenzen/Hilfen für Enum-Ranges (Sentinel bevorzugt, sonst *_Elements)
|
||||||
|
const int maxSpeedSrc = static_cast<int>(SPEEDSOURCE_COUNT);
|
||||||
|
const int maxGPSBaud = static_cast<int>(GPSBAUDRATE_COUNT);
|
||||||
|
const int maxCANSrc = static_cast<int>(CANSOURCE_COUNT);
|
||||||
|
|
||||||
if (!index)
|
if (!index)
|
||||||
{
|
{
|
||||||
validext = (filename.indexOf(".ee.json") > -1);
|
validext = (filename.indexOf(".ee.json") > -1);
|
||||||
if (validext)
|
if (validext)
|
||||||
{
|
{
|
||||||
buffer = (char *)malloc(1536);
|
buffer = (char *)malloc(kBufCap);
|
||||||
read_ptr = 0;
|
read_ptr = 0;
|
||||||
if (buffer == NULL)
|
if (!buffer)
|
||||||
|
{
|
||||||
Debug_pushMessage("malloc() failed for EEPROM-Restore\n");
|
Debug_pushMessage("malloc() failed for EEPROM-Restore\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buffer != NULL && len > 0)
|
// Chunked receive mit Cap/Trunkierungsschutz
|
||||||
|
if (buffer && len > 0)
|
||||||
{
|
{
|
||||||
memcpy(buffer + read_ptr, data, len);
|
size_t remain = (read_ptr < kBufCap) ? (kBufCap - read_ptr) : 0;
|
||||||
read_ptr = read_ptr + len;
|
size_t to_copy = (len <= remain) ? len : remain;
|
||||||
|
if (to_copy > 0)
|
||||||
|
{
|
||||||
|
memcpy(buffer + read_ptr, data, to_copy);
|
||||||
|
read_ptr += to_copy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug_pushMessage("EEPROM-Restore input exceeds buffer, truncating\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (final)
|
if (final)
|
||||||
{
|
{
|
||||||
if (buffer != NULL)
|
if (buffer)
|
||||||
{
|
{
|
||||||
// Ensure zero-termination just in case
|
// Null-terminieren
|
||||||
if (read_ptr >= 1536)
|
if (read_ptr == kBufCap)
|
||||||
read_ptr = 1535;
|
read_ptr = kBufCap - 1;
|
||||||
buffer[read_ptr] = '\0';
|
buffer[read_ptr] = '\0';
|
||||||
|
|
||||||
Serial.print(buffer);
|
// Parse
|
||||||
JsonDocument json;
|
JsonDocument json; // entspricht deinem bisherigen Stil
|
||||||
error = deserializeJson(json, buffer);
|
error = deserializeJson(json, buffer);
|
||||||
if (error)
|
if (error)
|
||||||
{
|
{
|
||||||
Debug_pushMessage("deserializeJson() failed: %s\n", error.f_str());
|
Debug_pushMessage("deserializeJson() failed: %s\n", error.f_str());
|
||||||
}
|
}
|
||||||
else
|
else if (validext)
|
||||||
{
|
{
|
||||||
|
// ---- Konfiguration sicher in RAM übernehmen ----
|
||||||
|
// clamp-Helfer passend zu deinen Sanity-Grenzen
|
||||||
|
auto clamp_u32 = [](uint32_t v, uint32_t lo, uint32_t hi)
|
||||||
|
{ return (v < lo) ? lo : (v > hi ? hi : v); };
|
||||||
|
auto clamp_u16 = [](uint16_t v, uint16_t lo, uint16_t hi)
|
||||||
|
{ return (v < lo) ? lo : (v > hi ? hi : v); };
|
||||||
|
auto clamp_u8 = [](uint8_t v, uint8_t lo, uint8_t hi)
|
||||||
|
{ return (v < lo) ? lo : (v > hi ? hi : v); };
|
||||||
|
|
||||||
LubeConfig.DistancePerLube_Default = json["config"]["DistancePerLube_Default"].as<uint32_t>();
|
// config.*
|
||||||
LubeConfig.DistancePerLube_Rain = json["config"]["DistancePerLube_Rain"].as<uint32_t>();
|
LubeConfig.DistancePerLube_Default = clamp_u32(json["config"]["DistancePerLube_Default"].as<uint32_t>(), 0, 50000);
|
||||||
LubeConfig.tankCapacity_ml = json["config"]["tankCapacity_ml"].as<uint32_t>();
|
LubeConfig.DistancePerLube_Rain = clamp_u32(json["config"]["DistancePerLube_Rain"].as<uint32_t>(), 0, 50000);
|
||||||
LubeConfig.amountPerDose_microL = json["config"]["amountPerDose_microL"].as<uint32_t>();
|
LubeConfig.tankCapacity_ml = clamp_u32(json["config"]["tankCapacity_ml"].as<uint32_t>(), 0, 5000);
|
||||||
LubeConfig.TankRemindAtPercentage = json["config"]["TankRemindAtPercentage"].as<uint8_t>();
|
LubeConfig.amountPerDose_microL = clamp_u32(json["config"]["amountPerDose_microL"].as<uint32_t>(), 0, 100);
|
||||||
LubeConfig.PulsePerRevolution = json["config"]["PulsePerRevolution"].as<uint8_t>();
|
LubeConfig.TankRemindAtPercentage = clamp_u8(json["config"]["TankRemindAtPercentage"].as<uint8_t>(), 0, 100);
|
||||||
LubeConfig.TireWidth_mm = json["config"]["TireWidth_mm"].as<uint32_t>();
|
LubeConfig.PulsePerRevolution = clamp_u8(json["config"]["PulsePerRevolution"].as<uint8_t>(), 0, 255);
|
||||||
LubeConfig.TireWidthHeight_Ratio = json["config"]["TireWidthHeight_Ratio"].as<uint32_t>();
|
LubeConfig.TireWidth_mm = clamp_u32(json["config"]["TireWidth_mm"].as<uint32_t>(), 0, 500);
|
||||||
LubeConfig.RimDiameter_Inch = json["config"]["RimDiameter_Inch"].as<uint32_t>();
|
LubeConfig.TireWidthHeight_Ratio = clamp_u32(json["config"]["TireWidthHeight_Ratio"].as<uint32_t>(), 0, 150);
|
||||||
LubeConfig.DistancePerRevolution_mm = json["config"]["DistancePerRevolution_mm"].as<uint32_t>();
|
LubeConfig.RimDiameter_Inch = clamp_u32(json["config"]["RimDiameter_Inch"].as<uint32_t>(), 0, 30);
|
||||||
LubeConfig.BleedingPulses = json["config"]["BleedingPulses"].as<uint16_t>();
|
LubeConfig.DistancePerRevolution_mm = clamp_u32(json["config"]["DistancePerRevolution_mm"].as<uint32_t>(), 0, 10000);
|
||||||
LubeConfig.SpeedSource = (SpeedSource_t)json["config"]["SpeedSource"].as<int>();
|
LubeConfig.BleedingPulses = clamp_u16(json["config"]["BleedingPulses"].as<uint16_t>(), 0, 1000);
|
||||||
LubeConfig.GPSBaudRate = (GPSBaudRate_t)json["config"]["GPSBaudRate"].as<int>();
|
LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as<uint16_t>();
|
||||||
LubeConfig.CANSource = (CANSource_t)json["config"]["CANSource"].as<int>();
|
LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>();
|
||||||
LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as<bool>();
|
LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as<bool>();
|
||||||
LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as<uint8_t>();
|
LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as<uint8_t>();
|
||||||
LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>();
|
LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>();
|
||||||
strncpy(LubeConfig.wifi_ap_ssid, json["config"]["wifi_ap_ssid"].as<const char *>(), sizeof(LubeConfig.wifi_ap_ssid));
|
|
||||||
strncpy(LubeConfig.wifi_ap_password, json["config"]["wifi_ap_password"].as<const char *>(), sizeof(LubeConfig.wifi_ap_password));
|
|
||||||
strncpy(LubeConfig.wifi_client_ssid, json["config"]["wifi_client_ssid"].as<const char *>(), sizeof(LubeConfig.wifi_client_ssid));
|
|
||||||
strncpy(LubeConfig.wifi_client_password, json["config"]["wifi_client_password"].as<const char *>(), sizeof(LubeConfig.wifi_client_password));
|
|
||||||
|
|
||||||
|
// Enums nur nach Range-Check übernehmen
|
||||||
|
{
|
||||||
|
int v = json["config"]["SpeedSource"].as<int>();
|
||||||
|
if (v >= 0 && v < maxSpeedSrc)
|
||||||
|
LubeConfig.SpeedSource = (SpeedSource_t)v;
|
||||||
|
else
|
||||||
|
Debug_pushMessage("Restore: invalid SpeedSource=%d\n", v);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
int v = json["config"]["GPSBaudRate"].as<int>();
|
||||||
|
if (v >= 0 && v < maxGPSBaud)
|
||||||
|
LubeConfig.GPSBaudRate = (GPSBaudRate_t)v;
|
||||||
|
else
|
||||||
|
Debug_pushMessage("Restore: invalid GPSBaudRate=%d\n", v);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
int v = json["config"]["CANSource"].as<int>();
|
||||||
|
if (v >= 0 && v < maxCANSrc)
|
||||||
|
LubeConfig.CANSource = (CANSource_t)v;
|
||||||
|
else
|
||||||
|
Debug_pushMessage("Restore: invalid CANSource=%d\n", v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings sicher kopieren (0-terminiert)
|
||||||
|
safe_copy(LubeConfig.wifi_ap_ssid, sizeof(LubeConfig.wifi_ap_ssid), json["config"]["wifi_ap_ssid"]);
|
||||||
|
safe_copy(LubeConfig.wifi_ap_password, sizeof(LubeConfig.wifi_ap_password), json["config"]["wifi_ap_password"]);
|
||||||
|
safe_copy(LubeConfig.wifi_client_ssid, sizeof(LubeConfig.wifi_client_ssid), json["config"]["wifi_client_ssid"]);
|
||||||
|
safe_copy(LubeConfig.wifi_client_password, sizeof(LubeConfig.wifi_client_password), json["config"]["wifi_client_password"]);
|
||||||
|
|
||||||
|
// persis.*
|
||||||
PersistenceData.writeCycleCounter = json["persis"]["writeCycleCounter"].as<uint16_t>();
|
PersistenceData.writeCycleCounter = json["persis"]["writeCycleCounter"].as<uint16_t>();
|
||||||
PersistenceData.tankRemain_microL = json["persis"]["tankRemain_microL"].as<uint32_t>();
|
PersistenceData.tankRemain_microL = json["persis"]["tankRemain_microL"].as<uint32_t>();
|
||||||
PersistenceData.TravelDistance_highRes_mm = json["persis"]["TravelDistance_highRes_mm"].as<uint32_t>();
|
PersistenceData.TravelDistance_highRes_mm = json["persis"]["TravelDistance_highRes_mm"].as<uint32_t>();
|
||||||
@@ -427,24 +796,30 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
|
|||||||
PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>();
|
PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>();
|
||||||
PersistenceData.checksum = json["persis"]["checksum"].as<uint32_t>();
|
PersistenceData.checksum = json["persis"]["checksum"].as<uint32_t>();
|
||||||
|
|
||||||
|
uint32_t sanity = ConfigSanityCheck(true);
|
||||||
|
if (sanity > 0)
|
||||||
|
{
|
||||||
|
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity);
|
||||||
|
Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity);
|
||||||
|
}
|
||||||
|
|
||||||
ee_done = true;
|
ee_done = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (buffer)
|
|
||||||
{
|
|
||||||
free(buffer);
|
free(buffer);
|
||||||
buffer = NULL;
|
buffer = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots");
|
// Browser zurückleiten & ggf. Shutdown
|
||||||
|
AsyncWebServerResponse *response =
|
||||||
|
request->beginResponse(302, "text/plain", "Please wait while the device reboots");
|
||||||
response->addHeader("Refresh", "20");
|
response->addHeader("Refresh", "20");
|
||||||
response->addHeader("Location", "/");
|
response->addHeader("Location", "/");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
|
|
||||||
if (ee_done)
|
if (ee_done)
|
||||||
{
|
{
|
||||||
Debug_pushMessage("Update complete\n");
|
Debug_pushMessage("EEPROM restore complete\n");
|
||||||
globals.systemStatus = sysStat_Shutdown;
|
globals.systemStatus = sysStat_Shutdown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,8 +854,8 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
|
|||||||
generateJsonObject_PersistenceData(persis);
|
generateJsonObject_PersistenceData(persis);
|
||||||
|
|
||||||
JsonObject eepart = json["eepart"].to<JsonObject>();
|
JsonObject eepart = json["eepart"].to<JsonObject>();
|
||||||
sprintf(buffer, "0x%04X", globals.eePersistanceAdress);
|
sprintf(buffer, "0x%04X", globals.eePersistenceAddress);
|
||||||
eepart["PersistanceAddress"] = buffer;
|
eepart["PersistenceAddress"] = buffer;
|
||||||
|
|
||||||
serializeJsonPretty(json, *response);
|
serializeJsonPretty(json, *response);
|
||||||
|
|
||||||
@@ -489,6 +864,10 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
|
|||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WebSocket Handling
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback function for handling WebSocket events.
|
* @brief Callback function for handling WebSocket events.
|
||||||
*
|
*
|
||||||
@@ -519,10 +898,17 @@ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *clien
|
|||||||
}
|
}
|
||||||
case WS_EVT_DISCONNECT:
|
case WS_EVT_DISCONNECT:
|
||||||
Debug_pushMessage("WebSocket client #%u disconnected\n", client->id());
|
Debug_pushMessage("WebSocket client #%u disconnected\n", client->id());
|
||||||
|
// Falls Owner: Trace sofort stoppen
|
||||||
|
if (g_traceOwnerId == client->id())
|
||||||
|
TRACE_StopWs("owner-disconnect");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_EVT_DATA:
|
case WS_EVT_DATA:
|
||||||
|
g_wsCurrentClient = client; // für HandleMessage → Owner-ID
|
||||||
Websocket_HandleMessage(arg, data, len);
|
Websocket_HandleMessage(arg, data, len);
|
||||||
|
g_wsCurrentClient = nullptr;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_EVT_PONG:
|
case WS_EVT_PONG:
|
||||||
case WS_EVT_ERROR:
|
case WS_EVT_ERROR:
|
||||||
break;
|
break;
|
||||||
@@ -549,19 +935,69 @@ void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len)
|
|||||||
memcpy(buf.get(), data, len);
|
memcpy(buf.get(), data, len);
|
||||||
buf[len] = '\0';
|
buf[len] = '\0';
|
||||||
|
|
||||||
Debug_pushMessage("Websocket-Message (len: %d): %s\n", (int)len, buf.get());
|
const uint32_t senderId = g_wsCurrentClient ? g_wsCurrentClient->id() : 0;
|
||||||
|
Debug_pushMessage("Websocket-Message from #%u (len: %d): %s\n", (unsigned)senderId, (int)len, buf.get());
|
||||||
|
|
||||||
|
// Steuerkommandos für Trace hier direkt behandeln (brauchen senderId)
|
||||||
if (strncmp(buf.get(), "btn-", 4) == 0)
|
if (strncmp(buf.get(), "btn-", 4) == 0)
|
||||||
{
|
{
|
||||||
|
// Format: "btn-<identifier>[:<value>]"
|
||||||
|
char identifier[32];
|
||||||
|
char value[64];
|
||||||
|
parseWebsocketString((char *)buf.get() + 4, identifier, sizeof(identifier), value, sizeof(value));
|
||||||
|
|
||||||
|
if (strcmp(identifier, "trace-start") == 0)
|
||||||
|
{
|
||||||
|
// Lock: Nur starten, wenn nicht aktiv
|
||||||
|
if (g_traceMode != TraceMode::None)
|
||||||
|
{
|
||||||
|
String busy = "TRACEBUSY;owner=";
|
||||||
|
busy += String(g_traceOwnerId);
|
||||||
|
if (senderId)
|
||||||
|
webSocket.text(senderId, busy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TraceMode m = TraceMode::None;
|
||||||
|
if (!strcmp(value, "raw"))
|
||||||
|
m = TraceMode::Raw;
|
||||||
|
else if (!strcmp(value, "obd"))
|
||||||
|
m = TraceMode::Obd;
|
||||||
|
|
||||||
|
if (m == TraceMode::None)
|
||||||
|
{
|
||||||
|
if (senderId)
|
||||||
|
webSocket.text(senderId, "TRACEERROR;msg=mode-missing");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TRACE_StartWs(m, senderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (strcmp(identifier, "trace-stop") == 0)
|
||||||
|
{
|
||||||
|
// Stop darf jeder
|
||||||
|
TRACE_StopWs("user-stop");
|
||||||
|
// optional: ACK an Sender (Owner bekommt STOPTRACE ohnehin)
|
||||||
|
if (senderId)
|
||||||
|
webSocket.text(senderId, "TRACEACK;cmd=stop");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sonst: in "normalen" Button-Handler
|
||||||
Websocket_HandleButtons((uint8_t *)buf.get() + 4);
|
Websocket_HandleButtons((uint8_t *)buf.get() + 4);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (strncmp(buf.get(), "set-", 4) == 0)
|
else if (strncmp(buf.get(), "set-", 4) == 0)
|
||||||
{
|
{
|
||||||
Websocket_HandleSettings((uint8_t *)buf.get() + 4);
|
Websocket_HandleSettings((uint8_t *)buf.get() + 4);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", buf.get());
|
Debug_pushMessage("Got unknown Websocket-Message '%s'\n", buf.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -648,24 +1084,24 @@ void Websocket_HandleSettings(uint8_t *data)
|
|||||||
}
|
}
|
||||||
else if (strcmp(identifier, "speedsource") == 0)
|
else if (strcmp(identifier, "speedsource") == 0)
|
||||||
{
|
{
|
||||||
int index = findIndexByString(value, SpeedSourceString, (int)SpeedSourceString_Elements);
|
int index = findIndexByString(value, SpeedSourceString, (int)SPEEDSOURCE_COUNT);
|
||||||
if (validIndex(index, (int)SpeedSourceString_Elements))
|
if (validIndex(index, (int)SPEEDSOURCE_COUNT))
|
||||||
speedsourcePreselect = (SpeedSource_t)index;
|
speedsourcePreselect = (SpeedSource_t)index;
|
||||||
else
|
else
|
||||||
Debug_pushMessage("Invalid speedsource '%s'\n", value);
|
Debug_pushMessage("Invalid speedsource '%s'\n", value);
|
||||||
}
|
}
|
||||||
else if (strcmp(identifier, "cansource") == 0)
|
else if (strcmp(identifier, "cansource") == 0)
|
||||||
{
|
{
|
||||||
int index = findIndexByString(value, CANSourceString, (int)CANSourceString_Elements);
|
int index = findIndexByString(value, CANSourceString, (int)CANSOURCE_COUNT);
|
||||||
if (validIndex(index, (int)CANSourceString_Elements))
|
if (validIndex(index, (int)CANSOURCE_COUNT))
|
||||||
LubeConfig.CANSource = (CANSource_t)index;
|
LubeConfig.CANSource = (CANSource_t)index;
|
||||||
else
|
else
|
||||||
Debug_pushMessage("Invalid cansource '%s'\n", value);
|
Debug_pushMessage("Invalid cansource '%s'\n", value);
|
||||||
}
|
}
|
||||||
else if (strcmp(identifier, "gpsbaud") == 0)
|
else if (strcmp(identifier, "gpsbaud") == 0)
|
||||||
{
|
{
|
||||||
int index = findIndexByString(value, GPSBaudRateString, (int)GPSBaudRateString_Elements);
|
int index = findIndexByString(value, GPSBaudRateString, (int)GPSBAUDRATE_COUNT);
|
||||||
if (validIndex(index, (int)GPSBaudRateString_Elements))
|
if (validIndex(index, (int)GPSBAUDRATE_COUNT))
|
||||||
LubeConfig.GPSBaudRate = (GPSBaudRate_t)index;
|
LubeConfig.GPSBaudRate = (GPSBaudRate_t)index;
|
||||||
else
|
else
|
||||||
Debug_pushMessage("Invalid gpsbaud '%s'\n", value);
|
Debug_pushMessage("Invalid gpsbaud '%s'\n", value);
|
||||||
@@ -790,7 +1226,6 @@ void Websocket_RefreshClientData_DTCs(uint32_t client_id)
|
|||||||
*/
|
*/
|
||||||
void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
|
void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (send_mapping)
|
if (send_mapping)
|
||||||
{
|
{
|
||||||
if (client_id > 0)
|
if (client_id > 0)
|
||||||
@@ -800,12 +1235,11 @@ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
|
|||||||
}
|
}
|
||||||
|
|
||||||
String temp = "STATUS:";
|
String temp = "STATUS:";
|
||||||
|
temp.concat(String(ToString(globals.systemStatus)) + ";");
|
||||||
temp.concat(String(nz(globals.systemStatustxt)) + ";");
|
|
||||||
|
|
||||||
// Guard against division by zero (capacity==0)
|
// Guard against division by zero (capacity==0)
|
||||||
uint32_t cap = LubeConfig.tankCapacity_ml;
|
uint32_t cap = LubeConfig.tankCapacity_ml;
|
||||||
uint32_t remain10 = (PersistenceData.tankRemain_microL / 10); // keep your original math
|
uint32_t remain10 = (PersistenceData.tankRemain_microL / 10);
|
||||||
uint32_t ratio = (cap > 0) ? (remain10 / cap) : 0;
|
uint32_t ratio = (cap > 0) ? (remain10 / cap) : 0;
|
||||||
temp.concat(String(ratio) + ";");
|
temp.concat(String(ratio) + ";");
|
||||||
|
|
||||||
@@ -856,26 +1290,26 @@ void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping)
|
|||||||
temp += String(LubeConfig.RimDiameter_Inch) + ";";
|
temp += String(LubeConfig.RimDiameter_Inch) + ";";
|
||||||
|
|
||||||
// speedsource + Optionen
|
// speedsource + Optionen
|
||||||
temp += tableStr(SpeedSourceString, (int)LubeConfig.SpeedSource, (int)SpeedSourceString_Elements) + ";";
|
temp += String(ToString(LubeConfig.SpeedSource)) + ";";
|
||||||
{
|
{
|
||||||
String csv;
|
String csv;
|
||||||
appendCsv(csv, SpeedSourceString, SpeedSourceString_Elements);
|
appendCsv(csv, SpeedSourceString, SPEEDSOURCE_COUNT);
|
||||||
temp += csv + ";";
|
temp += csv + ";";
|
||||||
}
|
}
|
||||||
|
|
||||||
// gpsbaud + Optionen
|
// gpsbaud + Optionen
|
||||||
temp += tableStr(GPSBaudRateString, (int)LubeConfig.GPSBaudRate, (int)GPSBaudRateString_Elements) + ";";
|
temp += String(ToString(LubeConfig.GPSBaudRate)) + ";";
|
||||||
{
|
{
|
||||||
String csv;
|
String csv;
|
||||||
appendCsv(csv, GPSBaudRateString, GPSBaudRateString_Elements);
|
appendCsv(csv, GPSBaudRateString, GPSBAUDRATE_COUNT);
|
||||||
temp += csv + ";";
|
temp += csv + ";";
|
||||||
}
|
}
|
||||||
|
|
||||||
// cansource + Optionen
|
// cansource + Optionen
|
||||||
temp += tableStr(CANSourceString, (int)LubeConfig.CANSource, (int)CANSourceString_Elements) + ";";
|
temp += String(ToString(LubeConfig.CANSource)) + ";";
|
||||||
{
|
{
|
||||||
String csv;
|
String csv;
|
||||||
appendCsv(csv, CANSourceString, CANSourceString_Elements);
|
appendCsv(csv, CANSourceString, CANSOURCE_COUNT);
|
||||||
temp += csv + ";";
|
temp += csv + ";";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,4 +1456,4 @@ void Websocket_PushNotification(String Message, NotificationType_t type)
|
|||||||
}
|
}
|
||||||
webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
|
webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
|
||||||
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str());
|
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str());
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user