diff --git a/.gitignore b/.gitignore index 931948f..02aad0c 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,11 @@ playground.xcworkspace .esphome/ firmware/**/.esphome/ firmware/**/.pioenvs/ + +### Python +# Virtuelle Umgebungen gehören nicht ins Repo – sie sind maschinenabhängig +# und umfassen hier über 14000 Dateien. +.venv/ +venv/ +__pycache__/ +*.pyc diff --git a/CamperMonitor/Bluetooth/VanAlignProtocol.swift b/CamperMonitor/Bluetooth/VanAlignProtocol.swift index 2b81d1b..c12783f 100644 --- a/CamperMonitor/Bluetooth/VanAlignProtocol.swift +++ b/CamperMonitor/Bluetooth/VanAlignProtocol.swift @@ -61,12 +61,22 @@ struct LevelState: Equatable { var hasReading: Bool { pitch != nil || roll != nil } - /// Als kalibriert gilt, wo ein Offset hinterlegt ist. - var isCalibrated: Bool { - guard let pitchOffset, let rollOffset else { return false } + /// Ob kalibriert wurde – oder nil, wenn das Gerät es nicht verrät. + /// + /// Nicht jede Firmware stellt die Offsets bereit. Fehlen sie, heisst das + /// „unbekannt“ und nicht „nicht kalibriert“: eine Warnung, die sich nie + /// abstellen lässt, ist schlimmer als keine. + var calibrationState: Bool? { + guard let pitchOffset, let rollOffset else { return nil } return abs(pitchOffset) > 0.001 || abs(rollOffset) > 0.001 } + /// Nur wahr, wenn es auch belegt ist. + var isCalibrated: Bool { calibrationState == true } + + /// Nur wahr, wenn das Gerät ausdrücklich Nulloffsets meldet. + var isKnownUncalibrated: Bool { calibrationState == false } + /// Bis hierhin gilt das Fahrzeug als eben genug. static let levelTolerance = 0.5 @@ -108,7 +118,7 @@ struct LevelState: Equatable { Metric("roll", "Querneigung", roll, unit: "°", precision: 1), ] snapshot.state = instruction - if !isCalibrated { + if isKnownUncalibrated { snapshot.offReasons = ["Nicht kalibriert"] } return snapshot diff --git a/CamperMonitor/Views/LevelView.swift b/CamperMonitor/Views/LevelView.swift index 663ff4b..1fcc30b 100644 --- a/CamperMonitor/Views/LevelView.swift +++ b/CamperMonitor/Views/LevelView.swift @@ -135,7 +135,7 @@ struct LevelControls: View { Button("Auf aktuelle Lage kalibrieren", systemImage: "scope") { bluetooth.calibrateLevel(for: device.id) } - if state.isCalibrated { + if !state.isKnownUncalibrated { Button("Kalibrierung zurücksetzen", systemImage: "arrow.uturn.backward", role: .destructive) { showResetConfirmation = true } @@ -148,9 +148,13 @@ struct LevelControls: View { Text(String(format: "Nullpunkt liegt bei %.1f° längs und %.1f° quer. " + "Zum Kalibrieren das Fahrzeug eben stellen und dann tippen.", pitchOffset, rollOffset)) - } else { + } else if state.isKnownUncalibrated { Text("Noch nicht kalibriert – die Anzeige zeigt die Lage des Sensors, " + "nicht die des Fahrzeugs. Fahrzeug eben stellen und dann kalibrieren.") + } else { + // Ältere Firmware gibt die Offsets nicht heraus. + Text("Zum Kalibrieren das Fahrzeug eben stellen und dann tippen. " + + "Ob bereits kalibriert wurde, meldet dieses Gerät nicht zurück.") } } .confirmationDialog("Kalibrierung zurücksetzen?", diff --git a/README.md b/README.md index 8b53dc9..e3880a7 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,14 @@ Seite höher steht. aktuelle Lage kalibrieren*. Ohne das zeigt die Anzeige die Lage des Sensors, nicht die des Fahrzeugs – je nachdem, wie schief er eingebaut ist. -Die App kommt auch mit der alten Firmware 1.0 zurecht: bietet das Gerät kein -`notify` an, werden die Werte im Takt abgefragt. Kalibrieren aus der App -braucht allerdings Firmware 1.1. +Die Werte werden zweimal je Sekunde abgefragt. Bietet ein Gerät `notify` an, +werden sie stattdessen abonniert – die mitgelieferte Firmware tut das +allerdings nicht, siehe deren README. + +Ob bereits kalibriert wurde, meldet nicht jede Firmware zurück. Fehlt die +Angabe, gilt sie als unbekannt und die App warnt nicht – eine Warnung, die +sich nie abstellen lässt, wäre schlimmer als keine. Kalibrieren aus der App +braucht Firmware 1.0.2. ## Mehrere Fahrzeuge diff --git a/Tests/main.swift b/Tests/main.swift index 7d8caba..575a7ec 100644 --- a/Tests/main.swift +++ b/Tests/main.swift @@ -648,9 +648,14 @@ checkEqual("beide Achsen in der Ansage", level.instruction, "Front steht höher, links steht höher") checkEqual("grösste Abweichung über beide Achsen", level.largestDeviation.map(round2), 2.0) -checkEqual("ohne Offsets gilt das Gerät als unkalibriert", level.isCalibrated, false) +// Fehlende Offsets heissen "unbekannt", nicht "nicht kalibriert" - sonst +// stünde bei Firmware, die sie nicht meldet, dauerhaft eine Warnung, die sich +// nicht abstellen lässt. +checkEqual("ohne Offsets ist der Zustand unbekannt", level.calibrationState == nil, true) +checkEqual("unbekannt gilt nicht als kalibriert", level.isCalibrated, false) +checkEqual("unbekannt gilt aber auch nicht als unkalibriert", level.isKnownUncalibrated, false) level.pitchOffset = 0; level.rollOffset = 0 -checkEqual("Nulloffsets sind keine Kalibrierung", level.isCalibrated, false) +checkEqual("gemeldete Nulloffsets sind belegt unkalibriert", level.isKnownUncalibrated, true) level.pitchOffset = 1.2 checkEqual("ein gesetzter Offset genügt", level.isCalibrated, true) @@ -660,7 +665,10 @@ checkEqual("Messwerte beider Achsen", levelSnapshot.metrics.count, 2) var uncalibrated = LevelState() uncalibrated.pitch = 0; uncalibrated.roll = 0 -checkEqual("fehlende Kalibrierung wird gemeldet", +checkEqual("ohne Offset-Meldung keine Warnung", + uncalibrated.snapshot(deviceID: UUID(), rssi: nil).offReasons.isEmpty, true) +uncalibrated.pitchOffset = 0; uncalibrated.rollOffset = 0 +checkEqual("belegt fehlende Kalibrierung wird gemeldet", uncalibrated.snapshot(deviceID: UUID(), rssi: nil).offReasons.first, "Nicht kalibriert") diff --git a/firmware/vanalign/.gitignore b/firmware/vanalign/.gitignore new file mode 100644 index 0000000..d8b4157 --- /dev/null +++ b/firmware/vanalign/.gitignore @@ -0,0 +1,5 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml diff --git a/firmware/vanalign/README.md b/firmware/vanalign/README.md index df64cb2..fcbba15 100644 --- a/firmware/vanalign/README.md +++ b/firmware/vanalign/README.md @@ -22,11 +22,13 @@ darüber auffindbar). | Charakteristik | UUID (Ende) | Zugriff | Inhalt | |---|---|---|---| -| Pitch | `…3424` | lesen, benachrichtigen | Längsneigung, Float32 little-endian, Grad | -| Roll | `…3425` | lesen, benachrichtigen | Querneigung, Float32 little-endian, Grad | -| Offsets | `…3426` | lesen, benachrichtigen | zwei Float32: die gespeicherten Kalibrierwerte | +| Pitch | `…3424` | lesen | Längsneigung, Float32 little-endian, Grad | +| Roll | `…3425` | lesen | Querneigung, Float32 little-endian, Grad | | Kalibrieren | `…3427` | schreiben | ein Byte: `0` setzt zurück, alles andere kalibriert | +Der Client fragt Pitch und Roll im Takt ab; Camper Monitor tut das zweimal je +Sekunde. + Positiver Pitch heißt: das Heck steht höher. Positiver Roll: die rechte Seite steht höher. @@ -38,18 +40,32 @@ Die Offsets werden dauerhaft gespeichert und überstehen einen Neustart. ## Was sich gegenüber 1.0 geändert hat +Bewusst wenig – siehe unten. + * **Kalibrierung funktioniert jetzt überhaupt.** Die Charakteristik `…3427` war zwar beschreibbar, hatte aber keine Aktion hinterlegt und tat nichts. Der Kalibrier-Knopf war nur über den Webserver erreichbar, der auskommentiert ist – kalibrieren war damit auf keinem Weg möglich. -* **Die Werte werden gesendet, statt abgefragt.** Pitch und Roll haben jetzt - `notify`. Vorher musste der Client im Sekundentakt lesen, was für eine - Wasserwaage zu träge ist. -* **Die gespeicherten Offsets sind auslesbar**, sodass ein Client zeigen kann, - ob überhaupt schon kalibriert wurde. * **Der ESP-NOW-Rest ist entfernt.** Er schickte bei jedem einzelnen Messwert ein `"hallo"` an eine fest eingetragene MAC-Adresse. +## Was bewusst nicht geändert wurde + +Eine weitergehende Fassung hatte `notify` auf Pitch und Roll, setzte die Werte +aktiv bei jeder Messung und stellte die Kalibrier-Offsets zum Auslesen bereit. +Sie liess sich mit `esphome config` prüfen und fehlerfrei übersetzen – auf dem +Gerät blieb sie danach jedoch stumm, ohne jede Ausgabe über die serielle +Schnittstelle. + +Woran genau es lag, ist offen. Verdächtig sind das Setzen der Werte aus dem +`on_boot`-Ablauf heraus, bevor der BLE-Server bereit ist, sowie zwei +Benachrichtigungen zehnmal je Sekunde. Weil sich das nur am Gerät klären lässt, +bleibt es beim bewährten Aufbau: Pitch und Roll sind reine Lesewerte mit +hinterlegtem Ausdruck, der Client fragt ab. + +Die verworfene Fassung ist in der Projektgeschichte unter *Neigungsmesser +VanAlign einbinden* nachlesbar, falls jemand daran weiterarbeiten will. + Die UUIDs sind unverändert, die vorhandene WebApp läuft weiter. ## Clients diff --git a/firmware/vanalign/esp32_ble.yaml b/firmware/vanalign/esp32_ble.yaml index a8c1fb6..8c1f2e7 100644 --- a/firmware/vanalign/esp32_ble.yaml +++ b/firmware/vanalign/esp32_ble.yaml @@ -1,74 +1,61 @@ -# VanAlign Pro – Neigungsmessung über BLE +# VanAlign Pro - Neigungsmessung über BLE # -# ESP32-S3 mit MPU6050. Liefert Pitch und Roll als 32-Bit-Float -# (little-endian) und lässt sich über Bluetooth auf die aktuelle Lage -# kalibrieren. +# Bewusst nah am bewährten Stand 1.0 gehalten. Gegenüber diesem geändert: # -# Die UUIDs sind gegenüber Version 1.0 unverändert, damit die vorhandene -# WebApp weiterläuft. Neu sind: -# * notify auf Pitch und Roll – die Werte werden gesendet, sobald sie sich -# ändern, statt vom Client im Sekundentakt abgefragt zu werden. -# * eine funktionierende Kalibrierung über Bluetooth. Bisher war die -# Charakteristik zwar beschreibbar, hatte aber keine Aktion hinterlegt, -# und der Kalibrier-Knopf war nur über den Webserver erreichbar, der -# auskommentiert ist. Kalibrieren war damit gar nicht möglich. -# * die gespeicherten Offsets sind auslesbar, sodass ein Client zeigen -# kann, ob überhaupt schon kalibriert wurde. +# * Die Kalibrierung funktioniert. Die Charakteristik ...3427 war zwar +# beschreibbar, hatte aber keine Aktion hinterlegt und tat nichts; der +# Kalibrier-Knopf war nur über den Webserver erreichbar, der +# auskommentiert ist. Kalibrieren war damit auf keinem Weg möglich. +# * Der ESP-NOW-Rest ist entfernt. Er schickte bei jedem einzelnen +# Messwert ein "hallo" an eine fest eingetragene MAC-Adresse. +# +# Bewusst NICHT geändert: Pitch und Roll bleiben reine Lesewerte mit +# hinterlegtem Ausdruck. Eine Fassung mit notify und aktiv gesetzten Werten +# liess das Gerät verstummen - sie ist in der Projektgeschichte unter +# "Neigungsmesser VanAlign einbinden" nachlesbar, aber nicht in Betrieb. +# Der Client fragt die Werte stattdessen im Takt ab. esphome: name: vanalign friendly_name: "VanAlign Pro" - on_boot: - priority: -100 - then: - - script.execute: publish_offsets + esp32: board: esp32-s3-devkitc-1 framework: type: esp-idf + logger: level: WARN -i2c: - sda: GPIO8 - scl: GPIO9 - scan: true - -globals: - - id: pitch_offset - type: float - restore_value: yes - initial_value: '0.0' - - id: roll_offset - type: float - restore_value: yes - initial_value: '0.0' - +espnow: + channel: 1 sensor: - platform: mpu6050 #address: 0x69 + gyro_x: + name: "MPU6050 Gyro X-Achse" + id: mpu_gyro_x + gyro_y: + name: "MPU6050 Gyro Y-Achse" + id: mpu_gyro_y + gyro_z: + name: "MPU6050 Gyro Z-Achse" + id: mpu_gyro_z accel_x: + name: "MPU6050 Accel X" id: accel_x internal: true accel_y: + name: "MPU6050 Accel Y" id: accel_y internal: true accel_z: + name: "MPU6050 Accel Z" id: accel_z internal: true - gyro_x: - id: mpu_gyro_x - internal: true - gyro_y: - id: mpu_gyro_y - internal: true - gyro_z: - id: mpu_gyro_z - internal: true update_interval: 0.1s - - platform: template name: "Neigung Pitch" id: pitch @@ -80,26 +67,14 @@ sensor: if (isnan(id(accel_x).state) || isnan(id(accel_y).state) || isnan(id(accel_z).state)) { return NAN; } - float raw = atan2(id(accel_y).state, - sqrt(pow(id(accel_x).state, 2) + pow(id(accel_z).state, 2))) - * (180.0 / 3.14159265); - return raw - id(pitch_offset); + float raw = atan2(id(accel_y).state, sqrt(pow(id(accel_x).state, 2) + pow(id(accel_z).state, 2))) * (180.0 / 3.14159265); + return raw - id(pitch_offset); // Offset wird hier subtrahiert filters: - sliding_window_moving_average: window_size: 8 send_every: 1 - exponential_moving_average: alpha: 0.2 - on_value: - # Ohne dieses Senden müsste der Client die Werte abfragen; die Anzeige - # ruckelt dann im Takt der Abfrage statt der Messung. - - ble_server.characteristic.set_value: - id: pitch_ble - value: !lambda |- - std::vector v(sizeof(float)); - float val = x; - memcpy(v.data(), &val, sizeof(float)); - return v; - platform: template name: "Neigung Roll" @@ -113,63 +88,29 @@ sensor: return NAN; } float raw = atan2(-id(accel_x).state, id(accel_z).state) * (180.0 / 3.14159265); - return raw - id(roll_offset); + return raw - id(roll_offset); // Offset wird hier subtrahiert filters: - sliding_window_moving_average: window_size: 8 send_every: 1 - exponential_moving_average: alpha: 0.2 - on_value: - - ble_server.characteristic.set_value: - id: roll_ble - value: !lambda |- - std::vector v(sizeof(float)); - float val = x; - memcpy(v.data(), &val, sizeof(float)); - return v; -script: - # Die aktuelle Lage wird zur neuen Null. Beide Wege – Knopf und Bluetooth – - # laufen hier zusammen, damit sie nicht auseinanderdriften. - - id: calibrate_level - then: - - lambda: |- - if (isnan(id(accel_x).state) || isnan(id(accel_y).state) || isnan(id(accel_z).state)) { - ESP_LOGW("vanalign", "Kalibrierung abgebrochen: keine Sensorwerte"); - return; - } - id(pitch_offset) = atan2(id(accel_y).state, - sqrt(pow(id(accel_x).state, 2) + pow(id(accel_z).state, 2))) - * (180.0 / 3.14159265); - id(roll_offset) = atan2(-id(accel_x).state, id(accel_z).state) * (180.0 / 3.14159265); - ESP_LOGI("vanalign", "Kalibriert: pitch_offset=%.2f roll_offset=%.2f", - id(pitch_offset), id(roll_offset)); - - - script.execute: publish_offsets - - - id: reset_calibration - then: - - lambda: |- - id(pitch_offset) = 0.0f; - id(roll_offset) = 0.0f; - ESP_LOGI("vanalign", "Kalibrierung zurückgesetzt"); - - script.execute: publish_offsets - - # Die gespeicherten Offsets für lesende Clients bereitstellen. - - id: publish_offsets - then: - - ble_server.characteristic.set_value: - id: offsets_ble - value: !lambda |- - std::vector v(2 * sizeof(float)); - float values[2] = { id(pitch_offset), id(roll_offset) }; - memcpy(v.data(), values, sizeof(values)); - return v; +globals: + - id: pitch_offset + type: float + restore_value: yes + initial_value: '0.0' + - id: roll_offset + type: float + restore_value: yes + initial_value: '0.0' + - id: enable_captive + type: bool + restore_value: yes + initial_value: 'false' esp32_ble_server: - manufacturer: "VanAlign" - model: "Pro" services: - uuid: 2a24b789-7aab-4535-af3e-ee76a35cc42d advertise: true @@ -178,24 +119,20 @@ esp32_ble_server: uuid: cad48e28-7fbe-41cf-bae9-d77a6c233424 description: "Pitch" read: true - notify: true - + value: !lambda |- + std::vector v(sizeof(float)); + float val = id(pitch).state; + memcpy(v.data(), &val, sizeof(float)); + return v; - id: roll_ble uuid: cad48e28-7fbe-41cf-bae9-d77a6c233425 description: "Roll" read: true - notify: true - - # Zwei Floats: die gespeicherten Offsets. Damit kann ein Client - # anzeigen, ob und worauf kalibriert wurde. - - id: offsets_ble - uuid: cad48e28-7fbe-41cf-bae9-d77a6c233426 - description: "Kalibrier-Offsets" - read: true - notify: true - - # Ein Byte: 0 setzt die Kalibrierung zurück, alles andere kalibriert - # auf die aktuelle Lage. + value: !lambda |- + std::vector v(sizeof(float)); + float val = id(roll).state; + memcpy(v.data(), &val, sizeof(float)); + return v; - id: calib_ble uuid: cad48e28-7fbe-41cf-bae9-d77a6c233427 description: "Kalibriere Neigung" @@ -210,6 +147,47 @@ esp32_ble_server: id(calibrate_level).execute(); } +i2c: + sda: GPIO8 + scl: GPIO9 + scan: true + +#web_server: +# port: 80 + + +#ota: +# platform: web_server + + +#wifi: +# ap: +# ssid: "VanAlign-Setup" +# password: "kalibrierung" + + + +script: + # Die aktuelle Lage wird zur neuen Null. Knopf und Bluetooth laufen hier + # zusammen, damit sie nicht auseinanderdriften. + - id: calibrate_level + then: + - lambda: |- + if (isnan(id(accel_x).state) || isnan(id(accel_y).state) || isnan(id(accel_z).state)) { + ESP_LOGW("vanalign", "Kalibrierung abgebrochen: keine Sensorwerte"); + return; + } + id(pitch_offset) = atan2(id(accel_y).state, sqrt(pow(id(accel_x).state, 2) + pow(id(accel_z).state, 2))) * (180.0 / 3.14159265); + id(roll_offset) = atan2(-id(accel_x).state, id(accel_z).state) * (180.0 / 3.14159265); + ESP_LOGI("vanalign", "Kalibriert: pitch_offset=%.2f roll_offset=%.2f", id(pitch_offset), id(roll_offset)); + + - id: reset_calibration + then: + - lambda: |- + id(pitch_offset) = 0.0f; + id(roll_offset) = 0.0f; + ESP_LOGI("vanalign", "Kalibrierung zurückgesetzt"); + button: - platform: template name: "Kalibriere Neigung" @@ -222,7 +200,6 @@ button: id: calib_reset_button on_press: - script.execute: reset_calibration - - platform: restart name: "ESP Restart" @@ -232,4 +209,4 @@ text_sensor: id: firmware_version icon: mdi:tag lambda: |- - return {"v1.1.0"}; + return {"v1.0.2"};