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.
- codegen/__init__.py +38 -0
- codegen/c_code_generator.py +1752 -0
- codegen/code_merger.py +371 -0
- codegen/code_templates.py +509 -0
- codegen/config.py +167 -0
- codegen/enum_generator.py +377 -0
- codegen/event_queue_generator.py +303 -0
- codegen/interrupt_generator.py +345 -0
- codegen/naming_convention.py +171 -0
- codegen/osal_generator.py +282 -0
- codegen/role_function_generator.py +1244 -0
- codegen/sample_data.py +269 -0
- codegen/state_actions_generator.py +395 -0
- codegen/struct_generator.py +721 -0
- codegen/timer_generator.py +299 -0
- codegen/transition_generator.py +907 -0
- codegen/type_mapper.py +69 -0
- codegen/validate/__init__.py +38 -0
- codegen/validate/change_actions.py +90 -0
- codegen/validate/change_applier.py +416 -0
- codegen/validate/clipboard_manager.py +47 -0
- codegen/validate/data/__init__.py +30 -0
- codegen/validate/data/action_definitions.py +200 -0
- codegen/validate/data/keywords.py +46 -0
- codegen/validate/data/prompt_templates.py +176 -0
- codegen/validate/data/validation_rules.py +205 -0
- codegen/validate/items/__init__.py +30 -0
- codegen/validate/items/base_validator.py +25 -0
- codegen/validate/items/cell_validator.py +275 -0
- codegen/validate/items/custom_type_validator.py +75 -0
- codegen/validate/items/event_validator.py +78 -0
- codegen/validate/items/flag_validator.py +74 -0
- codegen/validate/items/interrupt_validator.py +75 -0
- codegen/validate/items/queue_validator.py +73 -0
- codegen/validate/items/role_function_validator.py +175 -0
- codegen/validate/items/state_validator.py +88 -0
- codegen/validate/items/timer_validator.py +120 -0
- codegen/validate/items/transition_validator.py +113 -0
- codegen/validate/items/variable_validator.py +132 -0
- codegen/validate/logger.py +52 -0
- codegen/validate/models.py +177 -0
- codegen/validate/prompt_generator.py +724 -0
- codegen/validate/response_parser.py +154 -0
- codegen/validate/response_validator.py +275 -0
- codegen/validate/validation_dialog.py +322 -0
- codegen/validate/validator.py +94 -0
- codegen/variable_generator.py +419 -0
- statable/__init__.py +71 -0
- statable/__main__.py +49 -0
- statable/cli.py +251 -0
- statable/global_defs.py +256 -0
- statable/mermaid_gen.py +144 -0
- statable/model.py +426 -0
- statable/parser.py +64 -0
- statable/sample_data.py +435 -0
- statable/shared/__init__.py +24 -0
- statable/shared/condition_library.py +63 -0
- statable/shared/literal_library.py +66 -0
- statable/shared/role_function_library.py +150 -0
- statable/state_machine.py +121 -0
- statable/xml_io.py +1164 -0
- statable-3.1.0.dist-info/METADATA +151 -0
- statable-3.1.0.dist-info/RECORD +117 -0
- statable-3.1.0.dist-info/WHEEL +5 -0
- statable-3.1.0.dist-info/entry_points.txt +2 -0
- statable-3.1.0.dist-info/top_level.txt +3 -0
- statable_gui/__init__.py +23 -0
- statable_gui/action_edit_dialog.py +214 -0
- statable_gui/code_generation_dialog.py +486 -0
- statable_gui/code_generation_settings_dialog.py +522 -0
- statable_gui/common_widgets.py +537 -0
- statable_gui/condition_builder_dialog.py +500 -0
- statable_gui/condition_edit_dialog.py +123 -0
- statable_gui/condition_template_dialog.py +58 -0
- statable_gui/config.py +21 -0
- statable_gui/dialogs.py +439 -0
- statable_gui/event_definition_dialog.py +605 -0
- statable_gui/event_delivery_settings_dialog.py +191 -0
- statable_gui/event_queue_dialog.py +245 -0
- statable_gui/global_defs.py +167 -0
- statable_gui/global_defs_dialog.py +796 -0
- statable_gui/i18n/__init__.py +118 -0
- statable_gui/interrupt_handler_edit_dialog.py +810 -0
- statable_gui/layer_settings_dialog.py +180 -0
- statable_gui/libcntrl/__init__.py +23 -0
- statable_gui/libcntrl/condition_library.py +13 -0
- statable_gui/libcntrl/literal_library.py +13 -0
- statable_gui/libcntrl/literal_management_dialog.py +185 -0
- statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
- statable_gui/libcntrl/role_function_library.py +13 -0
- statable_gui/literal_definition_dialog.py +62 -0
- statable_gui/logger.py +83 -0
- statable_gui/main_window.py +1380 -0
- statable_gui/matrix_table.py +413 -0
- statable_gui/preference_keys.py +18 -0
- statable_gui/preferences.py +74 -0
- statable_gui/role_function_dialog.py +244 -0
- statable_gui/sample_data.py +11 -0
- statable_gui/state_actions_dialog.py +398 -0
- statable_gui/symbol_picker.py +170 -0
- statable_gui/traceball.py +37 -0
- statable_gui/transition_editor_direct/__init__.py +15 -0
- statable_gui/transition_editor_direct/actions_tab.py +856 -0
- statable_gui/transition_editor_direct/code_widget.py +344 -0
- statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
- statable_gui/transition_editor_direct/dialog.py +335 -0
- statable_gui/transition_editor_direct/draft.py +270 -0
- statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
- statable_gui/transition_editor_direct/overview_tab.py +173 -0
- statable_gui/transition_editor_direct/palette_widget.py +233 -0
- statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
- statable_gui/transition_editor_direct/relations_tab.py +230 -0
- statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
- statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
- statable_gui/transition_editor_direct/transitions_tab.py +620 -0
- statable_gui/validation_dialog.py +285 -0
- 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)
|