feat(blockly): real event transitions — onButton/onAudioDone/onUnlock/onSerial/onEspnow/onPuzzleSolved (D4)
This commit is contained in:
@@ -478,6 +478,49 @@ function registerZacusBlocks(Blockly) {
|
||||
stack(this, ZACUS_COLOR.hardware);
|
||||
});
|
||||
|
||||
// ── Phase D4 : transitions réelles ────────────────────────────────
|
||||
// Blocs « Quand … → aller à [target] » : chacun émet UNE transition avec le
|
||||
// vrai event_type firmware-backed (button/serial/audio_done/unlock/espnow).
|
||||
// event_type ∈ runtime3_common.EVENT_TYPES (pas d'extension de schéma, juste
|
||||
// de l'émission). target = id d'étape cible (même pattern que sceneGoto).
|
||||
// Voie RÉELLE : consommés côté gateway/web (le firmware n'a pas de moteur de
|
||||
// transitions, il est piloté pas-à-pas). Compile dans blocks_to_runtime3.py.
|
||||
def("zacus_onButton", function () {
|
||||
this.appendDummyInput().appendField("Quand bouton")
|
||||
.appendField(new Blockly.FieldTextInput("NEXT"), "button")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onAudioDone", function () {
|
||||
this.appendDummyInput().appendField("Quand audio terminé → aller à")
|
||||
.appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onUnlock", function () {
|
||||
this.appendDummyInput().appendField("Quand déverrouillage")
|
||||
.appendField(new Blockly.FieldTextInput("door"), "source")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onSerial", function () {
|
||||
this.appendDummyInput().appendField("Quand série reçoit")
|
||||
.appendField(new Blockly.FieldTextInput("OK"), "token")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onEspnow", function () {
|
||||
this.appendDummyInput().appendField("Quand ESP-NOW reçoit")
|
||||
.appendField(new Blockly.FieldTextInput("ready"), "command")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onPuzzleSolved", function () {
|
||||
this.appendDummyInput().appendField("Quand énigme résolue")
|
||||
.appendField(new Blockly.FieldTextInput("1"), "puzzle")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
|
||||
// Logique
|
||||
def("zacus_logicIf", function () {
|
||||
// condition : reporter branché (appendValueInput "condition") OU champ
|
||||
@@ -622,6 +665,7 @@ function registerZacusBlocks(Blockly) {
|
||||
|
||||
const ZACUS_BLOCK_TYPES = [
|
||||
"zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch",
|
||||
"zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved",
|
||||
"zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch",
|
||||
"zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume","zacus_mediaPlay",
|
||||
"zacus_hwLCDText","zacus_hwLCDClear","zacus_hwLCDImage","zacus_hwLCDTouchWait",
|
||||
@@ -649,6 +693,13 @@ const ZACUS_FIELDS_BY_KIND = {
|
||||
zacus_npcSay: ["text","voice","emotion","rate"],
|
||||
zacus_npcWaitResponse: ["timeout_s"],
|
||||
zacus_npcIntentMatch: ["intent","then"],
|
||||
// Phase D4 — transitions réelles : « Quand … → aller à [target] ».
|
||||
zacus_onButton: ["button","target"],
|
||||
zacus_onAudioDone: ["target"],
|
||||
zacus_onUnlock: ["source","target"],
|
||||
zacus_onSerial: ["token","target"],
|
||||
zacus_onEspnow: ["command","target"],
|
||||
zacus_onPuzzleSolved: ["puzzle","target"],
|
||||
zacus_hwSoundPlay: ["asset"],
|
||||
zacus_hwAudioStop: [],
|
||||
zacus_hwAudioVolume: ["level"],
|
||||
@@ -739,6 +790,7 @@ const ZACUS_REAL_KINDS = new Set([
|
||||
"zacus_scene","zacus_puzzleQR","zacus_puzzleSound",
|
||||
"zacus_hwOnBoard",
|
||||
"zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch",
|
||||
"zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved",
|
||||
"zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume","zacus_mediaPlay",
|
||||
"zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait",
|
||||
"zacus_logicIf","zacus_logicTimer","zacus_logicScore","zacus_logicSetVar",
|
||||
@@ -767,6 +819,7 @@ const ZACUS_TOOLBOX_CATEGORIES = [
|
||||
// ── Voie réelle (exécutée on-device ou côté gateway) ──────────────────
|
||||
{ name: "Cartes", colour: ZACUS_COLOR.hardware, types: ["zacus_hwOnBoard"] },
|
||||
{ name: "Scènes", colour: ZACUS_COLOR.scene, types: ["zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch"] },
|
||||
{ name: "Transitions", colour: ZACUS_COLOR.scene, types: ["zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved"] },
|
||||
{ name: "NPC voix", colour: ZACUS_COLOR.npc, types: ["zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch"] },
|
||||
{ name: "Audio", colour: ZACUS_COLOR.audio, types: ["zacus_mediaPlay","zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume"] },
|
||||
{ name: "ESP-NOW", colour: ZACUS_COLOR.espnow, types: ["zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait"] },
|
||||
|
||||
@@ -474,6 +474,49 @@ export function registerZacusBlocks(Blockly) {
|
||||
stack(this, ZACUS_COLOR.hardware);
|
||||
});
|
||||
|
||||
// ── Phase D4 : transitions réelles ────────────────────────────────
|
||||
// Blocs « Quand … → aller à [target] » : chacun émet UNE transition avec le
|
||||
// vrai event_type firmware-backed (button/serial/audio_done/unlock/espnow).
|
||||
// event_type ∈ runtime3_common.EVENT_TYPES (pas d'extension de schéma, juste
|
||||
// de l'émission). target = id d'étape cible (même pattern que sceneGoto).
|
||||
// Voie RÉELLE : consommés côté gateway/web (le firmware n'a pas de moteur de
|
||||
// transitions, il est piloté pas-à-pas). Compile dans blocks_to_runtime3.py.
|
||||
def("zacus_onButton", function () {
|
||||
this.appendDummyInput().appendField("Quand bouton")
|
||||
.appendField(new Blockly.FieldTextInput("NEXT"), "button")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onAudioDone", function () {
|
||||
this.appendDummyInput().appendField("Quand audio terminé → aller à")
|
||||
.appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onUnlock", function () {
|
||||
this.appendDummyInput().appendField("Quand déverrouillage")
|
||||
.appendField(new Blockly.FieldTextInput("door"), "source")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onSerial", function () {
|
||||
this.appendDummyInput().appendField("Quand série reçoit")
|
||||
.appendField(new Blockly.FieldTextInput("OK"), "token")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onEspnow", function () {
|
||||
this.appendDummyInput().appendField("Quand ESP-NOW reçoit")
|
||||
.appendField(new Blockly.FieldTextInput("ready"), "command")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
def("zacus_onPuzzleSolved", function () {
|
||||
this.appendDummyInput().appendField("Quand énigme résolue")
|
||||
.appendField(new Blockly.FieldTextInput("1"), "puzzle")
|
||||
.appendField("→ aller à").appendField(new Blockly.FieldTextInput("next_scene"), "target");
|
||||
stack(this, ZACUS_COLOR.scene); this.setInputsInline(true);
|
||||
});
|
||||
|
||||
// Logique
|
||||
def("zacus_logicIf", function () {
|
||||
// condition : reporter branché (appendValueInput "condition") OU champ
|
||||
@@ -618,6 +661,7 @@ export function registerZacusBlocks(Blockly) {
|
||||
|
||||
export const ZACUS_BLOCK_TYPES = [
|
||||
"zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch",
|
||||
"zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved",
|
||||
"zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch",
|
||||
"zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume","zacus_mediaPlay",
|
||||
"zacus_hwLCDText","zacus_hwLCDClear","zacus_hwLCDImage","zacus_hwLCDTouchWait",
|
||||
@@ -645,6 +689,13 @@ export const ZACUS_FIELDS_BY_KIND = {
|
||||
zacus_npcSay: ["text","voice","emotion","rate"],
|
||||
zacus_npcWaitResponse: ["timeout_s"],
|
||||
zacus_npcIntentMatch: ["intent","then"],
|
||||
// Phase D4 — transitions réelles : « Quand … → aller à [target] ».
|
||||
zacus_onButton: ["button","target"],
|
||||
zacus_onAudioDone: ["target"],
|
||||
zacus_onUnlock: ["source","target"],
|
||||
zacus_onSerial: ["token","target"],
|
||||
zacus_onEspnow: ["command","target"],
|
||||
zacus_onPuzzleSolved: ["puzzle","target"],
|
||||
zacus_hwSoundPlay: ["asset"],
|
||||
zacus_hwAudioStop: [],
|
||||
zacus_hwAudioVolume: ["level"],
|
||||
@@ -735,6 +786,7 @@ export const ZACUS_REAL_KINDS = new Set([
|
||||
"zacus_scene","zacus_puzzleQR","zacus_puzzleSound",
|
||||
"zacus_hwOnBoard",
|
||||
"zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch",
|
||||
"zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved",
|
||||
"zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume","zacus_mediaPlay",
|
||||
"zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait",
|
||||
"zacus_logicIf","zacus_logicTimer","zacus_logicScore","zacus_logicSetVar",
|
||||
@@ -763,6 +815,7 @@ export const ZACUS_TOOLBOX_CATEGORIES = [
|
||||
// ── Voie réelle (exécutée on-device ou côté gateway) ──────────────────
|
||||
{ name: "Cartes", colour: ZACUS_COLOR.hardware, types: ["zacus_hwOnBoard"] },
|
||||
{ name: "Scènes", colour: ZACUS_COLOR.scene, types: ["zacus_sceneStart","zacus_sceneEnd","zacus_sceneGoto","zacus_sceneBranch"] },
|
||||
{ name: "Transitions", colour: ZACUS_COLOR.scene, types: ["zacus_onButton","zacus_onAudioDone","zacus_onUnlock","zacus_onSerial","zacus_onEspnow","zacus_onPuzzleSolved"] },
|
||||
{ name: "NPC voix", colour: ZACUS_COLOR.npc, types: ["zacus_npcSay","zacus_npcWaitResponse","zacus_npcIntentMatch"] },
|
||||
{ name: "Audio", colour: ZACUS_COLOR.audio, types: ["zacus_mediaPlay","zacus_hwSoundPlay","zacus_hwAudioStop","zacus_hwAudioVolume"] },
|
||||
{ name: "ESP-NOW", colour: ZACUS_COLOR.espnow, types: ["zacus_espnowRegisterPeer","zacus_espnowSend","zacus_espnowBroadcast","zacus_espnowWait"] },
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Blocks studio export (phase D4): the « Quand … → aller à [target] » blocks
|
||||
become real-event Runtime 3 transitions with the firmware-backed event_type.
|
||||
|
||||
Mapping (blocks_to_runtime3.py):
|
||||
onButton -> {event_type: button, event_name: <button>}
|
||||
onAudioDone -> {event_type: audio_done, event_name: "done"}
|
||||
onUnlock -> {event_type: unlock, event_name: <source>}
|
||||
onSerial -> {event_type: serial, event_name: <token>}
|
||||
onEspnow -> {event_type: espnow, event_name: <command>}
|
||||
onPuzzleSolved -> {event_type: unlock, event_name: "puzzle:<id>"}
|
||||
|
||||
Each carries target_step_id = STEP_<target> (same construction as sceneGoto).
|
||||
EVENT_TYPES is unchanged by D4 — these event_types are all already valid, the
|
||||
phase only emits new transitions. The existing sceneGoto/sceneBranch/
|
||||
npcIntentMatch/logicTimer transitions stay untouched.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(REPO_ROOT / "tools" / "zacus-gateway"))
|
||||
sys.path.insert(0, str(REPO_ROOT / "tools" / "scenario"))
|
||||
|
||||
from blocks_to_runtime3 import compile_blocks # noqa: E402
|
||||
from runtime3_common import EVENT_TYPES, validate_runtime3_document # noqa: E402
|
||||
|
||||
|
||||
def _compile(nodes: list[dict]) -> dict:
|
||||
doc = {"blocks_studio_version": 2, "nodes": nodes}
|
||||
return compile_blocks(yaml.safe_dump(doc), scenario_id="TEST_TRANSITIONS")
|
||||
|
||||
|
||||
def _chain(*nodes: dict) -> list[dict]:
|
||||
out = []
|
||||
for i, node in enumerate(nodes):
|
||||
n = dict(node)
|
||||
n["id"] = f"n{i}"
|
||||
if i + 1 < len(nodes):
|
||||
n["next"] = f"n{i + 1}"
|
||||
out.append(n)
|
||||
return out
|
||||
|
||||
|
||||
START = {"kind": "sceneStart", "params": {"id": "intro"}}
|
||||
# A reachable target step so the document validates (transition target must
|
||||
# point at a declared step). Built with a second sceneStart in the same chain.
|
||||
TARGET = {"kind": "sceneStart", "params": {"id": "fin"}}
|
||||
|
||||
|
||||
def _only_transition(ir: dict) -> dict:
|
||||
transitions = ir["steps"][0]["transitions"]
|
||||
assert len(transitions) == 1, transitions
|
||||
return transitions[0]
|
||||
|
||||
|
||||
class TransitionBlockExport(unittest.TestCase):
|
||||
def test_on_button(self):
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "onButton", "params": {"button": "NEXT", "target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "button")
|
||||
self.assertEqual(t["event_name"], "NEXT")
|
||||
self.assertEqual(t["target_step_id"], "STEP_fin")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_on_audio_done(self):
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "onAudioDone", "params": {"target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "audio_done")
|
||||
self.assertEqual(t["event_name"], "done")
|
||||
self.assertEqual(t["target_step_id"], "STEP_fin")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_on_unlock(self):
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "onUnlock", "params": {"source": "door", "target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "unlock")
|
||||
self.assertEqual(t["event_name"], "door")
|
||||
self.assertEqual(t["target_step_id"], "STEP_fin")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_on_serial(self):
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "onSerial", "params": {"token": "OK", "target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "serial")
|
||||
self.assertEqual(t["event_name"], "OK")
|
||||
self.assertEqual(t["target_step_id"], "STEP_fin")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_on_espnow(self):
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "onEspnow", "params": {"command": "ready", "target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "espnow")
|
||||
self.assertEqual(t["event_name"], "ready")
|
||||
self.assertEqual(t["target_step_id"], "STEP_fin")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_on_puzzle_solved_models_as_named_unlock(self):
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "onPuzzleSolved", "params": {"puzzle": "1", "target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "unlock")
|
||||
self.assertEqual(t["event_name"], "puzzle:1")
|
||||
self.assertEqual(t["target_step_id"], "STEP_fin")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_all_event_types_are_valid(self):
|
||||
for kind, params in (
|
||||
("onButton", {"button": "A", "target": "fin"}),
|
||||
("onAudioDone", {"target": "fin"}),
|
||||
("onUnlock", {"source": "x", "target": "fin"}),
|
||||
("onSerial", {"token": "x", "target": "fin"}),
|
||||
("onEspnow", {"command": "x", "target": "fin"}),
|
||||
("onPuzzleSolved", {"puzzle": "2", "target": "fin"}),
|
||||
):
|
||||
ir = _compile(_chain(START, {"kind": kind, "params": params}, TARGET))
|
||||
t = _only_transition(ir)
|
||||
self.assertIn(t["event_type"], EVENT_TYPES)
|
||||
self.assertTrue(t["target_step_id"])
|
||||
|
||||
def test_target_missing_is_ignored(self):
|
||||
ir = _compile(_chain(START, {"kind": "onButton", "params": {"button": "A"}}))
|
||||
self.assertEqual(ir["steps"][0]["transitions"], [])
|
||||
self.assertTrue(any("onButton without 'target'" in w
|
||||
for w in ir["metadata"]["warnings"]))
|
||||
|
||||
def test_existing_transitions_unchanged(self):
|
||||
# sceneGoto stays event_type action / goto.
|
||||
ir = _compile(_chain(
|
||||
START,
|
||||
{"kind": "sceneGoto", "params": {"target": "fin"}},
|
||||
TARGET,
|
||||
))
|
||||
t = _only_transition(ir)
|
||||
self.assertEqual(t["event_type"], "action")
|
||||
self.assertEqual(t["event_name"], "goto")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
def test_roundtrip_button_and_puzzle_solved(self):
|
||||
# The spec's round-trip proof: a step with onButton(NEXT -> fin) and
|
||||
# onPuzzleSolved(1 -> coffre). Both targets declared as reachable steps.
|
||||
nodes = _chain(
|
||||
START,
|
||||
{"kind": "onButton", "params": {"button": "NEXT", "target": "fin"}},
|
||||
{"kind": "onPuzzleSolved", "params": {"puzzle": "1", "target": "coffre"}},
|
||||
{"kind": "sceneStart", "params": {"id": "fin"}},
|
||||
{"kind": "sceneStart", "params": {"id": "coffre"}},
|
||||
)
|
||||
ir = _compile(nodes)
|
||||
transitions = ir["steps"][0]["transitions"]
|
||||
by_type = {t["event_type"]: t for t in transitions}
|
||||
self.assertEqual(by_type["button"]["event_name"], "NEXT")
|
||||
self.assertEqual(by_type["button"]["target_step_id"], "STEP_fin")
|
||||
self.assertEqual(by_type["unlock"]["event_name"], "puzzle:1")
|
||||
self.assertEqual(by_type["unlock"]["target_step_id"], "STEP_coffre")
|
||||
validate_runtime3_document(ir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -14,6 +14,17 @@ Block-to-IR mapping
|
||||
- `npcIntentMatch` → transition `event_type: action`,
|
||||
`event_name: intent:<intent>`,
|
||||
`target_step_id: STEP_<then>`.
|
||||
- Phase D4 « transitions réelles » — blocs « Quand … → aller à [target] » ;
|
||||
chacun émet UNE transition avec le vrai `event_type` (∈ EVENT_TYPES), même
|
||||
construction `STEP_<target>` que sceneGoto. Aucune extension de schéma.
|
||||
- `onButton` → `{event_type: button, event_name: <button>}`.
|
||||
- `onAudioDone` → `{event_type: audio_done, event_name: "done"}`.
|
||||
- `onUnlock` → `{event_type: unlock, event_name: <source>}`.
|
||||
- `onSerial` → `{event_type: serial, event_name: <token>}`.
|
||||
- `onEspnow` → `{event_type: espnow, event_name: <command>}`.
|
||||
- `onPuzzleSolved`→ `{event_type: unlock, event_name: "puzzle:<id>"}`
|
||||
(le « puzzle résolu » se modélise comme un unlock nommé,
|
||||
EVENT_TYPES n'ayant pas d'event puzzle_state dédié).
|
||||
- `hwServo` → action `{kind: hw_servo, channel, angle}`.
|
||||
- `hwReadQR` → action `{kind: qr_expect, value}` (+ serial transition).
|
||||
- `hwLEDPattern` → action `{kind: led_pattern, pattern}`.
|
||||
@@ -631,6 +642,63 @@ def _node_to_action_or_transition(
|
||||
"id": _to_int(params.get("id"), default=7),
|
||||
"on_success": str(params.get("on_success", "") or "").strip(),
|
||||
})
|
||||
# --- Phase D4 : transitions réelles ---
|
||||
# Blocs « Quand … → aller à [target] » : émettent UNE transition avec le vrai
|
||||
# event_type firmware-backed (∈ runtime3_common.EVENT_TYPES). target → même
|
||||
# construction STEP_{target} que sceneGoto/npcIntentMatch (la normalisation
|
||||
# stricte des ids vit côté runtime3_common._normalize_transition pour le
|
||||
# chemin firmware-import ; le chemin blocks_studio émet STEP_<target> brut,
|
||||
# cohérent avec les transitions existantes). target vide → bloc ignoré.
|
||||
if kind in ("onButton", "onAudioDone", "onUnlock", "onSerial", "onEspnow", "onPuzzleSolved"):
|
||||
target = str(params.get("target", "") or "").strip()
|
||||
if not target:
|
||||
warnings.append(f"{kind} without 'target' ignored")
|
||||
return None
|
||||
target_step_id = f"STEP_{target}"
|
||||
if kind == "onButton":
|
||||
return ("transitions", {
|
||||
"event_type": "button",
|
||||
"event_name": str(params.get("button", "") or "").strip(),
|
||||
"target_step_id": target_step_id,
|
||||
"priority": 5,
|
||||
})
|
||||
if kind == "onAudioDone":
|
||||
return ("transitions", {
|
||||
"event_type": "audio_done",
|
||||
"event_name": "done",
|
||||
"target_step_id": target_step_id,
|
||||
"priority": 5,
|
||||
})
|
||||
if kind == "onUnlock":
|
||||
return ("transitions", {
|
||||
"event_type": "unlock",
|
||||
"event_name": str(params.get("source", "") or "").strip(),
|
||||
"target_step_id": target_step_id,
|
||||
"priority": 5,
|
||||
})
|
||||
if kind == "onSerial":
|
||||
return ("transitions", {
|
||||
"event_type": "serial",
|
||||
"event_name": str(params.get("token", "") or "").strip(),
|
||||
"target_step_id": target_step_id,
|
||||
"priority": 5,
|
||||
})
|
||||
if kind == "onEspnow":
|
||||
return ("transitions", {
|
||||
"event_type": "espnow",
|
||||
"event_name": str(params.get("command", "") or "").strip(),
|
||||
"target_step_id": target_step_id,
|
||||
"priority": 5,
|
||||
})
|
||||
# onPuzzleSolved : « énigme résolue » se modélise comme un unlock nommé
|
||||
# (event_type unlock, event_name puzzle:<id>) — le firmware n'expose pas
|
||||
# d'event_type puzzle_state dédié, et EVENT_TYPES ne s'étend pas en D4.
|
||||
return ("transitions", {
|
||||
"event_type": "unlock",
|
||||
"event_name": f"puzzle:{str(params.get('puzzle', '') or '').strip()}",
|
||||
"target_step_id": target_step_id,
|
||||
"priority": 5,
|
||||
})
|
||||
if kind == "sceneGoto":
|
||||
target = params.get("target", "").strip()
|
||||
return ("transitions", {
|
||||
@@ -832,6 +900,7 @@ def _walk_slot_actions(parent: dict, slot_name: str, nodes_by_id: dict[str, dict
|
||||
kind = node.get("kind", "")
|
||||
_warn_future_hw(kind, warnings, future_hw_seen)
|
||||
if kind in ("sceneStart", "sceneEnd", "sceneGoto", "sceneBranch", "npcIntentMatch", "logicTimer",
|
||||
"onButton", "onAudioDone", "onUnlock", "onSerial", "onEspnow", "onPuzzleSolved",
|
||||
"scene", "puzzleQR", "puzzleSound",
|
||||
"puzzleLED", "puzzleRadio", "puzzleMorse", "puzzleNFC", "puzzleCoffre",
|
||||
"phase", "setParcours", "scorePenalty"):
|
||||
|
||||
Reference in New Issue
Block a user