Skip to content

Commit 63dc4d1

Browse files
malysclaude
andcommitted
feat: climate, charging, radio, calls, navigation and a location condition
Wires the vendor services MG4Hardware now reaches, and answers the reports that came with the request. **The test button answers the question it asks.** "Test now" said "test running" and stopped there, leaving the History tab to be opened to find out what happened. The cycle now reports back through CycleReporter and lands in a dialog: one line per rule with its outcome, the per-action verdict behind a failure, and the name of any signal that could not be read. **A rule's message is shown, not only posted.** A notification on a head unit is an icon in a bar nobody looks at while driving, and the channel can be silenced without the app being told. showRuleMessage now shows a toast as well, so the action always reaches the driver; a silenced channel is reported as a detail rather than as a refusal, which also means the picker stops hiding it. **Value controls open on the present setting.** A brightness slider starting at 5% invites confirming 5% by accident. A fresh action now opens on what the car reports — via the new ActionType.currentKey and a snapshot read when the editor opens. Reopening a saved action still shows what the rule says, so editing a name cannot rewrite a value. **The standstill limit is settable, 0 to 50 km/h**, from the Rules screen; 0 stays the default. The dialog says what raising it does and does not do: the vehicle still refuses what it refuses on its own, and an unreadable speed still fails closed. The value in force appears on the Diagnostic tab and in every exported report. **New entries**, most on vendor services (SWI68/SWI165, the apks/ platform): climate on/off, target temperature, A/C, AUTO, recirculation, fan level, defrosters energy charge limit, allow charging, scheduled charging + window, battery pre-heating audio play the radio system call a number, navigate to a destination reading battery level, charging, charge limit, climate on; near a place Climate conditions now read from the vendor service where it answers, falling back to the AOSP ids elsewhere — so what a rule tests and what an action writes are the same signal. "Near a place" takes a point and a radius in metres, prefilled with where the car is, because nobody types coordinates on a head unit. No fix means unavailable, not "somewhere else": otherwise every "when I am NOT at home" rule fires on the driveway while the GPS is still acquiring. Position is read as a last known fix, never requested live, and never leaves the car. The permission is asked for when a rule first needs it, not at startup. **The paste explains itself.** A shared report now opens with a localized block giving the password, the expiry, and credit to April — who host the instance free of charge — with a donation link, so whoever receives the link needs nothing explained separately. The diagnostic gained three rows — which vendor services bound, the position, the write threshold — and three reasons, so an entry blocked by a missing vendor service, no navigation app or no fix says which. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 80e3166 commit 63dc4d1

38 files changed

Lines changed: 1098 additions & 72 deletions

‎.github/security/permission-allowlist.txt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ android.permission.BLUETOOTH_CONNECT
3434
# apply an MG4Control profile when MG4Control is installed.
3535
com.mg4.control.permission.CONTROL_PROFILES
3636

37+
# --- Position ---
38+
# The "near a place" condition, and nothing else. Last known fix only (no live request), read
39+
# on the device, compared against a point stored in the rule; the coordinates never leave the
40+
# car. Refusing it disables that single condition.
41+
android.permission.ACCESS_FINE_LOCATION
42+
android.permission.ACCESS_COARSE_LOCATION
43+
3744
# --- Network ---
3845
# Two uses, both explicit:
3946
# • Console → Share uploads the diagnostic report to a PrivateBin paste

‎AGENTS.md‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,33 @@ per-generation routing in `MG4Hardware`. It is the single source of truth for:
4141

4242
Adding a vehicle entry without `@SupportedOn` fails `FirmwareSupportTest`.
4343

44+
## The standstill gate is configurable, and still fails closed
45+
46+
`VehicleWriteGate.allowUpToKmh` moves the "moving" boundary from 0 up to 50 km/h; the app
47+
stores the user's choice and pushes it in wherever the vehicle layer is initialised. Two
48+
invariants are not negotiable and are unit-tested: an unreadable or negative speed refuses at
49+
any threshold, and the shared layer clamps the value. Default is 0.
50+
4451
## Vehicle writes go in MG4Control, reads too
4552

4653
MG4Tasker adds no vehicle access code. When a new action needs a new capability, implement
4754
the read/write in MG4Control's `MG4Hardware` (branched per firmware generation — never
4855
assume universal), expose it through `TaskerBridgeService`, then add the catalogue entry
4956
here. Property ids and per-generation routing live in MG4Control; do not duplicate them.
5057

51-
Climate/window signals are **read-only and unverified** today: standard AOSP ids that the
52-
R69 sources name, but not confirmed on any MG4. They exist so the Diagnostic tab can prove
53-
a signal is readable before any write path is added. Do not add climate/window writes
54-
until the reads are confirmed on a vehicle.
58+
Climate, charging, radio and hands-free calls do **not** go through property ids. They use
59+
the SAIC vendor binder services the car's own apps use (`com.mg4.hardware.saic`), with every
60+
descriptor and transaction code read off the decompiled head-unit APKs in `apks/` — cite the
61+
APK and class when you add one. That is what made climate writes honest: the earlier AOSP
62+
climate ids were standard ids the R69 sources name but no MG4 confirmed, so writing them
63+
would have been a guess.
64+
65+
Window writes remain absent — no vendor service exposes them. The AOSP climate reads stay as
66+
a fallback where the vendor service does not answer.
67+
68+
Whether a car has a given vendor service is a **bind, not a table**: the `@SupportedOn` set
69+
records the generations we have evidence for (SWI68/SWI165, the `apks/` platform), and the
70+
Diagnostic tab's *Vehicle services* row is what widens it.
5571

5672
## Reference patterns (inherited from MG4Control / MG4ABRPUploader)
5773

‎MG4Hardware‎

‎README.md‎

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ profile* — becomes available.
3333
- [The speed gate](#the-speed-gate)
3434
- [Conditions and actions](#conditions-and-actions)
3535
- [Firmware compatibility](#firmware-compatibility)
36-
- [Climate and windows (read-only for now)](#climate-and-windows-read-only-for-now)
36+
- [Vehicle services: climate, charging, radio, calls](#vehicle-services-climate-charging-radio-calls)
3737
- [Diagnostic](#diagnostic)
38+
- [Testing a rule](#testing-a-rule)
3839
- [Exporting the diagnostic and the logs](#exporting-the-diagnostic-and-the-logs)
3940
- [Sharing the logs from the car](#sharing-the-logs-from-the-car)
4041
- [When a rule will not save](#when-a-rule-will-not-save)
@@ -119,12 +120,30 @@ and MG4Control enforce the identical rule.
119120

120121
| | Speed-gated |
121122
|---|---|
122-
| Drive mode, regeneration, one-pedal, ADAS, AEB, ELK, ACC/TJA, limiter, whole profile | **Yes** — when stopped only |
123-
| Seat and steering heating, volume, brightness, audio, notifications | No |
123+
| Drive mode, regeneration, one-pedal, ADAS, AEB, ELK, ACC/TJA, limiter, whole profile | **Yes** |
124+
| Seat and steering heating, volume, brightness, audio, climate, charging, radio, messages | No |
124125

125126
The editor shows "When stopped only" **at the moment you pick the action**, not after. When
126127
an action is refused, the history gives the exact reason instead of staying silent.
127128

129+
### Raising the threshold
130+
131+
The Rules screen lets you move the limit from 0 to 50 km/h. **0 — apply only when stopped —
132+
is the default**, and the value that needs no justification: a car creeping in a car park
133+
reads as moving, and a rule refused there is the most common false negative.
134+
135+
Two things do not change when you raise it:
136+
137+
- **An unreadable speed still refuses.** The gate fails closed whatever the threshold, and
138+
that is the case it exists for.
139+
- **The vehicle still refuses whatever it refuses.** MG4 firmware declines several of these
140+
settings while moving on its own. Raising the threshold does not make a drive-mode change
141+
succeed at 40 km/h; it only stops MG4Tasker from being the one that declines first. The
142+
history then shows the vehicle's refusal instead of the app's.
143+
144+
The threshold in force is reported on the Diagnostic tab and in every exported report, so a
145+
report from a car that had it raised says so.
146+
128147
`VEHICLE_POWER_OFF` is **deliberately absent** from the catalogue, and a unit test enforces
129148
that no catalogue entry can reach it. Cutting the vehicle stays an explicit human gesture.
130149

@@ -135,16 +154,30 @@ that no catalogue entry can reach it. Cutting the vehicle stays an explicit huma
135154
An unreadable condition makes the rule **not evaluable** — it does not fire, and the
136155
history names the missing signal. Unreadable is never treated as false.
137156

138-
Conditions span **context** (Bluetooth, time of day, day of week, firmware), **environment**
139-
(outside temperature), **driving** (ignition, park, speed, drive mode, regeneration, energy
140-
saving), **climate** (A/C, AUTO, recirculation, fan speed, set temperature, window open),
141-
**comfort** (seat/steering heating, media volume, brightness), and **driver assistance**
142-
(AEB, ELK, ACC/TJA, limiter, TSR, overspeed, speed-limit tone, ADAS sound).
143-
144-
Actions cover **profile** application, **driving**, **comfort**, **audio**, **driver
145-
assistance**, and **system** (launch an app, show a notification, speak a message through
146-
the head unit's text-to-speech engine). ADAS state — AEB, ELK,
147-
ACC/TJA, TSR, overspeed and so on — is fully covered as gated actions.
157+
Conditions span **context** (Bluetooth, time of day, day of week, firmware, near a place),
158+
**environment** (outside temperature), **driving** (ignition, park, speed, drive mode,
159+
regeneration, energy saving), **energy** (battery level, charging, charge limit), **climate**
160+
(climate on, A/C, AUTO, recirculation, fan speed, set temperature, window open), **comfort**
161+
(seat/steering heating, media volume, brightness), and **driver assistance** (AEB, ELK,
162+
ACC/TJA, limiter, TSR, overspeed, speed-limit tone, ADAS sound).
163+
164+
Actions cover **profile** application, **driving**, **comfort**, **climate** (on/off, target
165+
temperature, A/C, AUTO, recirculation, fan level, front and rear defrosters), **energy**
166+
(charge limit, allow charging, scheduled charging and its window, battery pre-heating),
167+
**audio** (volume, the fine controls, play the radio), **driver assistance**, and **system**
168+
(launch an app, show a message, speak through the head unit's text-to-speech engine,
169+
navigate to a destination, call a number). ADAS state — AEB, ELK, ACC/TJA, TSR, overspeed and
170+
so on — is fully covered as gated actions.
171+
172+
**Near a place** compares the car's last known fix with a point stored in the rule and a
173+
radius in metres. The point is prefilled with where the car is when the condition is created,
174+
because nobody types coordinates on a head unit. No fix means the condition is *unavailable*,
175+
not "somewhere else" — otherwise every "when I am NOT at home" rule would fire on the
176+
driveway while the GPS was still acquiring. The coordinates never leave the car.
177+
178+
Value controls open on **what the car reports right now** — the brightness slider starts where
179+
the screen already is, not at 5%. Reopening a saved action shows what the rule says instead,
180+
so editing a rule cannot quietly rewrite it.
148181

149182
The exact per-entry list and its firmware support is generated, not hand-written — see
150183
below.
@@ -177,21 +210,47 @@ Highlights (see the generated matrix for the full grid):
177210
- **Overspeed alarm / speed-limit tone** — SWI133 and SWI132 only.
178211
- **ADAS sound warning** — every generation except SWI133.
179212
- **Lane-departure sound + vibration** — SWI132 only.
180-
- **Fine audio** (Bose, balance, fader, tone, 3D, speed volume) — SWI133 and SWI132.
213+
- **Fine audio** (Bose, balance, fader, tone, 3D, speed volume) — SWI69, SWI131, SWI132: the
214+
SAIC `caradapter` audio helper these go through is bound on the A9 platform only.
215+
- **Climate, charging, radio and calls** — SWI68 and SWI165, the generations the decompiled
216+
head-unit APKs in `apks/` come from. They run on SAIC vendor services rather than on
217+
property ids, and whether a given car answers is a bind, not a table: the Diagnostic tab's
218+
*Vehicle services* row reports which of the four responded, and the matrix widens once a
219+
car outside that set reports one.
181220

182221
---
183222

184-
## Climate and windows (read-only for now)
223+
## Vehicle services: climate, charging, radio, calls
224+
225+
Climate and charging used to be read-only and unverified — standard AOSP property ids the
226+
R69 sources name, that no MG4 confirmed. Writing one of those would have been a guess, so
227+
there were no write actions.
185228

186-
Air conditioning (A/C, AUTO, recirculation, fan speed, set temperature) and window state
187-
are available as **conditions** — read-only. They use standard AOSP HVAC/window property
188-
ids that the R69 OEM sources expose, but which are **not yet verified on any MG4
189-
generation**. MG4Hardware reads them null-safely; the Diagnostic tab shows whether each one
190-
is actually readable on your car.
229+
They now go through the **SAIC vendor services** instead: the same binder interfaces the
230+
car's own HVAC, charging, radio and hands-free apps use, read off the decompiled head-unit
231+
APKs in [`apks/`](../apks). One bound service (`com.saicmotor.service.vehicle`) exposes a hub
232+
that hands out `aircondition` and `vehiclecharging`; radio and telephony are their own
233+
services. What those calls do is not in doubt — they are what the car does to itself.
191234

192-
Climate/window **writes** are intentionally not in the catalogue. Adding them means a
193-
verified property map first — writing a wrong id to the vehicle is exactly the risk being
194-
deferred. Confirm the reads on the Diagnostic tab, and the write actions can follow.
235+
That buys four things the property ids could not:
236+
237+
- **Climate writes** — on/off, target temperature (17–33 °C, the ends being LO and HI as in
238+
the stock UI), A/C, AUTO, recirculation, fan level, front and rear defrosters.
239+
- **Battery and charging** — charge limit in percent, allow/deny charging, the scheduled
240+
charging window, and battery pre-heating.
241+
- **Radio** — resume the last station as the current audio source. It does not open the
242+
radio screen: a rule firing at ignition wants the sound, not a screen in front of the
243+
driver.
244+
- **Calls** — placed by the car's hands-free stack on the paired phone. The head unit has no
245+
SIM and no dialer, so `ACTION_CALL` would find nothing to handle it.
246+
247+
Climate **conditions** now read from the same service where it answers, falling back to the
248+
AOSP ids elsewhere — so what a rule tests and what an action writes are the same signal.
249+
250+
Window **writes** stay absent: no vendor service exposes them.
251+
252+
Navigation is the exception with no vendor API — `NAVIGATE_TO` uses the standard `geo:`
253+
intent, then `google.navigation:`, and reports honestly when the head unit has neither.
195254

196255
---
197256

@@ -228,6 +287,15 @@ An entry can read fine and still be marked *hidden in the editor*: the firmware
228287
not list this generation, so the picker will not offer it, but an imported rule can still
229288
reach it.
230289

290+
### Testing a rule
291+
292+
**Test now** on the Rules screen runs one full cycle — the exact ignition path, so a passing
293+
test proves the real thing — and then **shows what it decided**: one line per rule with its
294+
outcome (applied, conditions not met, cannot be evaluated, failed), the per-action verdict
295+
behind a failure, and the name of any signal that could not be read. It used to say "test
296+
running" and stop there, which answers nothing for a button whose whole purpose is to answer
297+
that question.
298+
231299
### Exporting the diagnostic and the logs
232300

233301
**Export**, on the Diagnostic tab and on the Console tab, writes one text file to storage:
@@ -262,6 +330,10 @@ because the car has no share target worth the name.
262330
head unit has no clipboard worth using, so the paste URL and the password are written to
263331
the in-app log — the screen the user is already on, and one that survives into the next
264332
exported report.
333+
- **The paste says how to read it.** Its first block, in the app's language, gives the
334+
password, the expiry, and credit to [April](https://www.april.org) — who run the instance
335+
free of charge — with a donation link. Whoever receives the link needs none of that
336+
explained to them separately.
265337

266338
Read it back by opening the link and entering the password. The upload is the only network
267339
use of the stable build.

‎SECURITY.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ moving. A proof of concept helps; a working exploit is not required.
5353
would write to a car on an assumption.
5454
- **The ignition trigger is delayed by ~8 s** so MG4Control's own profile application
5555
finishes first. Interleaving two write sequences was the failure mode being avoided.
56+
- **The standstill gate is configurable, and fails closed regardless.** The Rules screen can
57+
raise the write threshold from 0 to 50 km/h. The default is 0 — apply only when stopped —
58+
the cap is enforced in the shared layer, an unreadable speed refuses at any threshold, and
59+
the value in force is printed in every exported diagnostic.
60+
- **Position never leaves the car.** The "near a place" condition reads the last known fix
61+
(no live request), compares it with a point stored in the rule, and yields a yes/no. The
62+
coordinates are not sent anywhere, including in a shared paste's rule dump, which stores
63+
the point the user chose — the same one they wrote.
5664
- **One network use, user-triggered.** `INTERNET` exists for a single path: *Share* on the
5765
Console tab uploads the diagnostic report to a PrivateBin paste
5866
(`https://paste.chapril.org`). PrivateBin is zero-knowledge — the report is encrypted and

‎app/src/main/AndroidManifest.xml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
<uses-permission android:name="android.permission.BLUETOOTH" />
2929
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
3030

31+
<!-- Position, for the "near a place" condition only. Last known fix, never a live
32+
request, never sent anywhere: it is compared to a point stored in the rule and the
33+
result is a yes/no. Denying it disables that one condition and nothing else.
34+
COARSE is declared alongside FINE because the platform requires the pair; a radius
35+
in metres is what FINE is for, and COARSE alone still answers a wide-enough one. -->
36+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
37+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
38+
3139
<!-- The one network use: uploading a diagnostic report to a PrivateBin paste, on an
3240
explicit user action from the Console tab. Nothing is sent in the background, and
3341
the paste is encrypted client-side — the server never receives the key. -->

‎app/src/main/java/com/mg4/tasker/debug/DebugReport.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,20 @@ object DebugReport {
116116
* fenced block, because the alignment is what makes a 40-row verdict list readable at a
117117
* glance. Reflowing it as prose would lose exactly that.
118118
*/
119-
fun renderMarkdown(context: Context, report: DiagnosticProbe.Report): String = buildString {
119+
fun renderMarkdown(
120+
context: Context,
121+
report: DiagnosticProbe.Report,
122+
/** Front matter for a shared paste: how to open it, and who is hosting it. */
123+
header: String? = null,
124+
): String = buildString {
120125
appendLine("# MG4Tasker diagnostic report")
121126
appendLine()
127+
header?.let {
128+
appendLine(it.trim())
129+
appendLine()
130+
appendLine("---")
131+
appendLine()
132+
}
122133
appendLine("| | |")
123134
appendLine("|---|---|")
124135
appendLine("| generated | ${timestamp(report.at)} |")

0 commit comments

Comments
 (0)