Skip to content

Commit dd6844a

Browse files
malysclaude
andcommitted
feat(catalog): a confirmation that carries its own wait, and a text message
ASK_CONFIRM moves from ValueKind.TEXT to a new ValueKind.CONFIRM: the question and the seconds it waits for an answer are one decision, and the editor draws one control per kind, so the wait had to become part of the kind rather than a special case for one action. Bounds are 5-60 s; Action.number carries the wait and 0 — every rule saved before the field existed — means ASK_CONFIRM_DEFAULT_SECONDS (10), not the floor. SEND_SMS is new, with ValueKind.SMS: the number in text, the contact it was picked from in displayName, the message in payload. Where it goes was read off the head unit's own APKs rather than guessed. IBtCall carries twenty-six transactions and not one of them is a message, so the vendor hands-free service cannot send one; the system settings service, on the other hand, manages a "MAP Client" profile (18, UUIDs MAP/MNS/MAS), which is the Bluetooth Message Access Profile in the role a car takes. So the message leaves through the paired phone. No @SupportedOn on SEND_SMS: MAP is AOSP, not SAIC routing, so there is nothing to declare per firmware generation. Whether it works is a bind read at runtime, which is the same rule the vendor services already follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ca613e1 commit dd6844a

8 files changed

Lines changed: 108 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ All notable changes to this project are documented here. Format follows
44
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
55
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.3.0] - 2026-08-23
8+
9+
### Added
10+
11+
- `ActionType.SEND_SMS`: send a text message through the paired phone, with `ValueKind.SMS`
12+
(recipient in `text`, contact label in `displayName`, message in `payload`). No
13+
`@SupportedOn`: the message leaves over the Bluetooth Message Access Profile, not over a
14+
SAIC service, so nothing is routed per firmware generation — availability is a bind, read at
15+
runtime. Verified against the head unit's own APKs: `IBtCall` carries twenty-six
16+
transactions and none of them is a message, while the system settings service manages a
17+
"MAP Client" profile (18, UUIDs MAP/MNS/MAS).
18+
- `ValueKind.CONFIRM`: a yes/no question plus the seconds it waits for an answer. The editor
19+
draws one control per kind, so the wait had to become part of the kind rather than a
20+
special case for one action.
21+
22+
### Changed
23+
24+
- `ActionType.ASK_CONFIRM` now carries `ValueKind.CONFIRM` (5–60 s) instead of plain text.
25+
`Action.number` is the wait; `0` — every rule saved before the field existed — means
26+
`ActionType.ASK_CONFIRM_DEFAULT_SECONDS` (10), not the bottom of the range.
27+
728
## [1.2.0] - 2026-08-22
829

930
### Added

‎docs/firmware-matrix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
| SPEAK_TEXT | — | — | — | — | — | — |
6161
| NAVIGATE_TO | — | — | — | — | — | — |
6262
| ASK_CONFIRM | — | — | — | — | — | — |
63+
| SEND_SMS | — | — | — | — | — | — |
6364
| WEBHOOK | — | — | — | — | — | — |
6465
| DELAY | — | — | — | — | — | — |
6566
| APPLY_PROFILE | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

‎lib/src/main/java/com/evsuite/hardware/catalog/ActionType.kt‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,40 @@ enum class ActionType(
439439
* most of the time and wrong the once, and the driver is the only one who knows which
440440
* this is. Placed first, it turns an automatic rule into a proposed one.
441441
*
442-
* [Action.text] is the question. No answer within the prompt's own timeout counts as
443-
* no — the rest of the rule needs a deliberate yes, not a driver who walked away.
442+
* `Action.text` is the question and `Action.number` how many seconds it waits. No answer
443+
* within that time counts as no — the rest of the rule needs a deliberate yes, not a
444+
* driver who walked away. `0` means [ASK_CONFIRM_DEFAULT_SECONDS]: rules saved before the
445+
* wait was configurable carry no value, and the field they never set must not read as an
446+
* instant refusal.
444447
*/
445448
ASK_CONFIRM(
446449
R.string.act_ask_confirm, ActionGroup.SYSTEM,
447-
ValueSpec(ValueKind.TEXT, hintRes = R.string.act_ask_confirm_hint), bridgeAction = null
450+
ValueSpec(
451+
ValueKind.CONFIRM,
452+
min = 5, max = 60,
453+
unitRes = R.string.unit_second, hintRes = R.string.act_ask_confirm_hint
454+
),
455+
bridgeAction = null
456+
),
457+
/**
458+
* Sends a text message through the paired phone.
459+
*
460+
* Same reasoning as [CALL_NUMBER], one profile further out: the head unit has no SIM, so
461+
* there is nothing here to send from. The vendor hands-free service does not help either
462+
* — its `IBtCall` interface is calls only, with no message transaction to address — so
463+
* the message goes out over the Bluetooth Message Access Profile, which the car's own
464+
* Bluetooth settings manage as "MAP Client". Whether that profile is up is a bind, not a
465+
* table: the action reports itself unsupported on a car or a phone that does not carry it.
466+
*
467+
* `Action.text` is the number, `Action.displayName` the contact it was picked from, and
468+
* `Action.payload` the message.
469+
*
470+
* Not standstill-gated, for the reason a call is not: it writes nothing to the vehicle,
471+
* and the message was written when the rule was, not at the wheel.
472+
*/
473+
SEND_SMS(
474+
R.string.act_send_sms, ActionGroup.SYSTEM,
475+
ValueSpec(ValueKind.SMS, hintRes = R.string.act_send_sms_hint), bridgeAction = null
448476
),
449477
/**
450478
* Calls an HTTP(S) endpoint. [Action.flag] carries the verb — false for GET, true for
@@ -498,5 +526,14 @@ enum class ActionType(
498526
@Suppress("DEPRECATION")
499527
fun byGroup(): Map<ActionGroup, List<ActionType>> =
500528
entries.filterNot { it == CALL_CONTACT }.groupBy { it.group }
529+
530+
/**
531+
* The wait [ASK_CONFIRM] uses when its action carries none.
532+
*
533+
* The spec's own floor (5 s) is not zero: a question that closes before it can be
534+
* read is a refusal dressed as a choice. Its ceiling (60 s) holds a rule's cycle for
535+
* at most a minute, the same order as the delay budget the engine already enforces.
536+
*/
537+
const val ASK_CONFIRM_DEFAULT_SECONDS = 10
501538
}
502539
}

‎lib/src/main/java/com/evsuite/hardware/catalog/ValueSpec.kt‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ enum class ValueKind {
4242
DESTINATION,
4343
/** Webhook URL plus an optional POST body. */
4444
WEBHOOK,
45+
/**
46+
* A recipient and a message: the number in `text`, the contact it was picked from in
47+
* `displayName`, and the message itself in the field a webhook body already uses.
48+
*
49+
* Two controls rather than one, and no new model field — a message has an addressee, and
50+
* it is the same phone-book field [CONTACT] offers, so a number can still be typed where
51+
* no contact exists.
52+
*/
53+
SMS,
54+
/**
55+
* A yes/no question in `text`, and how long it waits for an answer in `number` seconds.
56+
*
57+
* The wait belongs to the rule, not to the app: a question asked before the doors unlock
58+
* is answered at once or not at all, while one asked at the end of a drive has to survive
59+
* the driver looking away. `0` — every rule saved before the field existed — means the
60+
* default wait.
61+
*/
62+
CONFIRM,
4563
/**
4664
* A point and a radius: `text` holds "latitude,longitude", `number` the radius in
4765
* metres. Two controls rather than one, but no new model field — the flat union in

‎lib/src/main/res/values-fr/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@
162162
<string name="act_ask_confirm_hint">ex. : ouvrir les vitres ?</string>
163163
<string name="act_webhook">Appeler un webhook</string>
164164
<string name="act_call">Appeler</string>
165+
<string name="act_send_sms">Envoyer un SMS</string>
166+
<string name="act_send_sms_hint">ex. : j’arrive</string>
165167
<string name="act_call_contact">Appeler un contact</string>
166168
<string name="cond_window_position">Ouverture des vitres</string>
167169
<string name="cond_doors_locked">Portes verrouillées</string>

‎lib/src/main/res/values/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@
162162
<string name="act_ask_confirm_hint">e.g. Open the windows?</string>
163163
<string name="act_webhook">Call webhook</string>
164164
<string name="act_call">Call</string>
165+
<string name="act_send_sms">Send a text message</string>
166+
<string name="act_send_sms_hint">e.g. On my way</string>
165167
<string name="act_call_contact">Call a contact</string>
166168
<string name="cond_window_position">Window opening</string>
167169
<string name="cond_doors_locked">Doors locked</string>

‎lib/src/test/java/com/evsuite/hardware/FirmwareSupportTest.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ class FirmwareSupportTest {
3838
ActionType.NAVIGATE_TO,
3939
ActionType.WEBHOOK,
4040
ActionType.ASK_CONFIRM,
41-
ActionType.DELAY
41+
ActionType.DELAY,
42+
// Le SMS part par le profil Bluetooth MAP, pas par un service SAIC : rien à router
43+
// par génération de firmware. Sa disponibilité se lit au bind, pas dans la matrice.
44+
ActionType.SEND_SMS
4245
)
4346

4447
@Test

‎lib/src/test/java/com/evsuite/hardware/catalog/CatalogConsistencyTest.kt‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,26 @@ class CatalogConsistencyTest {
3636
}
3737

3838
@Test
39-
fun `la demande de confirmation porte sa question et rien d autre`() {
39+
fun `le sms porte un destinataire et un message`() {
40+
// Le destinataire est un numéro (ou un contact du répertoire PBAP) et le message est
41+
// du texte libre : un seul contrôle ne pourrait pas porter les deux.
42+
assertEquals(ValueKind.SMS, ActionType.SEND_SMS.spec.kind)
43+
assertTrue(ActionType.SEND_SMS.spec.hintRes != 0)
44+
assertFalse("le SMS n'écrit rien dans le véhicule", ActionType.SEND_SMS.gated)
45+
}
46+
47+
@Test
48+
fun `la demande de confirmation porte sa question et son delai`() {
4049
// Le texte EST la question posée au conducteur ; un exemple est indispensable, la
41-
// formulation par défaut d'un champ vide n'apprend rien.
42-
assertEquals(ValueKind.TEXT, ActionType.ASK_CONFIRM.spec.kind)
50+
// formulation par défaut d'un champ vide n'apprend rien. Le nombre est l'attente en
51+
// secondes, bornée pour qu'une question reste lisible sans bloquer le cycle.
52+
assertEquals(ValueKind.CONFIRM, ActionType.ASK_CONFIRM.spec.kind)
4353
assertTrue(ActionType.ASK_CONFIRM.spec.hintRes != 0)
54+
assertTrue(ActionType.ASK_CONFIRM.spec.min > 0)
55+
assertTrue(ActionType.ASK_CONFIRM.spec.max > ActionType.ASK_CONFIRM.spec.min)
56+
// Le défaut des règles enregistrées sans délai doit rester dans les bornes offertes.
57+
assertTrue(ActionType.ASK_CONFIRM_DEFAULT_SECONDS in
58+
ActionType.ASK_CONFIRM.spec.min..ActionType.ASK_CONFIRM.spec.max)
4459
assertFalse("la question ne touche pas le véhicule", ActionType.ASK_CONFIRM.gated)
4560
}
4661

@@ -51,7 +66,7 @@ class CatalogConsistencyTest {
5166
val localOnly = setOf(
5267
ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT,
5368
ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM,
54-
ActionType.DELAY
69+
ActionType.DELAY, ActionType.SEND_SMS
5570
)
5671

5772
ActionType.entries.filterNot { it in localOnly }.forEach { type ->
@@ -64,7 +79,7 @@ class CatalogConsistencyTest {
6479
listOf(
6580
ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT,
6681
ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM,
67-
ActionType.DELAY
82+
ActionType.DELAY, ActionType.SEND_SMS
6883
).forEach { type ->
6984
assertTrue(
7085
"${type.name} ne touche pas le véhicule et ne doit pas avoir de bridgeAction",

0 commit comments

Comments
 (0)