statable 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. codegen/__init__.py +38 -0
  2. codegen/c_code_generator.py +1752 -0
  3. codegen/code_merger.py +371 -0
  4. codegen/code_templates.py +509 -0
  5. codegen/config.py +167 -0
  6. codegen/enum_generator.py +377 -0
  7. codegen/event_queue_generator.py +303 -0
  8. codegen/interrupt_generator.py +345 -0
  9. codegen/naming_convention.py +171 -0
  10. codegen/osal_generator.py +282 -0
  11. codegen/role_function_generator.py +1244 -0
  12. codegen/sample_data.py +269 -0
  13. codegen/state_actions_generator.py +395 -0
  14. codegen/struct_generator.py +721 -0
  15. codegen/timer_generator.py +299 -0
  16. codegen/transition_generator.py +907 -0
  17. codegen/type_mapper.py +69 -0
  18. codegen/validate/__init__.py +38 -0
  19. codegen/validate/change_actions.py +90 -0
  20. codegen/validate/change_applier.py +416 -0
  21. codegen/validate/clipboard_manager.py +47 -0
  22. codegen/validate/data/__init__.py +30 -0
  23. codegen/validate/data/action_definitions.py +200 -0
  24. codegen/validate/data/keywords.py +46 -0
  25. codegen/validate/data/prompt_templates.py +176 -0
  26. codegen/validate/data/validation_rules.py +205 -0
  27. codegen/validate/items/__init__.py +30 -0
  28. codegen/validate/items/base_validator.py +25 -0
  29. codegen/validate/items/cell_validator.py +275 -0
  30. codegen/validate/items/custom_type_validator.py +75 -0
  31. codegen/validate/items/event_validator.py +78 -0
  32. codegen/validate/items/flag_validator.py +74 -0
  33. codegen/validate/items/interrupt_validator.py +75 -0
  34. codegen/validate/items/queue_validator.py +73 -0
  35. codegen/validate/items/role_function_validator.py +175 -0
  36. codegen/validate/items/state_validator.py +88 -0
  37. codegen/validate/items/timer_validator.py +120 -0
  38. codegen/validate/items/transition_validator.py +113 -0
  39. codegen/validate/items/variable_validator.py +132 -0
  40. codegen/validate/logger.py +52 -0
  41. codegen/validate/models.py +177 -0
  42. codegen/validate/prompt_generator.py +724 -0
  43. codegen/validate/response_parser.py +154 -0
  44. codegen/validate/response_validator.py +275 -0
  45. codegen/validate/validation_dialog.py +322 -0
  46. codegen/validate/validator.py +94 -0
  47. codegen/variable_generator.py +419 -0
  48. statable/__init__.py +71 -0
  49. statable/__main__.py +49 -0
  50. statable/cli.py +251 -0
  51. statable/global_defs.py +256 -0
  52. statable/mermaid_gen.py +144 -0
  53. statable/model.py +426 -0
  54. statable/parser.py +64 -0
  55. statable/sample_data.py +435 -0
  56. statable/shared/__init__.py +24 -0
  57. statable/shared/condition_library.py +63 -0
  58. statable/shared/literal_library.py +66 -0
  59. statable/shared/role_function_library.py +150 -0
  60. statable/state_machine.py +121 -0
  61. statable/xml_io.py +1164 -0
  62. statable-3.1.0.dist-info/METADATA +151 -0
  63. statable-3.1.0.dist-info/RECORD +117 -0
  64. statable-3.1.0.dist-info/WHEEL +5 -0
  65. statable-3.1.0.dist-info/entry_points.txt +2 -0
  66. statable-3.1.0.dist-info/top_level.txt +3 -0
  67. statable_gui/__init__.py +23 -0
  68. statable_gui/action_edit_dialog.py +214 -0
  69. statable_gui/code_generation_dialog.py +486 -0
  70. statable_gui/code_generation_settings_dialog.py +522 -0
  71. statable_gui/common_widgets.py +537 -0
  72. statable_gui/condition_builder_dialog.py +500 -0
  73. statable_gui/condition_edit_dialog.py +123 -0
  74. statable_gui/condition_template_dialog.py +58 -0
  75. statable_gui/config.py +21 -0
  76. statable_gui/dialogs.py +439 -0
  77. statable_gui/event_definition_dialog.py +605 -0
  78. statable_gui/event_delivery_settings_dialog.py +191 -0
  79. statable_gui/event_queue_dialog.py +245 -0
  80. statable_gui/global_defs.py +167 -0
  81. statable_gui/global_defs_dialog.py +796 -0
  82. statable_gui/i18n/__init__.py +118 -0
  83. statable_gui/interrupt_handler_edit_dialog.py +810 -0
  84. statable_gui/layer_settings_dialog.py +180 -0
  85. statable_gui/libcntrl/__init__.py +23 -0
  86. statable_gui/libcntrl/condition_library.py +13 -0
  87. statable_gui/libcntrl/literal_library.py +13 -0
  88. statable_gui/libcntrl/literal_management_dialog.py +185 -0
  89. statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
  90. statable_gui/libcntrl/role_function_library.py +13 -0
  91. statable_gui/literal_definition_dialog.py +62 -0
  92. statable_gui/logger.py +83 -0
  93. statable_gui/main_window.py +1380 -0
  94. statable_gui/matrix_table.py +413 -0
  95. statable_gui/preference_keys.py +18 -0
  96. statable_gui/preferences.py +74 -0
  97. statable_gui/role_function_dialog.py +244 -0
  98. statable_gui/sample_data.py +11 -0
  99. statable_gui/state_actions_dialog.py +398 -0
  100. statable_gui/symbol_picker.py +170 -0
  101. statable_gui/traceball.py +37 -0
  102. statable_gui/transition_editor_direct/__init__.py +15 -0
  103. statable_gui/transition_editor_direct/actions_tab.py +856 -0
  104. statable_gui/transition_editor_direct/code_widget.py +344 -0
  105. statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
  106. statable_gui/transition_editor_direct/dialog.py +335 -0
  107. statable_gui/transition_editor_direct/draft.py +270 -0
  108. statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
  109. statable_gui/transition_editor_direct/overview_tab.py +173 -0
  110. statable_gui/transition_editor_direct/palette_widget.py +233 -0
  111. statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
  112. statable_gui/transition_editor_direct/relations_tab.py +230 -0
  113. statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
  114. statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
  115. statable_gui/transition_editor_direct/transitions_tab.py +620 -0
  116. statable_gui/validation_dialog.py +285 -0
  117. statable_gui/widgets.py +1315 -0
statable/xml_io.py ADDED
@@ -0,0 +1,1164 @@
1
+ # statable/xml_io.py
2
+ """
3
+ XML I/O for StaTable.
4
+
5
+ Version History
6
+ ---------------
7
+ v2.2 - State.entry / exit: str -> List[str]
8
+ Transition.early_return / label
9
+ Cell-level metadata: <Cells><Cell>...</Cell></Cells>
10
+ §12-5: TransitionRelation.children (recursive nesting)
11
+
12
+ v3.7 - Reserved fields documented:
13
+ * State.do
14
+ * RoleFunction.return_type / arg1_type / arg1_name
15
+ / arg2_type / arg2_name
16
+ These are still read/written for backward compatibility,
17
+ even though the GUI no longer exposes them.
18
+
19
+ v3.8 - RoleFunction: persist used_global_vars / used_events /
20
+ used_literals as comma-separated attributes. Old XML
21
+ without these attributes loads with empty lists.
22
+ Also applies to the shared library (role_function_library).
23
+
24
+ v3.8.1 - Only emit used_* attributes when non-empty. This keeps
25
+ the canonical XML of pre-v3.8 project files identical
26
+ after a save, so that test_v2_2_p12_9.py's canonical
27
+ tree comparison (Level 1) still passes.
28
+ Empty values still load as [] via _split_csv().
29
+
30
+ v3.8.2 - role_function_library_to_element / _from_element now
31
+ also emit / read the reserved signature fields
32
+ (return_type / arg1_* / arg2_*) of the shared library.
33
+ The test fixture tests/data/v22_features_test3.xml
34
+ contains these attributes in <SharedLibraries>, so
35
+ omitting them caused a canonical XML mismatch.
36
+ libcntrl.RoleFunction carries these fields since v1.5.
37
+ """
38
+
39
+ import xml.etree.ElementTree as ET
40
+ import logging
41
+ from typing import List, Tuple, Optional
42
+
43
+ from .model import (
44
+ State, Event, Transition, StateType, EventKind, RoleFunction,
45
+ EventDeliveryType, EventSourceLayer,
46
+ ActionStep, TransitionRelation, EventTrigger,
47
+ )
48
+ from .state_machine import StateMachine
49
+ from .global_defs import (
50
+ GlobalDefinitions, SystemVariable, EventFlag,
51
+ InterruptHandlerDef, InterruptAction,
52
+ DevicePlaceholderDef, TimerBaseDef, TimerDerivedDef,
53
+ EventQueueDef, CustomTypeDef, StructMemberDef,
54
+ )
55
+
56
+ logger = logging.getLogger("statable.xml_io")
57
+
58
+ try:
59
+ from statable.shared.role_function_library import (
60
+ RoleFunctionLibrary, RoleFunction as LibRoleFunction)
61
+ from statable.shared.condition_library import (
62
+ ConditionLibrary, ConditionTemplate)
63
+ from statable.shared.literal_library import (
64
+ LiteralLibrary, LiteralDefinition)
65
+ except ImportError:
66
+ RoleFunctionLibrary = ConditionLibrary = LiteralLibrary = None
67
+ LibRoleFunction = ConditionTemplate = LiteralDefinition = None
68
+
69
+
70
+ # ======================================================================
71
+ # Helpers: string / list normalization
72
+ # ======================================================================
73
+ def _normalize_actions(value) -> List[str]:
74
+ """Always normalize action collections to List[str]."""
75
+ if value is None:
76
+ return []
77
+ if isinstance(value, str):
78
+ stripped = value.strip()
79
+ return [stripped] if stripped else []
80
+ if isinstance(value, list):
81
+ if value and all(isinstance(s, str) and len(s) == 1 for s in value):
82
+ joined = "".join(value)
83
+ logger.warning(
84
+ f"Detected 1-char split actions, joining: {value} -> ['{joined}']"
85
+ )
86
+ return [joined]
87
+ return [str(s) for s in value if str(s).strip()]
88
+ logger.warning(f"Unexpected type for actions: {type(value)}. Converting to str.")
89
+ return [str(value)]
90
+
91
+
92
+ def _normalize_str_list(value) -> List[str]:
93
+ return _normalize_actions(value)
94
+
95
+
96
+ def _split_csv(text: str) -> List[str]:
97
+ """Split a comma-separated attribute value into a clean list."""
98
+ if not text:
99
+ return []
100
+ return [x.strip() for x in text.split(",") if x.strip()]
101
+
102
+
103
+ def _add_used_attrs(attrs: dict, rf) -> None:
104
+ """[v3.8.1] Attach used_* attributes only when non-empty.
105
+
106
+ This keeps canonical XML identical for project files created
107
+ before v3.8: a role function with no symbol references produces
108
+ no used_* attributes at all, instead of three empty ones.
109
+ """
110
+ for attr_name, field_name in (
111
+ ("used_global_vars", "used_global_vars"),
112
+ ("used_events", "used_events"),
113
+ ("used_literals", "used_literals"),
114
+ ):
115
+ values = list(getattr(rf, field_name, []) or [])
116
+ if values:
117
+ attrs[attr_name] = ",".join(str(v) for v in values)
118
+
119
+
120
+ # ======================================================================
121
+ # v2.2 §12-5: Recursive relation serialization
122
+ # ======================================================================
123
+ def _relation_to_element(rel) -> ET.Element:
124
+ """Serialize one TransitionRelation recursively."""
125
+ elem = ET.Element(
126
+ "Relation",
127
+ kind=getattr(rel, "kind", "sequential") or "sequential",
128
+ members=",".join(getattr(rel, "members", []) or []),
129
+ shared_condition=getattr(rel, "shared_condition", "") or "",
130
+ note=getattr(rel, "note", "") or "",
131
+ )
132
+ children = getattr(rel, "children", None) or []
133
+ if children:
134
+ children_elem = ET.SubElement(elem, "Children")
135
+ for c in children:
136
+ children_elem.append(_relation_to_element(c))
137
+ return elem
138
+
139
+
140
+ def _relation_from_element(elem) -> TransitionRelation:
141
+ """Deserialize one TransitionRelation recursively."""
142
+ members_str = elem.get("members", "")
143
+ members = [m.strip() for m in members_str.split(",") if m.strip()]
144
+ rel = TransitionRelation(
145
+ kind=elem.get("kind", "sequential"),
146
+ members=members,
147
+ shared_condition=elem.get("shared_condition", ""),
148
+ note=elem.get("note", ""),
149
+ )
150
+ children_elem = elem.find("Children")
151
+ if children_elem is not None:
152
+ rel.children = [
153
+ _relation_from_element(c)
154
+ for c in children_elem.findall("Relation")
155
+ ]
156
+ return rel
157
+
158
+
159
+ # ======================================================================
160
+ # StateMachine -> XML
161
+ # ======================================================================
162
+ def _state_to_element(state: State) -> ET.Element:
163
+ """Serialize one State (v2.2: entry/exit as child elements).
164
+
165
+ [v3.7] `do` is a reserved field (not exposed in UI). It is
166
+ still written to XML for backward compatibility.
167
+ """
168
+ attrs = {
169
+ "name": state.name,
170
+ "type": state.type.value,
171
+ "parent": state.parent or "",
172
+ # [Reserved] Not exposed in UI / not used by codegen.
173
+ # Kept in XML output for backward compatibility.
174
+ "do": state.do,
175
+ "description": state.description,
176
+ }
177
+ elem = ET.Element("State", **attrs)
178
+
179
+ # [v2.7.0] entry / exit / do_actions are List[ActionStep]
180
+ _action_list_to_subelement(elem, "Entry",
181
+ getattr(state, 'entry', []) or [])
182
+ _action_list_to_subelement(elem, "Exit",
183
+ getattr(state, 'exit', []) or [])
184
+ _action_list_to_subelement(elem, "Do",
185
+ getattr(state, 'do_actions', []) or [])
186
+
187
+ return elem
188
+
189
+
190
+ def _action_list_to_subelement(parent, tag: str, actions) -> None:
191
+ """[v2.7.0] Serialize List[ActionStep] to <Entry>/<Exit>/<Do>.
192
+
193
+ Only emits the container when non-empty. ActionStep fields are
194
+ stored as XML attributes (role_function / condition / action_type
195
+ / event_name / title). Empty/default values are omitted to keep
196
+ canonical XML compact.
197
+ """
198
+ if not actions:
199
+ return
200
+ container = ET.SubElement(parent, tag)
201
+ for a in actions:
202
+ attrs = {}
203
+ role = getattr(a, 'role_function', '') or ''
204
+ cond = getattr(a, 'condition', '') or ''
205
+ atype = getattr(a, 'action_type', 'role') or 'role'
206
+ ename = getattr(a, 'event_name', '') or ''
207
+ title = getattr(a, 'title', '') or ''
208
+ # [v2.7.0 backward-compat] Simple role-only actions emit
209
+ # legacy <Action name="X"/> to keep canonical XML identical
210
+ # for pre-v2.7 project files.
211
+ is_simple = (role and not cond and not ename and atype == 'role')
212
+ if is_simple:
213
+ attrs['name'] = role
214
+ else:
215
+ if role:
216
+ attrs['role_function'] = role
217
+ if atype and atype != 'role':
218
+ attrs['action_type'] = atype
219
+ if ename:
220
+ attrs['event_name'] = ename
221
+ if cond:
222
+ attrs['condition'] = cond
223
+ # title is auto-generated; skip to keep XML compact
224
+ ET.SubElement(container, "Action", **attrs)
225
+
226
+
227
+ def _state_from_element(elem: ET.Element) -> State:
228
+ """Deserialize one State (supports old and new formats).
229
+
230
+ [v3.7] `do` is a reserved field. It is still read from XML so
231
+ that old projects round-trip without data loss, even though the
232
+ GUI does not expose it.
233
+ """
234
+ # [v2.7.0] entry / exit / do_actions as List[ActionStep]
235
+ entry_list = _actions_from_subelement(
236
+ elem.find("Entry"), elem.get("entry", ""))
237
+ exit_list = _actions_from_subelement(
238
+ elem.find("Exit"), elem.get("exit", ""))
239
+ do_actions_list = _actions_from_subelement(elem.find("Do"), "")
240
+
241
+ return State(
242
+ name=elem.get("name", ""),
243
+ type=StateType(elem.get("type", "normal")),
244
+ parent=elem.get("parent") or None,
245
+ entry=entry_list,
246
+ exit=exit_list,
247
+ do_actions=do_actions_list,
248
+ # [Reserved] preserved through XML I/O for backward compatibility.
249
+ do=elem.get("do", ""),
250
+ description=elem.get("description", ""),
251
+ )
252
+
253
+
254
+ def _actions_from_subelement(container, legacy_csv: str) -> List[ActionStep]:
255
+ """[v2.7.0] Deserialize <Entry>/<Exit>/<Do> to List[ActionStep].
256
+
257
+ Supports:
258
+ 1. New format: <Action role_function="..." condition="..."
259
+ action_type="..." event_name="..." />
260
+ 2. Legacy format (entry/exit only): <Action name="..." />
261
+ 3. Legacy attribute (entry/exit only): <State entry="A,B" />
262
+ """
263
+ result: List[ActionStep] = []
264
+ if container is not None:
265
+ for a_elem in container.findall("Action"):
266
+ # New format uses role_function; legacy uses name
267
+ role = a_elem.get("role_function", "") or a_elem.get("name", "")
268
+ cond = a_elem.get("condition", "")
269
+ atype = a_elem.get("action_type", "role") or "role"
270
+ ename = a_elem.get("event_name", "")
271
+ title = a_elem.get("title", "")
272
+ if not role and not ename:
273
+ continue
274
+ result.append(ActionStep(
275
+ role_function=role,
276
+ condition=cond,
277
+ action_type=atype,
278
+ event_name=ename,
279
+ title=title,
280
+ ))
281
+ return result
282
+
283
+ # Legacy: <State entry="A,B" /> — comma-separated
284
+ if legacy_csv:
285
+ for name in _split_csv(legacy_csv):
286
+ result.append(ActionStep(role_function=name))
287
+ return result
288
+
289
+
290
+ def _event_trigger_from_element(elem) -> Optional["EventTrigger"]:
291
+ """[C-51 Step 3] Deserialize <Trigger> child element (or None)."""
292
+ if elem is None:
293
+ return None
294
+ return EventTrigger.from_dict(dict(elem.attrib))
295
+
296
+
297
+ def state_machine_to_element(sm: StateMachine) -> ET.Element:
298
+ """Convert StateMachine to XML Element."""
299
+ logger.debug(
300
+ f"state_machine_to_element: states={len(sm.states)}, "
301
+ f"events={len(sm.events)}, roles={len(sm.role_functions)}, "
302
+ f"transitions={len(sm.transitions)}"
303
+ )
304
+
305
+ root = ET.Element("StateMachine")
306
+ if sm.initial_state:
307
+ root.set("initial", sm.initial_state)
308
+
309
+ root.set("layer_priority", str(getattr(sm, 'layer_priority', 5)))
310
+ root.set("layer_description", getattr(sm, 'layer_description', ''))
311
+ root.set("layer_name", getattr(sm, 'layer_name', ''))
312
+
313
+ # States
314
+ states_elem = ET.SubElement(root, "States")
315
+ for state in sm.states.values():
316
+ states_elem.append(_state_to_element(state))
317
+
318
+ # Events
319
+ events_elem = ET.SubElement(root, "Events")
320
+ for event in sm.events.values():
321
+ attrs = {
322
+ "name": event.name,
323
+ "id": str(event.id) if event.id is not None else "",
324
+ "kind": event.kind.value,
325
+ "params": ",".join(event.params),
326
+ "priority": str(event.priority),
327
+ "description": event.description,
328
+ "delivery_type": event.delivery_type.value,
329
+ "source_layer": event.source_layer.value,
330
+ "data_type": event.data_type,
331
+ "data_name": event.data_name,
332
+ "title": event.title,
333
+ }
334
+ # [C-51 Step 2] Emit trigger only when non-empty, so
335
+ # pre-C-51 projects round-trip with identical XML.
336
+ _trigger = getattr(event, 'trigger', '') or ''
337
+ if _trigger:
338
+ attrs['trigger'] = _trigger
339
+ event_elem = ET.SubElement(events_elem, "Event", **attrs)
340
+ # [C-51 Step 3] Structured trigger detail (optional)
341
+ _td = getattr(event, 'trigger_detail', None)
342
+ if _td is not None:
343
+ td_attrs = {k: str(v) for k, v in _td.to_dict().items()}
344
+ ET.SubElement(event_elem, "Trigger", **td_attrs)
345
+
346
+ # RoleFunctions
347
+ # [v3.7] return_type / arg1_* / arg2_* are reserved fields
348
+ # (not exposed in UI, not used by codegen). They are still
349
+ # written to XML for backward compatibility.
350
+ # [v3.8] used_global_vars / used_events / used_literals are
351
+ # written as comma-separated attributes.
352
+ # [v3.8.1] used_* attributes are only emitted when non-empty,
353
+ # so pre-v3.8 files round-trip with identical canonical XML.
354
+ roles_elem = ET.SubElement(root, "RoleFunctions")
355
+ for rf in sm.role_functions.values():
356
+ attrs = {
357
+ "name": rf.name,
358
+ "namespace": getattr(rf, 'namespace', ''),
359
+ "description": rf.description,
360
+ # [Reserved] kept for backward compatibility.
361
+ "return_type": rf.return_type,
362
+ "arg1_type": rf.arg1_type,
363
+ "arg1_name": rf.arg1_name,
364
+ "arg2_type": rf.arg2_type,
365
+ "arg2_name": rf.arg2_name,
366
+ "title": rf.title,
367
+ }
368
+ _add_used_attrs(attrs, rf)
369
+ ET.SubElement(roles_elem, "RoleFunction", **attrs)
370
+
371
+ # Transitions
372
+ trans_elem = ET.SubElement(root, "Transitions")
373
+ for t in sm.transitions:
374
+ pre_actions = _normalize_actions(getattr(t, 'pre_actions', []))
375
+ else_actions = _normalize_actions(getattr(t, 'else_actions', []))
376
+ has_else = getattr(t, 'has_else', True)
377
+ else_target = getattr(t, 'else_target', '')
378
+ early_return = getattr(t, 'early_return', False)
379
+ label = getattr(t, 'label', '')
380
+
381
+ attrs = {
382
+ "source": t.source,
383
+ "event": t.event,
384
+ "condition": t.condition,
385
+ "action": t.action,
386
+ "target": t.target,
387
+ "transition_type": t.transition_type,
388
+ "title": t.title,
389
+ "has_else": "true" if has_else else "false",
390
+ "else_target": else_target,
391
+ "early_return": "true" if early_return else "false",
392
+ }
393
+ if label:
394
+ attrs["label"] = label
395
+
396
+ trans_child = ET.SubElement(trans_elem, "Transition", **attrs)
397
+ for pre in pre_actions:
398
+ ET.SubElement(trans_child, "PreAction", action=str(pre))
399
+ for ea in else_actions:
400
+ ET.SubElement(trans_child, "ElseAction", action=str(ea))
401
+
402
+ # Cells (v2.2)
403
+ cell_keys = sm.get_cell_keys()
404
+ if cell_keys:
405
+ cells_elem = ET.SubElement(root, "Cells")
406
+ for (source, event) in cell_keys:
407
+ cell_elem = ET.SubElement(
408
+ cells_elem, "Cell",
409
+ source=source, event=event,
410
+ )
411
+
412
+ actions = sm.get_actions_for_cell(source, event)
413
+ if actions:
414
+ actions_elem = ET.SubElement(cell_elem, "Actions")
415
+ for a in actions:
416
+ ET.SubElement(actions_elem, "Action",
417
+ role_function=a.role_function,
418
+ trigger=a.trigger,
419
+ title=a.title)
420
+
421
+ relations = sm.get_relations_for_cell(source, event)
422
+ if relations:
423
+ rels_elem = ET.SubElement(cell_elem, "Relations")
424
+ for r in relations:
425
+ rels_elem.append(_relation_to_element(r))
426
+
427
+ return root
428
+
429
+
430
+ def state_machine_from_element(elem: ET.Element) -> StateMachine:
431
+ """Build StateMachine from XML Element.
432
+
433
+ [v3.7] Reserved role-function fields (return_type / arg1_* /
434
+ arg2_*) are still read here so that old projects round-trip
435
+ without data loss.
436
+
437
+ [v3.8] used_global_vars / used_events / used_literals are read
438
+ from comma-separated attributes; missing -> [].
439
+ """
440
+ logger.debug(f"state_machine_from_element: tag={elem.tag}")
441
+
442
+ sm = StateMachine()
443
+ initial_state_name = elem.get("initial")
444
+
445
+ try:
446
+ sm.layer_priority = int(elem.get("layer_priority", "5"))
447
+ except ValueError:
448
+ sm.layer_priority = 5
449
+ sm.layer_description = elem.get("layer_description", "")
450
+ sm.layer_name = elem.get("layer_name", "")
451
+
452
+ # States
453
+ states_elem = elem.find("States")
454
+ if states_elem is None:
455
+ logger.error(" <States> element not found!")
456
+ return sm
457
+ for state_elem in states_elem:
458
+ try:
459
+ sm.add_state(_state_from_element(state_elem))
460
+ except Exception as e:
461
+ logger.error(f" Failed to load state: {e}", exc_info=True)
462
+
463
+ # Events
464
+ events_elem = elem.find("Events")
465
+ if events_elem is not None:
466
+ for event_elem in events_elem:
467
+ params_str = event_elem.get("params", "")
468
+ params = [p.strip() for p in params_str.split(",") if p.strip()] if params_str else []
469
+ try:
470
+ sm.add_event(Event(
471
+ name=event_elem.get("name", ""),
472
+ id=int(event_elem.get("id")) if event_elem.get("id") else None,
473
+ kind=EventKind(event_elem.get("kind", "signal")),
474
+ params=params,
475
+ priority=int(event_elem.get("priority", 0)),
476
+ description=event_elem.get("description", ""),
477
+ delivery_type=EventDeliveryType(event_elem.get("delivery_type", "direct")),
478
+ source_layer=EventSourceLayer(event_elem.get("source_layer", "driver")),
479
+ data_type=event_elem.get("data_type", ""),
480
+ data_name=event_elem.get("data_name", ""),
481
+ title=event_elem.get("title", ""),
482
+ # [C-51 Step 2] Free-text trigger; missing -> ""
483
+ trigger=event_elem.get("trigger", ""),
484
+ # [C-51 Step 3] Structured trigger detail
485
+ trigger_detail=_event_trigger_from_element(
486
+ event_elem.find("Trigger")),
487
+ ))
488
+ except Exception as e:
489
+ logger.error(f" Failed to load event: {e}", exc_info=True)
490
+
491
+ # RoleFunctions
492
+ # [v3.7] return_type / arg1_* / arg2_* are reserved fields.
493
+ # [v3.8] used_global_vars / used_events / used_literals are read
494
+ # from comma-separated attributes; missing -> [].
495
+ roles_elem = elem.find("RoleFunctions")
496
+ if roles_elem is not None:
497
+ for rf_elem in roles_elem:
498
+ raw_name = rf_elem.get("name", "")
499
+ namespace = rf_elem.get("namespace", "")
500
+ if not namespace and sm.layer_name:
501
+ legacy_prefix = f"{sm.layer_name}_"
502
+ if raw_name.startswith(legacy_prefix):
503
+ namespace = sm.layer_name
504
+ raw_name = raw_name[len(legacy_prefix):]
505
+ try:
506
+ sm.add_role_function(RoleFunction(
507
+ name=raw_name,
508
+ namespace=namespace,
509
+ description=rf_elem.get("description", ""),
510
+ # [Reserved] preserved for backward compatibility.
511
+ return_type=rf_elem.get("return_type", "int"),
512
+ arg1_type=rf_elem.get("arg1_type", "int"),
513
+ arg1_name=rf_elem.get("arg1_name", "arg1"),
514
+ arg2_type=rf_elem.get("arg2_type", "int"),
515
+ arg2_name=rf_elem.get("arg2_name", "arg2"),
516
+ title=rf_elem.get("title", ""),
517
+ # [v3.8] GUI symbol tracking.
518
+ used_global_vars=_split_csv(
519
+ rf_elem.get("used_global_vars", "")),
520
+ used_events=_split_csv(
521
+ rf_elem.get("used_events", "")),
522
+ used_literals=_split_csv(
523
+ rf_elem.get("used_literals", "")),
524
+ ))
525
+ except Exception as e:
526
+ logger.error(f" Failed to load role_function: {e}", exc_info=True)
527
+
528
+ # Transitions
529
+ trans_elem = elem.find("Transitions")
530
+ if trans_elem is not None:
531
+ for trans_elem_child in trans_elem:
532
+ pre_actions_raw = [
533
+ p.get("action", "")
534
+ for p in trans_elem_child.findall("PreAction")
535
+ ]
536
+ else_actions_raw = [
537
+ e.get("action", "")
538
+ for e in trans_elem_child.findall("ElseAction")
539
+ ]
540
+ pre_actions = _normalize_actions(pre_actions_raw)
541
+ else_actions = _normalize_actions(else_actions_raw)
542
+
543
+ has_else = trans_elem_child.get("has_else", "true").lower() == "true"
544
+ else_target = trans_elem_child.get("else_target", "")
545
+ early_return = trans_elem_child.get("early_return", "false").lower() == "true"
546
+ label = trans_elem_child.get("label", "")
547
+
548
+ try:
549
+ sm.add_transition(Transition(
550
+ source=trans_elem_child.get("source", ""),
551
+ event=trans_elem_child.get("event", ""),
552
+ condition=trans_elem_child.get("condition", ""),
553
+ action=trans_elem_child.get("action", ""),
554
+ target=trans_elem_child.get("target", ""),
555
+ transition_type=trans_elem_child.get("transition_type", "external"),
556
+ title=trans_elem_child.get("title", ""),
557
+ pre_actions=pre_actions,
558
+ has_else=has_else,
559
+ else_target=else_target,
560
+ else_actions=else_actions,
561
+ early_return=early_return,
562
+ label=label,
563
+ ))
564
+ except Exception as e:
565
+ logger.error(f" Failed to load transition: {e}", exc_info=True)
566
+
567
+ # Cells (v2.2)
568
+ cells_elem = elem.find("Cells")
569
+ if cells_elem is not None:
570
+ for cell_elem in cells_elem.findall("Cell"):
571
+ source = cell_elem.get("source", "")
572
+ event = cell_elem.get("event", "")
573
+
574
+ actions_elem = cell_elem.find("Actions")
575
+ if actions_elem is not None:
576
+ actions = []
577
+ for a_elem in actions_elem.findall("Action"):
578
+ actions.append(ActionStep(
579
+ role_function=a_elem.get("role_function", ""),
580
+ trigger=a_elem.get("trigger", "before_transitions"),
581
+ title=a_elem.get("title", ""),
582
+ ))
583
+ sm.set_actions_for_cell(source, event, actions)
584
+
585
+ rels_elem = cell_elem.find("Relations")
586
+ if rels_elem is not None:
587
+ relations = [
588
+ _relation_from_element(r_elem)
589
+ for r_elem in rels_elem.findall("Relation")
590
+ ]
591
+ sm.set_relations_for_cell(source, event, relations)
592
+
593
+ if initial_state_name:
594
+ sm.set_initial(initial_state_name)
595
+
596
+ logger.debug(
597
+ f"state_machine_from_element: loaded states={len(sm.states)}, "
598
+ f"events={len(sm.events)}, roles={len(sm.role_functions)}, "
599
+ f"transitions={len(sm.transitions)}, "
600
+ f"cells={len(sm.get_cell_keys())}"
601
+ )
602
+ return sm
603
+
604
+
605
+ # ======================================================================
606
+ # GlobalDefinitions -> XML (unchanged)
607
+ # ======================================================================
608
+ def _timer_to_element(parent: ET.Element, timer: TimerBaseDef, tag: str = "Timer"):
609
+ elem = ET.SubElement(parent, tag)
610
+ elem.set("variable_name", timer.variable_name)
611
+ elem.set("unit", timer.unit)
612
+ elem.set("data_type", timer.data_type)
613
+ elem.set("title", timer.title)
614
+ elem.set("interrupt_name", timer.interrupt_name)
615
+ for derived in timer.derived:
616
+ ET.SubElement(elem, "Derived", **{
617
+ "period_name": derived.period_name,
618
+ "multiplier": str(derived.multiplier),
619
+ "variable_name": derived.variable_name,
620
+ "data_type": derived.data_type,
621
+ "title": derived.title,
622
+ })
623
+ return elem
624
+
625
+
626
+ def _timer_from_element(elem: ET.Element) -> TimerBaseDef:
627
+ derived_list = []
628
+ for d_elem in elem.findall("Derived"):
629
+ try:
630
+ mult = int(d_elem.get("multiplier", "1"))
631
+ except ValueError:
632
+ mult = 1
633
+ derived_list.append(TimerDerivedDef(
634
+ period_name=d_elem.get("period_name", ""),
635
+ multiplier=mult,
636
+ variable_name=d_elem.get("variable_name", ""),
637
+ data_type=d_elem.get("data_type", "uint8_t"),
638
+ title=d_elem.get("title", ""),
639
+ ))
640
+ return TimerBaseDef(
641
+ variable_name=elem.get("variable_name", "g_system_tick"),
642
+ unit=elem.get("unit", "1ms"),
643
+ data_type=elem.get("data_type", "volatile uint32_t"),
644
+ derived=derived_list,
645
+ title=elem.get("title", ""),
646
+ interrupt_name=elem.get("interrupt_name", ""),
647
+ )
648
+
649
+
650
+ def global_defs_to_element(defs: GlobalDefinitions) -> ET.Element:
651
+ root = ET.Element("GlobalDefinitions")
652
+
653
+ if defs.custom_types:
654
+ ct_elem = ET.SubElement(root, "CustomTypes")
655
+ for ct in defs.custom_types:
656
+ ct_child = ET.SubElement(ct_elem, "CustomType", **{
657
+ "name": ct.name, "description": ct.description, "title": ct.title,
658
+ })
659
+ for member in ct.members:
660
+ ET.SubElement(ct_child, "Member", **{
661
+ "name": member.name, "data_type": member.data_type,
662
+ "bit_width": str(member.bit_width),
663
+ "description": member.description, "title": member.title,
664
+ "array_size": str(member.array_size),
665
+ })
666
+
667
+ vars_elem = ET.SubElement(root, "SystemVariables")
668
+ for var in defs.variables:
669
+ ET.SubElement(vars_elem, "Variable", **{
670
+ "name": var.name, "type": var.type, "unit": var.unit,
671
+ "default_value": var.default_value, "group": var.group,
672
+ "description": var.description, "title": var.title,
673
+ "array_size": str(var.array_size),
674
+ })
675
+
676
+ flags_elem = ET.SubElement(root, "EventFlags")
677
+ for flag in defs.flags:
678
+ ET.SubElement(flags_elem, "Flag", **{
679
+ "name": flag.name, "min_value": str(flag.min_value),
680
+ "max_value": str(flag.max_value), "group": flag.group,
681
+ "description": flag.description, "title": flag.title,
682
+ })
683
+
684
+ if defs.interrupts:
685
+ intrs_elem = ET.SubElement(root, "Interrupts")
686
+ for intr in defs.interrupts:
687
+ intr_child = ET.SubElement(intrs_elem, "Interrupt", **{
688
+ "name": intr.name, "description": intr.description,
689
+ "event_names": ",".join(intr.event_names),
690
+ "is_timer": "true" if intr.is_timer else "false",
691
+ "title": intr.title,
692
+ })
693
+ for act in intr.actions:
694
+ ET.SubElement(intr_child, "Action", **{
695
+ "condition": act.condition, "action": act.action,
696
+ })
697
+ for ref in getattr(intr, 'used_role_functions', []):
698
+ ET.SubElement(intr_child, "UsedRoleFunction", ref=ref)
699
+ for var in getattr(intr, 'used_variables', []):
700
+ ET.SubElement(intr_child, "UsedVariable", name=var)
701
+
702
+ if defs.placeholders:
703
+ ph_elem = ET.SubElement(root, "DevicePlaceholders")
704
+ for ph in defs.placeholders:
705
+ ET.SubElement(ph_elem, "Placeholder", **{
706
+ "name": ph.name, "description": ph.description, "title": ph.title,
707
+ })
708
+
709
+ timer_elem = ET.SubElement(root, "TimerBase")
710
+ _timer_to_element(timer_elem, defs.timer_base, tag="Timer")
711
+
712
+ if defs.extra_timers:
713
+ extras_elem = ET.SubElement(root, "ExtraTimers")
714
+ for timer in defs.extra_timers:
715
+ _timer_to_element(extras_elem, timer, tag="Timer")
716
+
717
+ if defs.event_queues:
718
+ queues_elem = ET.SubElement(root, "EventQueues")
719
+ for q in defs.event_queues:
720
+ ET.SubElement(queues_elem, "Queue", **{
721
+ "name": q.name, "size": str(q.size),
722
+ "element_type": q.element_type,
723
+ "event_ids": ",".join(q.event_ids),
724
+ "priority_enabled": "true" if q.priority_enabled else "false",
725
+ "interrupt_safe": "true" if q.interrupt_safe else "false",
726
+ "rtos_enabled": "true" if q.rtos_enabled else "false",
727
+ "description": q.description, "title": q.title,
728
+ })
729
+
730
+ return root
731
+
732
+
733
+ def global_defs_from_element(elem: ET.Element) -> GlobalDefinitions:
734
+ defs = GlobalDefinitions()
735
+
736
+ ct_elem = elem.find("CustomTypes")
737
+ if ct_elem is not None:
738
+ for ct in ct_elem:
739
+ members = []
740
+ for m in ct.findall("Member"):
741
+ try:
742
+ bw = int(m.get("bit_width", "0"))
743
+ except ValueError:
744
+ bw = 0
745
+ try:
746
+ arr = int(m.get("array_size", "0"))
747
+ except ValueError:
748
+ arr = 0
749
+ members.append(StructMemberDef(
750
+ name=m.get("name", ""), data_type=m.get("data_type", ""),
751
+ bit_width=bw, description=m.get("description", ""),
752
+ title=m.get("title", ""), array_size=arr,
753
+ ))
754
+ defs.custom_types.append(CustomTypeDef(
755
+ name=ct.get("name", ""), description=ct.get("description", ""),
756
+ members=members, title=ct.get("title", ""),
757
+ ))
758
+
759
+ vars_elem = elem.find("SystemVariables")
760
+ if vars_elem is not None:
761
+ for var_elem in vars_elem:
762
+ try:
763
+ arr = int(var_elem.get("array_size", "0"))
764
+ except ValueError:
765
+ arr = 0
766
+ defs.variables.append(SystemVariable(
767
+ name=var_elem.get("name", ""), type=var_elem.get("type", ""),
768
+ unit=var_elem.get("unit", ""),
769
+ default_value=var_elem.get("default_value", ""),
770
+ group=var_elem.get("group", ""),
771
+ description=var_elem.get("description", ""),
772
+ title=var_elem.get("title", ""), array_size=arr,
773
+ ))
774
+
775
+ flags_elem = elem.find("EventFlags")
776
+ if flags_elem is not None:
777
+ for flag_elem in flags_elem:
778
+ try:
779
+ min_val = int(flag_elem.get("min_value", "0"))
780
+ max_val = int(flag_elem.get("max_value", "0"))
781
+ except ValueError:
782
+ min_val, max_val = 0, 0
783
+ defs.flags.append(EventFlag(
784
+ name=flag_elem.get("name", ""), min_value=min_val,
785
+ max_value=max_val, group=flag_elem.get("group", ""),
786
+ description=flag_elem.get("description", ""),
787
+ title=flag_elem.get("title", ""),
788
+ ))
789
+
790
+ intrs_elem = elem.find("Interrupts")
791
+ if intrs_elem is not None:
792
+ for intr_elem in intrs_elem:
793
+ actions = []
794
+ for act_elem in intr_elem.findall("Action"):
795
+ actions.append(InterruptAction(
796
+ condition=act_elem.get("condition", ""),
797
+ action=act_elem.get("action", ""),
798
+ ))
799
+ used_rfs = [r.get("ref", "")
800
+ for r in intr_elem.findall("UsedRoleFunction")
801
+ if r.get("ref")]
802
+ used_vars = [v.get("name", "")
803
+ for v in intr_elem.findall("UsedVariable")
804
+ if v.get("name")]
805
+ es = intr_elem.get("event_names", "")
806
+ defs.interrupts.append(InterruptHandlerDef(
807
+ name=intr_elem.get("name", ""),
808
+ description=intr_elem.get("description", ""),
809
+ event_names=[e.strip() for e in es.split(",") if e.strip()] if es else [],
810
+ is_timer=intr_elem.get("is_timer", "false").lower() == "true",
811
+ actions=actions,
812
+ title=intr_elem.get("title", ""),
813
+ used_role_functions=used_rfs,
814
+ used_variables=used_vars,
815
+ ))
816
+
817
+ ph_elem = elem.find("DevicePlaceholders")
818
+ if ph_elem is not None:
819
+ for ph in ph_elem:
820
+ defs.placeholders.append(DevicePlaceholderDef(
821
+ name=ph.get("name", ""), description=ph.get("description", ""),
822
+ title=ph.get("title", ""),
823
+ ))
824
+
825
+ timer_elem = elem.find("TimerBase")
826
+ if timer_elem is not None:
827
+ inner = timer_elem.find("Timer")
828
+ if inner is not None:
829
+ defs.timer_base = _timer_from_element(inner)
830
+
831
+ extras_elem = elem.find("ExtraTimers")
832
+ if extras_elem is not None:
833
+ for inner in extras_elem.findall("Timer"):
834
+ defs.extra_timers.append(_timer_from_element(inner))
835
+
836
+ queues_elem = elem.find("EventQueues")
837
+ if queues_elem is not None:
838
+ for q_elem in queues_elem:
839
+ es = q_elem.get("event_ids", "")
840
+ try:
841
+ size = int(q_elem.get("size", "8"))
842
+ except ValueError:
843
+ size = 8
844
+ defs.event_queues.append(EventQueueDef(
845
+ name=q_elem.get("name", ""), size=size,
846
+ element_type=q_elem.get("element_type", "uint8_t"),
847
+ event_ids=[e.strip() for e in es.split(",") if e.strip()] if es else [],
848
+ priority_enabled=q_elem.get("priority_enabled", "false").lower() == "true",
849
+ interrupt_safe=q_elem.get("interrupt_safe", "true").lower() == "true",
850
+ rtos_enabled=q_elem.get("rtos_enabled", "false").lower() == "true",
851
+ description=q_elem.get("description", ""),
852
+ title=q_elem.get("title", ""),
853
+ ))
854
+
855
+ defs.add_timer_variables()
856
+ return defs
857
+
858
+
859
+ # ======================================================================
860
+ # Shared library -> XML
861
+ # ======================================================================
862
+ def role_function_library_to_element(lib) -> Optional[ET.Element]:
863
+ """Serialize the shared role function library.
864
+
865
+ [v3.8]
866
+ libcntrl.RoleFunction has used_global_vars / used_events /
867
+ used_literals. They are written as comma-separated attributes.
868
+
869
+ [v3.8.1]
870
+ used_* attributes are only emitted when non-empty, so the
871
+ canonical XML of pre-v3.8 projects is unchanged after a
872
+ round-trip through the GUI.
873
+
874
+ [v3.8.2]
875
+ Also emit the reserved signature fields
876
+ (return_type / arg1_* / arg2_*). The test fixture
877
+ tests/data/v22_features_test3.xml contains these attributes
878
+ in the <SharedLibraries> section, so omitting them caused a
879
+ canonical XML mismatch in test_v2_2_p12_9.py.
880
+ libcntrl.RoleFunction carries these fields since v1.5.
881
+ """
882
+ if lib is None:
883
+ return None
884
+ root = ET.Element("RoleFunctionLibrary")
885
+ for rf in lib.list_all():
886
+ attrs = {
887
+ "name": rf.name,
888
+ "namespace": getattr(rf, 'namespace', ''),
889
+ "description": getattr(rf, 'description', ''),
890
+ # [v3.8.2] Reserved signature fields: preserved for
891
+ # canonical XML equality with pre-v3.8 fixtures.
892
+ "return_type": getattr(rf, 'return_type', ''),
893
+ "arg1_type": getattr(rf, 'arg1_type', ''),
894
+ "arg1_name": getattr(rf, 'arg1_name', ''),
895
+ "arg2_type": getattr(rf, 'arg2_type', ''),
896
+ "arg2_name": getattr(rf, 'arg2_name', ''),
897
+ "title": getattr(rf, 'title', ''),
898
+ }
899
+ _add_used_attrs(attrs, rf)
900
+ ET.SubElement(root, "RoleFunction", **attrs)
901
+ return root
902
+
903
+
904
+ def role_function_library_from_element(elem: Optional[ET.Element]):
905
+ """Deserialize the shared role function library.
906
+
907
+ [v3.8]
908
+ used_global_vars / used_events / used_literals are read from
909
+ comma-separated attributes; missing -> [].
910
+
911
+ [v3.8.2]
912
+ Reserved signature fields (return_type / arg1_* / arg2_*) are
913
+ read back so that the values round-trip unchanged.
914
+ """
915
+ if elem is None:
916
+ return RoleFunctionLibrary() if RoleFunctionLibrary else None
917
+ lib = RoleFunctionLibrary()
918
+ for rf_elem in elem.findall("RoleFunction"):
919
+ name = rf_elem.get("name", "")
920
+ namespace = rf_elem.get("namespace", "")
921
+ try:
922
+ try:
923
+ rf = LibRoleFunction(
924
+ name=name, namespace=namespace,
925
+ title=rf_elem.get("title", ""),
926
+ description=rf_elem.get("description", ""),
927
+ )
928
+ except TypeError:
929
+ # Older libcntrl.RoleFunction without namespace kwarg
930
+ rf = LibRoleFunction(
931
+ name=name,
932
+ title=rf_elem.get("title", ""),
933
+ description=rf_elem.get("description", ""),
934
+ )
935
+ if hasattr(rf, 'namespace'):
936
+ rf.namespace = namespace
937
+
938
+ # [v3.8.2] Reserved signature fields
939
+ for attr in ('return_type', 'arg1_type', 'arg1_name',
940
+ 'arg2_type', 'arg2_name'):
941
+ if hasattr(rf, attr):
942
+ setattr(rf, attr, rf_elem.get(attr, ''))
943
+
944
+ # [v3.8] symbol references
945
+ for attr in ('used_global_vars', 'used_events',
946
+ 'used_literals'):
947
+ if hasattr(rf, attr):
948
+ setattr(rf, attr,
949
+ _split_csv(rf_elem.get(attr, "")))
950
+ lib.add(rf)
951
+ except Exception as e:
952
+ logger.error(f"load role function failed: {e}", exc_info=True)
953
+ return lib
954
+
955
+
956
+ def condition_library_to_element(lib) -> Optional[ET.Element]:
957
+ if lib is None:
958
+ return None
959
+ root = ET.Element("ConditionLibrary")
960
+ for ct in lib.list_all():
961
+ ET.SubElement(root, "Condition", **{
962
+ "name": ct.name,
963
+ "condition": getattr(ct, 'condition', ''),
964
+ })
965
+ return root
966
+
967
+
968
+ def condition_library_from_element(elem: Optional[ET.Element]):
969
+ if elem is None:
970
+ return ConditionLibrary() if ConditionLibrary else None
971
+ lib = ConditionLibrary()
972
+ for ct_elem in elem.findall("Condition"):
973
+ try:
974
+ ct = ConditionTemplate(
975
+ name=ct_elem.get("name", ""),
976
+ condition=ct_elem.get("condition", ""),
977
+ )
978
+ lib.add(ct)
979
+ except Exception as e:
980
+ logger.error(f"condition load failed: {e}", exc_info=True)
981
+ return lib
982
+
983
+
984
+ def literal_library_to_element(lib) -> Optional[ET.Element]:
985
+ if lib is None:
986
+ return None
987
+ root = ET.Element("LiteralLibrary")
988
+ for lit in lib.list_all():
989
+ ET.SubElement(root, "Literal", **{
990
+ "name": lit.name,
991
+ "value": getattr(lit, 'value', ''),
992
+ "literal_type": getattr(lit, 'literal_type', ''),
993
+ "description": getattr(lit, 'description', ''),
994
+ })
995
+ return root
996
+
997
+
998
+ def literal_library_from_element(elem: Optional[ET.Element]):
999
+ if elem is None:
1000
+ return LiteralLibrary() if LiteralLibrary else None
1001
+ lib = LiteralLibrary()
1002
+ for lit_elem in elem.findall("Literal"):
1003
+ try:
1004
+ lit = LiteralDefinition(
1005
+ name=lit_elem.get("name", ""),
1006
+ value=lit_elem.get("value", ""),
1007
+ literal_type=lit_elem.get("literal_type", "int"),
1008
+ description=lit_elem.get("description", ""),
1009
+ )
1010
+ lib.add(lit)
1011
+ except Exception as e:
1012
+ logger.error(f"literal load failed: {e}", exc_info=True)
1013
+ return lib
1014
+
1015
+
1016
+ # ======================================================================
1017
+ # Project settings -> XML (unchanged)
1018
+ # ======================================================================
1019
+ def _project_settings_to_element(settings: Optional[dict]) -> ET.Element:
1020
+ elem = ET.Element("ProjectSettings")
1021
+ if settings:
1022
+ cg = ET.SubElement(elem, "CodeGeneration")
1023
+ cg.set("project_name", settings.get('project_name', 'MyProject'))
1024
+ cg.set("table_type", settings.get('table_type', 'array'))
1025
+ cg.set("generation_style", settings.get('generation_style', 'table_driven'))
1026
+ cg.set("os_type", settings.get('os_type', 'non_rtos'))
1027
+ cg.set("folder_structure", settings.get('folder_structure', 'by_type'))
1028
+ cg.set("include_dir_name", settings.get('include_dir_name', 'include'))
1029
+ cg.set("source_dir_name", settings.get('source_dir_name', 'src'))
1030
+ cg.set("common_dir_name", settings.get('common_dir_name', 'common'))
1031
+ cg.set("project_dir_name", settings.get('project_dir_name', 'project'))
1032
+ cg.set("generate_super_include",
1033
+ "true" if settings.get('generate_super_include', True) else "false")
1034
+ cg.set("super_include_file",
1035
+ settings.get('super_include_file', 'statable_all.h'))
1036
+ cg.set("max_consecutive_pending_events",
1037
+ str(settings.get('max_consecutive_pending_events', 16)))
1038
+ ext_includes = settings.get('external_includes', [])
1039
+ if ext_includes:
1040
+ ext_elem = ET.SubElement(cg, "ExternalIncludes")
1041
+ for inc in ext_includes:
1042
+ ET.SubElement(ext_elem, "Include", name=inc)
1043
+ cg.set("external_includes_in_super",
1044
+ "true" if settings.get('external_includes_in_super', True) else "false")
1045
+ cg.set("external_includes_in_role",
1046
+ "true" if settings.get('external_includes_in_role', True) else "false")
1047
+ cg.set("external_includes_in_transitions",
1048
+ "true" if settings.get('external_includes_in_transitions', False) else "false")
1049
+ cg.set("external_includes_in_common",
1050
+ "true" if settings.get('external_includes_in_common', False) else "false")
1051
+ return elem
1052
+
1053
+
1054
+ def _project_settings_from_element(elem: Optional[ET.Element]) -> dict:
1055
+ settings = {}
1056
+ if elem is None:
1057
+ return settings
1058
+ cg = elem.find("CodeGeneration")
1059
+ if cg is None:
1060
+ return settings
1061
+ settings['project_name'] = cg.get("project_name", "MyProject")
1062
+ settings['table_type'] = cg.get("table_type", "array")
1063
+ settings['generation_style'] = cg.get("generation_style", "table_driven")
1064
+ settings['os_type'] = cg.get("os_type", "non_rtos")
1065
+ settings['folder_structure'] = cg.get("folder_structure", "by_type")
1066
+ settings['include_dir_name'] = cg.get("include_dir_name", "include")
1067
+ settings['source_dir_name'] = cg.get("source_dir_name", "src")
1068
+ settings['common_dir_name'] = cg.get("common_dir_name", "common")
1069
+ settings['project_dir_name'] = cg.get("project_dir_name", "project")
1070
+ settings['generate_super_include'] = cg.get("generate_super_include", "true").lower() == "true"
1071
+ settings['super_include_file'] = cg.get("super_include_file", "statable_all.h")
1072
+ try:
1073
+ settings['max_consecutive_pending_events'] = int(
1074
+ cg.get("max_consecutive_pending_events", "16"))
1075
+ except ValueError:
1076
+ settings['max_consecutive_pending_events'] = 16
1077
+ ext_elem = cg.find("ExternalIncludes")
1078
+ if ext_elem is not None:
1079
+ settings['external_includes'] = [
1080
+ inc.get("name", "") for inc in ext_elem.findall("Include")]
1081
+ else:
1082
+ settings['external_includes'] = []
1083
+ settings['external_includes_in_super'] = cg.get("external_includes_in_super", "true").lower() == "true"
1084
+ settings['external_includes_in_role'] = cg.get("external_includes_in_role", "true").lower() == "true"
1085
+ settings['external_includes_in_transitions'] = cg.get("external_includes_in_transitions", "false").lower() == "true"
1086
+ settings['external_includes_in_common'] = cg.get("external_includes_in_common", "false").lower() == "true"
1087
+ return settings
1088
+
1089
+
1090
+ def project_to_xml(
1091
+ tabs: List[Tuple[str, StateMachine]],
1092
+ global_defs: GlobalDefinitions,
1093
+ filepath: str,
1094
+ role_function_library=None,
1095
+ condition_library=None,
1096
+ literal_library=None,
1097
+ project_settings: Optional[dict] = None,
1098
+ ) -> None:
1099
+ root = ET.Element("Project")
1100
+
1101
+ project_name = "MyProject"
1102
+ if project_settings:
1103
+ project_name = project_settings.get('project_name', 'MyProject')
1104
+ root.append(_project_settings_to_element(project_settings))
1105
+ root.set("name", project_name)
1106
+
1107
+ root.append(global_defs_to_element(global_defs))
1108
+
1109
+ if (role_function_library is not None
1110
+ or condition_library is not None
1111
+ or literal_library is not None):
1112
+ libs_elem = ET.SubElement(root, "SharedLibraries")
1113
+ if role_function_library is not None:
1114
+ libs_elem.append(role_function_library_to_element(role_function_library))
1115
+ if condition_library is not None:
1116
+ libs_elem.append(condition_library_to_element(condition_library))
1117
+ if literal_library is not None:
1118
+ libs_elem.append(literal_library_to_element(literal_library))
1119
+
1120
+ for name, sm in tabs:
1121
+ tab_elem = ET.SubElement(root, "Tab")
1122
+ tab_elem.set("name", name)
1123
+ tab_elem.append(state_machine_to_element(sm))
1124
+
1125
+ tree = ET.ElementTree(root)
1126
+ ET.indent(tree, space=" ")
1127
+ tree.write(filepath, encoding="utf-8", xml_declaration=True)
1128
+
1129
+
1130
+ def project_from_xml(filepath: str):
1131
+ tree = ET.parse(filepath)
1132
+ root = tree.getroot()
1133
+
1134
+ project_settings = _project_settings_from_element(root.find("ProjectSettings"))
1135
+ if 'project_name' not in project_settings and root.get("name"):
1136
+ project_settings['project_name'] = root.get("name")
1137
+
1138
+ gd_elem = root.find("GlobalDefinitions")
1139
+ global_defs = (global_defs_from_element(gd_elem)
1140
+ if gd_elem is not None else GlobalDefinitions())
1141
+
1142
+ role_function_library = (RoleFunctionLibrary() if RoleFunctionLibrary else None)
1143
+ condition_library = (ConditionLibrary() if ConditionLibrary else None)
1144
+ literal_library = (LiteralLibrary() if LiteralLibrary else None)
1145
+
1146
+ libs_elem = root.find("SharedLibraries")
1147
+ if libs_elem is not None:
1148
+ role_function_library = role_function_library_from_element(
1149
+ libs_elem.find("RoleFunctionLibrary"))
1150
+ condition_library = condition_library_from_element(
1151
+ libs_elem.find("ConditionLibrary"))
1152
+ literal_library = literal_library_from_element(
1153
+ libs_elem.find("LiteralLibrary"))
1154
+
1155
+ tabs = []
1156
+ for tab_elem in root.findall("Tab"):
1157
+ name = tab_elem.get("name", "Untitled")
1158
+ sm_elem = tab_elem.find("StateMachine")
1159
+ sm = (state_machine_from_element(sm_elem)
1160
+ if sm_elem is not None else StateMachine())
1161
+ tabs.append((name, sm))
1162
+
1163
+ return (tabs, global_defs, role_function_library,
1164
+ condition_library, literal_library, project_settings)