Renames every type and identifier we built for the solar charge
controller bridge from generic "Solar" naming to the product name
VotronicSolarESP: VanAlignSolarProtocol -> VotronicSolarESPProtocol,
SolarSession -> VotronicSolarESPSession, SolarState ->
VotronicSolarESPState, plus the dependent properties, parameters, and
user-facing discovery/empty-state strings.
Purely a Swift-side rename - the underlying BLE service/characteristic
UUIDs are untouched, so it has no effect on communicating with the
actual firmware. DeviceRole.solar's case name, and DemoData's device
instance variables, are left as-is since they describe the role/demo
device rather than this specific integration code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CAMPER_DEMO=1 only works when Xcode or simctl launches the process, so
a build installed locally and opened from the home screen had no way
to enable demo mode at all. Demo mode is now also a persisted toggle
under Settings > Entwicklung (Debug builds only), checked alongside the
environment variable. Takes effect after an app restart since
DeviceStore/BluetoothManager only read it at init.
Also mark the demo leveling sensor as live-connected - it had a level
reading but never a live link state, which left the Ausrichtungs-
Assistent button (and Live Activity toggle) disabled in demo mode.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rotating the phone into landscape while leveling used to leave a
portrait-shaped column of controls that needed scrolling to take in.
All three level-integration screens now branch on verticalSizeClass:
the bubble/vehicle status in the device screen and the full alignment
assistant (advice, readings, and wedge heights) lay out side-by-side
instead of stacked, sized to fit without scrolling.
The device screen also measures its actual available height via a
GeometryReader and feeds it to the level display so the bubble/vehicle
graphic scales to fill the landscape screen rather than being capped at
a fixed size, and the connection-status section moves to the bottom of
the list in landscape so it doesn't compete with the display for space.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the full sentence ("Heck steht höher, links steht höher") in
the .small activity family with a compact "H ↑ L" style readout: a
direction letter per axis (only shown when outside tolerance) around a
single up arrow, since only the higher side is ever named anyway. Text
now shares the same green/orange/red scale as the bubble glyph (factored
into one levelColour helper instead of two duplicated thresholds), and
uses a scalable font style with minimumScaleFactor so it fits whatever
size CarPlay actually grants the card - there's no API to query the
physical display size from Live Activity content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A dropped BLE connection ended the Live Activity immediately, which
flickered off and on during the brief, routine disconnects that happen
constantly while driving. It now waits 12s before actually ending, and
if the sensor reconnects within that window the pending end is simply
cancelled. If the disconnect does outlast the grace period and the
activity really ends, reconnecting afterwards restarts it automatically
with the last known reading, since that wasn't a deliberate user stop.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CarPlay renders Live Activities through the .small activity family
(shared with the Watch Smart Stack), not the lock-screen content view.
Without it, CarPlay fell back to the Dynamic Island's compactLeading/
compactTrailing, which only had the generic "level" SF Symbol and an
empty trailing view — hence the wrong icon and missing text.
Adds .supplementalActivityFamilies([.small]) and a compact layout that
reuses the real LevelBubbleGlyph plus a one-line instruction for that
family, while the lock screen keeps its existing two-line view.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Jedes Grafikset (Seiten-/Heckansicht) ist jetzt ein VehicleGraphicStyle
mit eigenem Seitenverhältnis, wählbar in der Neigungsmesser-Einrichtung
und angewendet in Fahrzeug-Ansicht und Ausrichtungs-Assistent. Die
bisherigen VehicleSide/VehicleRear-Assets heißen jetzt VansterSide/
VansterRear, damit sie eindeutig ihrem Set zuzuordnen sind; California
kommt als zweites Set mit eigener Seitenansicht dazu (Heckansicht
vorerst von Vanster übernommen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ersetzt die zehn wählbaren Symbole durch eine kürzere, konsistentere
Auswahl (car, car.side, suv.side, truck.pickup.side, box.truck, bus)
und setzt suv.side als neuen Standard für neu angelegte Fahrzeuge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Dynamic Island now shows only the level icon (compact/minimal); the
bubble, instruction text, and numbers move to the shared lock
screen/CarPlay ActivityView, which iOS renders identically on both
surfaces (no separate CarPlay layout API exists for this).
- Replace +/- signs with a leading direction letter (H/F for Heck-
Front, R/L for rechts-links) via a new shared LevelDirectionFormatting
helper, used by the level screen, alignment assistant, and dashboard
card. The generic "Messwerte" list keeps raw signed values.
- Live Activity's instruction now reads as two lines ("Heck steht
höher 1.8°" / "links steht höher 0.9°") instead of a sentence plus a
separate number column.
- Fix the bubble level graphic's pitch axis being inverted (front/rear
reversed) in all three copies (app, watch, Live Activity widget) —
confirmed against the known-correct VehicleTiltView orientation.
- Dashboard card shows pitch and roll side by side, large, without the
"Längsneigung"/"Querneigung" labels, for the leveling device.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No code changes; Xcode's own session state (open tabs/scroll position)
after building and running the solar integration in the simulator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
- Assign the shared development team to the widget extension and watch
complication targets, and correct the watch app's companion bundle
identifier so on-device installs succeed after the bundle ID rename
to de.s0.fototeddy.VanAligneiOS.
- Guard against two ConfiguredDevices sharing a peripheralID (e.g. a
double-tapped "Sichern"), which crashed BluetoothManager's
Dictionary(uniqueKeysWithValues:) on launch.
- Keep Bluetooth running in the background while a Live Activity is
active, so lock screen/CarPlay keep receiving fresh readings instead
of freezing at the last value before backgrounding.
- Render the actual bubble-level graphic (not just a static icon) in
the Live Activity's lock screen view and expanded Dynamic Island.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the ActivityKit boilerplate with a real lock screen/Dynamic
Island UI driven by live pitch/roll readings, toggled from the
leveling device's detail screen. Since iOS 17 shows any running Live
Activity on CarPlay's dashboard without a dedicated CarPlay app entitlement,
this covers the requested CarPlay use case without one.
Also fixes the widget extension never being embedded into the app
(missing Embed Foundation Extensions build phase and target
dependency), which meant no widget or Live Activity could ever have
rendered regardless of code changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>