Skip to content

iPhone M1: car view and a lean Now - #11

Closed
frahlg wants to merge 3 commits into
claude/ios-m1-foundationfrom
claude/ios-m1-car
Closed

frahlg wants to merge 3 commits into
claude/ios-m1-foundationfrom
claude/ios-m1-car

Conversation

@frahlg

@frahlg frahlg commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Stacked on #8 (base claude/ios-m1-foundation). This is M1 step 3: the car view, and Now cut down to what people look at every day. The price widgets come in a separate PR from the same base, and the two touch different files.

What changed

Car view (appleApp/FTW/Car/)

  • How full the car is, and who says so: "From the car", "From the car, last reported HH:MM" (older than five minutes, Core's VehicleMaxAge, or vehicle_stale), "You set 40 % at 14:02", FTW's estimate, or FTW's assumption. When a car is plugged in and unread, a badge says "FTW assumes" next to the 20 %, and the line asks for the real level.
  • 20/40/60/80 and ±5 send loadpoint.soc.set at once. Taps made while one is on its way fold into the next send. A tap shows at once as the chosen button and the receipt; the number moves only when the box confirms it. The box refuses a level for a car that is not plugged in, so the view shows the buttons only then.
  • The goal goes through the schedule PUT, for example "80 % by 07:00 on weekdays". The view shows the next deadline, whether the plan makes it, and when it charges. With no goal it offers "Use 80 % by 07:00 on weekdays", and an editor (target in 5 % steps, a time wheel, weekdays, every day, weekends or once).
  • Charge now, Pause and Back to the plan. Charge now is loadpoint.hold at the charger's full current with hold_s: 0 and no level gate.
  • A receipt under the level and under the charge buttons: Sent, Received (cmd.ack), Done (cmd.result), or why not, in one sentence. A success fades after 20 s; a refusal stays until the next tap. Session.command gains onAck for this.
  • In the demo, a section plugs the car in and out.

The ready time (srcfl/ftw#1582, srcfl/ftw#1586)

  • The view reads schedule.time_of_day_min (minutes after midnight on the box's clock) when the box sends it. Otherwise it reads time_of_day_min_utc on today's date the way Core's clockFromUTC does. So 07:00 stays 07:00 across 25 October, and a box that kept the UTC minute shows the 06:00 it really uses.
  • A save builds the body from nothing read and sends both time_of_day_min and today's time_of_day_min_utc. A Core with #1586 takes the first; an older one reads the second. It never sends back a read schedule with only the UTC field changed. No registry name is involved.
  • The household zone is the box's when its hello names one (not "Local" or UTC), else the phone's. When it is not the phone's, the next deadline says "home time".

Will the car be ready?

  • The answer comes from the plan's own loadpoint_shortfall_wh (GET /api/mpc/plan), read when the car view opens and when the car's plan changes. "On track: 80 % by tomorrow 07:00." or "Will not make it: about 62 % by tomorrow 07:00, not 80 %." From an assumed level it adds "Counted from FTW's guess of 20 %." It says nothing while Charge now holds the car or the plan is being remade.

Now

  • Stays: "Is FTW in control?", the power flow, the car as one row that zooms into the car view, and the price card. Gone: the headline sentence, today's totals, savings, the fuse card and the car banner above every screen.
  • "Is FTW in control?" comes from lane 0 alone: the box's dispatch block and each source's state. It is green only while frames arrive, nothing blocks FTW and every device reports. A quiet device is named. From a cache it is grey and dated. Core's per-device control feedback can join it when lane 0 carries it (M5); polling /api/status for it is gone.
  • The tab bar with one tab gives way to a navigation stack. -FTWTab car opens the car view for screenshots.

Feel and speed (Fredrik's bar, now in CLAUDE.md)

  • No timer asks the box. The chargers are read when a screen that shows them appears, after a command, when the stream says the car started or stopped drawing, and when the stream comes back. The shortfall: when the car view opens and when the car's plan changes.
  • First frame from the cache. A cold start paints the car from the App Group cache the app writes for its widgets, dated and marked as not live, before the box answers. With nothing known, the screens show their shape (.redacted), never a spinner.
  • A frame redraws what moved. Now is split into small views that read only what they show; the flow diagram, the control line and the price chart are Equatable, and the price chart's clock ticks by the minute.
  • System design. The car view is a List with system buttons; Liquid Glass comes with the navigation bar and the sheet. Haptics on every command and outcome, symbol effects, numeric transitions on every figure, one spring for all motion, and a zoom from the Now row to the car view.
  • No UIKit outside the camera. Theme colours move from UIColor to an asset catalog (FTW/UI/Theme.xcassets, same values as the design tokens).

Words

  • English and Swedish for all of the above, plus the flow diagram, the price card, the demo band and the box API's error sentences. FTWKit has no resources and Package.swift is outside this branch, so its Swedish lives in a table in Format/EV.swift (AppWords). It can move to a strings file later. Pairing is still English.

Demo box

  • A car that arrives plugged in and unread, a car that reports its level, a plan with a shortfall, 0.9 s from ack to result, and a charger that takes 4 s to start. It can play a Core with or without #1586. -FTWDemoCar opens one of eight situations (assumed, unplugged, estimated, car, carOld, late, noGoal, charging).
  • Fix: the loopback could deliver a frame before the one sent ahead of it. Each frame had its own timer, and live timers with the same delay need not fire in order. An api.chunk that arrived before its api.head read as "your box didn't answer". Each timer now delivers the oldest frame. A test fires the timers backwards.

Cut from LoadpointsModel: boost, solar only and battery size. The plan puts them under "More", later.

Checks

Check Result
swift test in appleApp/FTWKit (Xcode 27.0, Swift 6.4) FTWKitTests: 99 tests in 16 suites passed, live test skipped. FTWSharedTests: 13 passed.
xcodebuild FTW scheme, generic/platform=iOS Simulator, own derived data Builds, no warnings in the changed files
Launch, LaunchClock (process start to first frame / to first content), five cold launches of the demo, Debug build, iPhone 17 simulator, iOS 26.5, on a shared laptop First frame 500–744 ms, median 689 ms. First content 951–1197 ms, median 1121 ms. The gap is the demo's own latency (120 ms each way).
Demo in a new iPhone 17 simulator (iOS 26.5), Swedish and English Every screen below looked right

New tests (CarTests.swift): the level's origin and age, including the five-minute boundary and the box's stale flag; ±5; the receipt at every stage, including a refusal before and after the ack; the ready time across 25 October 2026 and the hours that happen twice or never; the goal from a box that sends time_of_day_min and from one that does not; a save that sends both fields and keeps 07:00 on either Core, on both sides of 25 October; the household zone; readiness from the shortfall; "Is FTW in control?" from the stream and never green from a cache; no charger read without a reason (none in a minute of quiet, one per appearance, one after a command, one when the car starts drawing) and no shortfall read in three quiet minutes; a cold start painted from the cache; a tap that never shows as the level before the box answers; and the whole flow against the demo box.

Screenshots (Swedish unless named): Now in light, dark and the largest text size; the car view in light, dark and the largest text size, including the level buttons there; a plugged-in car whose level FTW assumes; a car level last reported 40 minutes ago; a goal the night is too short for; unplugged; no goal; Charge now running; the receipt after the box took a level but before it answered, with the old number still shown. They are with the session that built this, not in the repo.

Open before merge

  • DCO: the commits carry no Signed-off-by. CONTRIBUTING asks for it, and an agent should not sign in Fredrik's name.
  • Not run against a real box. M1's proof on the home box is still to do: set a level on the Easee, read it back, and restart Core to see what soc_retention keeps.
  • The cold start from the cache is not measured. The simulator has no paired box, so the launch numbers above are the demo's. The cached paint itself is tested.

Next steps, left out on purpose

The core is here: the level with its source and age, the goal, Charge now, Pause and Back to the plan with a visible outcome, and the calm line on Now. These wait, because daily use does not need them:

  • Start connect() from the scene's first .task instead of FTWApp.init, so the Keychain reads cannot come before the first frame (AppModel, FTWApp; outside this branch).
  • Replace GCD in Platform/NetworkWatch.swift and Platform/QRScanner.swift, and UIApplication in Platform/Passkeys.swift (outside this branch).
  • A shortfall field on /api/loadpoints, so the app need not read the whole plan (Core).
  • Per-device control feedback on lane 0, so "Is FTW in control?" can name a device that does not follow (M5).
  • Keep "You set" across app restarts; today a level set earlier reads as FTW's estimate after a restart, which is still true.
  • Move FTWKit's Swedish from the AppWords table to a strings file once Package.swift can give FTWKit resources. Swedish for pairing.
  • A short background task so leaving just after Charge now still sees cmd.result, and a sign-out screen (both from iPhone M1 foundation: Swift app cut to Pair, Now and widgets #8).
  • Boost, solar only and battery size under "More".
  • The car view in appleApp/scripts/demo-screenshots.sh (-FTWTab car, -FTWDemoCar are ready).
  • A success receipt fades after 20 s and moves the content under it by one row; a fade without the move would be calmer.
  • M2 and later: Controls, Siri, a Live Activity for Charge now, notifications.

🤖 Generated with Claude Code

frahlg and others added 3 commits October 12, 2026 18:01
The car view's rules, in FTWKit so they are tested:

- Where the level comes from: from the car, from the car but older than
  five minutes ("last reported HH:MM"), set from this phone, FTW's
  estimate, or FTW's assumption of 20 % at plug-in.
- A receipt for every command: sent, received (cmd.ack), done
  (cmd.result), or why not. Session.command gains onAck for this.
- The ready time on the household clock, worked out the way Core does,
  so 07:00 stays 07:00 across 25 October and a box that kept the UTC
  minute shows the 06:00 it really uses. Goals go out as both
  time_of_day_min and time_of_day_min_utc.
- Whether the car makes its goal, from the plan's own shortfall.
- "Is FTW in control?" from Core's control feedback, never green from
  readings that are not live.
- English and Swedish words for all of it.

LoadpointsModel drops boost, solar-only and battery size from the daily
path. Charge now holds the charger's full current with no time limit and
no level gate. The demo box plugs in and out, has a car that reports its
level, a plan with a shortfall, and a charger that takes a moment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now keeps four things: "Is FTW in control?" in one calm line, the power
flow, the car as one row that opens the car view, and the price card.
The headline sentence, today's totals, savings and the fuse go. The
line is green only when Core's control feedback says so over a live
stream; a cache is grey and dated.

The car view, in FTW/Car:

- How full the car is, and who says so. Plugged in and unread, FTW
  assumes 20 % and says so with a badge. 20/40/60/80 and ±5 send
  loadpoint.soc.set at once; quick taps fold into one send.
- The goal, on the household clock, with the next deadline, whether
  the plan makes it, and when it charges. "Use 80 % by 07:00 on
  weekdays" when there is none; an editor for the rest.
- Charge now, Pause and Back to the plan, each with a receipt: sent,
  received, done, or why not.
- In the demo, a card to plug the car in and out.

Every word is in English and Swedish. The tab bar with one tab gives way
to a navigation stack. The bands above every screen stop growing past
xxxLarge text, and the flow diagram past xLarge.

The demo's loopback could deliver a frame before the one sent ahead of
it: each frame had its own timer, and live timers with the same delay
need not fire in order. An api.chunk before its api.head read as "your
box didn't answer". Each timer now delivers the oldest frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing asks the box on a timer any more. The chargers are read when a
screen that shows them appears, after a command, when the stream says
the car started or stopped drawing, and when the stream comes back. The
plan's shortfall is read when the car view opens and when the car's plan
changes. "Is FTW in control?" now comes from lane 0 alone (the dispatch
block and each source's state), so Now no longer polls /api/status.

A cold start paints the car from the App Group cache the app writes for
its widgets, dated and marked as not live, before the box answers. While
nothing at all is known the screens show their shape, not a spinner.
LaunchClock logs process start to first frame and first content.

A tapped level shows as the chosen chip and the receipt; the number only
moves once the box confirms it. Goals send time_of_day_min and today's
time_of_day_min_utc, built from nothing read; tests cover a Core with
srcfl/ftw#1586 and one without.

The car view is a system List with system buttons, haptics on commands
and outcomes, symbol effects and numeric transitions. The Now car row
zooms into it. Now is split into small views that redraw only what a
frame changed. Theme colours move to an asset catalog, so no UIKit is
left outside the camera. CLAUDE.md gains "Feel and speed".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg

frahlg commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Included in #14, which lands the whole iPhone M1 app as one PR. The branch stays.

@frahlg frahlg closed this Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant