Add solar charge controller integration and firmware

App:
- New DeviceRole.solar with its own BLE session/protocol/state
  (SolarSession, VanAlignSolarProtocol, SolarState), mirroring the
  leveling sensor but deliberately not wired into the Live Activity.
- BluetoothManager now keeps an in-memory history per metric (voltage,
  current, power) instead of a single primary-metric series; cleared on
  app restart by design, not persisted to disk.
- DeviceDetailView shows a channel picker above the history chart when a
  device has more than one chartable metric.
- AddDeviceView recognizes the solar service UUID during setup.
- DemoData gets a simulated solar device so the integration can be
  checked in the simulator without hardware.

Firmware:
- Add esp32_ble_solar.yaml (Votronic solar charge controller over BLE)
  and simulated variants (esp32_ble_sim.yaml, esp32_ble_solar_sim.yaml)
  for testing without a vehicle.
- esp32_ble.yaml: set flash_size/psram for the ESP32-S3 N16R8 module.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
fototeddy
2026-09-05 00:04:20 +02:00
co-authored by Claude Sonnet 5
parent 9a3b5cb472
commit 6d7b32d622
14 changed files with 1257 additions and 22 deletions
+40 -1
View File
@@ -59,8 +59,47 @@ enum DemoData {
name: "Nivellierung", role: .leveling, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000E1")!)
static let solar = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000D0")!,
name: "Solar Dach", role: .solar, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000D1")!)
static var devices: [ConfiguredDevice] {
[/*solar, booster, battery, fridge, */level/*, caravanSolar*/]
[/*booster, battery, fridge, */level, solar/*, caravanSolar*/]
}
/// Mittags, gute Sonne.
static var solarState: SolarState {
var state = SolarState()
state.batteryVoltage = 13.9
state.pvVoltage = 19.4
state.pvCurrent = 6.2
state.pvPower = 120
state.controllerTemperature = 34
state.isBatteryCharging = true
state.isControllerActive = true
state.isCurrentLimited = false
return state
}
/// Ein paar Stunden Verlauf je Kanal, damit die Grafik im Demo-Modus
/// sofort etwas zeigt statt erst nach ein paar Minuten Laufzeit.
static func solarHistory() -> DeviceHistory {
let now = Date()
func series(around value: Double, noise: Double) -> [HistorySample] {
(0..<120).reversed().map { step in
let t = Double(step)
let wave = sin(t / 14) * value * noise + cos(t / 31) * value * (noise / 2)
return HistorySample(time: now.addingTimeInterval(-t * 60),
value: max(0, value + wave))
}
}
return [
"pv_power": series(around: 120, noise: 0.35),
"pv_voltage": series(around: 19.4, noise: 0.08),
"pv_current": series(around: 6.2, noise: 0.3),
"battery_voltage": series(around: 13.9, noise: 0.03),
]
}
/// Leicht schräg stehend, damit die Libelle etwas zu zeigen hat.