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/cli.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# code/statable/cli.py
|
|
2
|
+
"""statable-cli - Command-line interface for StaTable SDK.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
generate Generate C code from a design XML file
|
|
6
|
+
validate Validate a design XML file
|
|
7
|
+
version Print version
|
|
8
|
+
|
|
9
|
+
Output:
|
|
10
|
+
stdout: JSON result (default) or plain text
|
|
11
|
+
stderr: log / error messages
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
0 success
|
|
15
|
+
1 validation error (validate --exit-on-error)
|
|
16
|
+
2 internal error (missing file, parse failure, etc.)
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
statable-cli generate --xml design.xml --out generated/ --format json
|
|
20
|
+
statable-cli validate --xml design.xml --format json --exit-on-error
|
|
21
|
+
statable-cli version
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
CLI_VERSION = "3.1.0"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _err(msg: str) -> None:
|
|
34
|
+
print(msg, file=sys.stderr)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_project(xml_path: str):
|
|
38
|
+
"""Load XML design file.
|
|
39
|
+
|
|
40
|
+
Returns (tabs, gd, rf_lib, cond_lib, lit_lib, settings).
|
|
41
|
+
"""
|
|
42
|
+
from statable.xml_io import project_from_xml
|
|
43
|
+
return project_from_xml(xml_path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_config(settings):
|
|
47
|
+
"""Build CodeGenerationConfig from project settings dict."""
|
|
48
|
+
from codegen.config import CodeGenerationConfig
|
|
49
|
+
cfg = CodeGenerationConfig()
|
|
50
|
+
if not settings:
|
|
51
|
+
return cfg
|
|
52
|
+
for key, value in settings.items():
|
|
53
|
+
if key == "project_name":
|
|
54
|
+
continue
|
|
55
|
+
if hasattr(cfg, key):
|
|
56
|
+
try:
|
|
57
|
+
setattr(cfg, key, value)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
return cfg
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_generate(args) -> int:
|
|
64
|
+
xml_path = args.xml
|
|
65
|
+
out_dir = args.out
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
tabs, gd, rf_lib, _cond, _lit, settings = _load_project(xml_path)
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
_err(f"error: XML file not found: {xml_path}")
|
|
71
|
+
return 2
|
|
72
|
+
except Exception as e:
|
|
73
|
+
_err(f"error: failed to load project: {e!r}")
|
|
74
|
+
return 2
|
|
75
|
+
|
|
76
|
+
if not tabs:
|
|
77
|
+
_err("error: no tabs/state machines found in project")
|
|
78
|
+
return 2
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
cfg = _build_config(settings)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
_err(f"error: config setup failed: {e!r}")
|
|
84
|
+
return 2
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
from codegen.c_code_generator import CCodeGenerator
|
|
88
|
+
gen = CCodeGenerator(config=cfg)
|
|
89
|
+
generated = gen.generate_all_layers(
|
|
90
|
+
tabs, gd, role_function_library=rf_lib,
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
_err(f"error: generation failed: {e!r}")
|
|
94
|
+
return 2
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
Path(out_dir).mkdir(parents=True, exist_ok=True)
|
|
98
|
+
saved = gen.save_generated_code(generated, str(out_dir))
|
|
99
|
+
except Exception as e:
|
|
100
|
+
_err(f"error: save failed: {e!r}")
|
|
101
|
+
return 2
|
|
102
|
+
|
|
103
|
+
out_res = Path(out_dir).resolve()
|
|
104
|
+
rel_files = []
|
|
105
|
+
for p in saved:
|
|
106
|
+
try:
|
|
107
|
+
rel_files.append(
|
|
108
|
+
Path(p).resolve().relative_to(out_res).as_posix()
|
|
109
|
+
)
|
|
110
|
+
except ValueError:
|
|
111
|
+
rel_files.append(Path(p).name)
|
|
112
|
+
rel_files.sort()
|
|
113
|
+
|
|
114
|
+
if args.format == "json":
|
|
115
|
+
result = {
|
|
116
|
+
"status": "ok",
|
|
117
|
+
"command": "generate",
|
|
118
|
+
"input": str(xml_path),
|
|
119
|
+
"output_dir": str(out_dir),
|
|
120
|
+
"files_generated": rel_files,
|
|
121
|
+
"warnings": [],
|
|
122
|
+
}
|
|
123
|
+
print(json.dumps(result, indent=2))
|
|
124
|
+
else:
|
|
125
|
+
print(f"OK: {len(saved)} files generated in {out_dir}")
|
|
126
|
+
for f in rel_files:
|
|
127
|
+
print(f" {f}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_validate(args) -> int:
|
|
132
|
+
xml_path = args.xml
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
tabs, gd, _rf, _cond, _lit, _settings = _load_project(xml_path)
|
|
136
|
+
except FileNotFoundError:
|
|
137
|
+
_err(f"error: XML file not found: {xml_path}")
|
|
138
|
+
return 2
|
|
139
|
+
except Exception as e:
|
|
140
|
+
_err(f"error: failed to load project: {e!r}")
|
|
141
|
+
return 2
|
|
142
|
+
|
|
143
|
+
if not tabs:
|
|
144
|
+
_err("error: no tabs/state machines found in project")
|
|
145
|
+
return 2
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
from codegen.validate.validator import CodeGenerationValidator
|
|
149
|
+
validator = CodeGenerationValidator()
|
|
150
|
+
except Exception as e:
|
|
151
|
+
_err(f"error: validator init failed: {e!r}")
|
|
152
|
+
return 2
|
|
153
|
+
|
|
154
|
+
all_issues = []
|
|
155
|
+
total_errors = 0
|
|
156
|
+
total_warnings = 0
|
|
157
|
+
|
|
158
|
+
for name, sm in tabs:
|
|
159
|
+
try:
|
|
160
|
+
res = validator.validate(sm, gd)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
_err(f"error: validation failed for tab {name!r}: {e!r}")
|
|
163
|
+
return 2
|
|
164
|
+
for issue in res.issues:
|
|
165
|
+
d = issue.to_dict()
|
|
166
|
+
d["tab"] = name
|
|
167
|
+
all_issues.append(d)
|
|
168
|
+
total_errors += res.error_count
|
|
169
|
+
total_warnings += res.warning_count
|
|
170
|
+
|
|
171
|
+
status = "ok" if total_errors == 0 else "error"
|
|
172
|
+
|
|
173
|
+
if args.format == "json":
|
|
174
|
+
out = {
|
|
175
|
+
"status": status,
|
|
176
|
+
"command": "validate",
|
|
177
|
+
"input": str(xml_path),
|
|
178
|
+
"errors": total_errors,
|
|
179
|
+
"warnings": total_warnings,
|
|
180
|
+
"issues": all_issues,
|
|
181
|
+
}
|
|
182
|
+
print(json.dumps(out, indent=2))
|
|
183
|
+
else:
|
|
184
|
+
print(
|
|
185
|
+
f"{status.upper()}: errors={total_errors} "
|
|
186
|
+
f"warnings={total_warnings}"
|
|
187
|
+
)
|
|
188
|
+
for d in all_issues:
|
|
189
|
+
sev = d.get("severity", "?")
|
|
190
|
+
code = d.get("code", "?")
|
|
191
|
+
msg = d.get("message", "")
|
|
192
|
+
print(f" [{sev}] {code}: {msg}")
|
|
193
|
+
|
|
194
|
+
if args.exit_on_error and total_errors > 0:
|
|
195
|
+
return 1
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_version(args) -> int:
|
|
200
|
+
try:
|
|
201
|
+
from statable import __version__ as pkg_version
|
|
202
|
+
except Exception:
|
|
203
|
+
pkg_version = CLI_VERSION
|
|
204
|
+
print(f"statable-cli {pkg_version}")
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
209
|
+
parser = argparse.ArgumentParser(
|
|
210
|
+
prog="statable-cli",
|
|
211
|
+
description="StaTable command-line interface",
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"--version",
|
|
215
|
+
action="version",
|
|
216
|
+
version=f"statable-cli {CLI_VERSION}",
|
|
217
|
+
)
|
|
218
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
219
|
+
|
|
220
|
+
p_gen = sub.add_parser("generate", help="Generate C code from XML")
|
|
221
|
+
p_gen.add_argument("--xml", required=True,
|
|
222
|
+
help="Input design XML file")
|
|
223
|
+
p_gen.add_argument("--out", required=True,
|
|
224
|
+
help="Output directory")
|
|
225
|
+
p_gen.add_argument("--format", choices=["json", "text"],
|
|
226
|
+
default="json")
|
|
227
|
+
p_gen.set_defaults(func=cmd_generate)
|
|
228
|
+
|
|
229
|
+
p_val = sub.add_parser("validate", help="Validate XML design file")
|
|
230
|
+
p_val.add_argument("--xml", required=True,
|
|
231
|
+
help="Input design XML file")
|
|
232
|
+
p_val.add_argument("--format", choices=["json", "text"],
|
|
233
|
+
default="json")
|
|
234
|
+
p_val.add_argument("--exit-on-error", action="store_true",
|
|
235
|
+
help="Return exit code 1 on validation error")
|
|
236
|
+
p_val.set_defaults(func=cmd_validate)
|
|
237
|
+
|
|
238
|
+
p_ver = sub.add_parser("version", help="Print version")
|
|
239
|
+
p_ver.set_defaults(func=cmd_version)
|
|
240
|
+
|
|
241
|
+
return parser
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main(argv=None) -> int:
|
|
245
|
+
parser = build_parser()
|
|
246
|
+
args = parser.parse_args(argv)
|
|
247
|
+
return args.func(args)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
sys.exit(main())
|
statable/global_defs.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Data model for global variables, event flags, interrupt handlers, device resources, timer settings, and user-defined types"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class StructMemberDef:
|
|
9
|
+
"""Struct member definition"""
|
|
10
|
+
name: str
|
|
11
|
+
data_type: str
|
|
12
|
+
bit_width: int = 0
|
|
13
|
+
description: str = ""
|
|
14
|
+
title: str = ""
|
|
15
|
+
array_size: int = 0
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
if not self.title:
|
|
19
|
+
if self.bit_width > 0:
|
|
20
|
+
self.title = f"{self.name}:{self.bit_width}"
|
|
21
|
+
elif self.array_size > 0:
|
|
22
|
+
self.title = f"{self.name}[{self.array_size}]"
|
|
23
|
+
else:
|
|
24
|
+
self.title = f"Member: {self.name}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CustomTypeDef:
|
|
29
|
+
"""User-defined types (structs, etc.)"""
|
|
30
|
+
name: str
|
|
31
|
+
description: str = ""
|
|
32
|
+
members: List[StructMemberDef] = field(default_factory=list)
|
|
33
|
+
title: str = ""
|
|
34
|
+
|
|
35
|
+
def __post_init__(self):
|
|
36
|
+
if not self.title:
|
|
37
|
+
self.title = f"Type: {self.name}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SystemVariable:
|
|
42
|
+
"""Global variables shared across the system"""
|
|
43
|
+
name: str
|
|
44
|
+
type: str
|
|
45
|
+
unit: str = ""
|
|
46
|
+
default_value: str = ""
|
|
47
|
+
group: str = ""
|
|
48
|
+
description: str = ""
|
|
49
|
+
title: str = ""
|
|
50
|
+
array_size: int = 0
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
if not self.title:
|
|
54
|
+
if self.array_size > 0:
|
|
55
|
+
self.title = f"{self.name}[{self.array_size}]"
|
|
56
|
+
else:
|
|
57
|
+
self.title = f"Variable: {self.name}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class EventFlag:
|
|
62
|
+
"""Event flags managed as bit fields"""
|
|
63
|
+
name: str
|
|
64
|
+
min_value: int
|
|
65
|
+
max_value: int
|
|
66
|
+
group: str = ""
|
|
67
|
+
description: str = ""
|
|
68
|
+
title: str = ""
|
|
69
|
+
|
|
70
|
+
def __post_init__(self):
|
|
71
|
+
if not self.title:
|
|
72
|
+
self.title = f"Flag: {self.name}"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def bit_width(self) -> int:
|
|
76
|
+
if self.max_value < self.min_value:
|
|
77
|
+
return 0
|
|
78
|
+
return (self.max_value - self.min_value).bit_length()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class InterruptAction:
|
|
83
|
+
"""Conditional actions inside an interrupt handler"""
|
|
84
|
+
condition: str = ""
|
|
85
|
+
action: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class InterruptHandlerDef:
|
|
90
|
+
"""Interrupt handler definition
|
|
91
|
+
|
|
92
|
+
used_role_functions: list of used role functions' qualified_name (auto-extracted)
|
|
93
|
+
- e.g. ["Driver.Init", "Application.HandleTick"]
|
|
94
|
+
- Updated when the ISR action is saved
|
|
95
|
+
- Also used in generated code comments
|
|
96
|
+
|
|
97
|
+
used_variables: list of used global variable names (auto-extracted)
|
|
98
|
+
- e.g. ["counter", "g_system_tick"]
|
|
99
|
+
"""
|
|
100
|
+
name: str
|
|
101
|
+
description: str = ""
|
|
102
|
+
event_names: List[str] = field(default_factory=list)
|
|
103
|
+
is_timer: bool = False
|
|
104
|
+
actions: List[InterruptAction] = field(default_factory=list)
|
|
105
|
+
title: str = ""
|
|
106
|
+
# Added: usage record
|
|
107
|
+
used_role_functions: List[str] = field(default_factory=list)
|
|
108
|
+
used_variables: List[str] = field(default_factory=list)
|
|
109
|
+
|
|
110
|
+
def __post_init__(self):
|
|
111
|
+
if not self.title:
|
|
112
|
+
self.title = f"Interrupt: {self.name}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class DevicePlaceholderDef:
|
|
117
|
+
"""Device resource placeholder definition"""
|
|
118
|
+
name: str
|
|
119
|
+
description: str = ""
|
|
120
|
+
title: str = ""
|
|
121
|
+
|
|
122
|
+
def __post_init__(self):
|
|
123
|
+
if not self.title:
|
|
124
|
+
self.title = f"Device: {self.name}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class TimerDerivedDef:
|
|
129
|
+
"""Derived timer variable definition"""
|
|
130
|
+
period_name: str
|
|
131
|
+
multiplier: int
|
|
132
|
+
variable_name: str
|
|
133
|
+
data_type: str = "uint8_t"
|
|
134
|
+
title: str = ""
|
|
135
|
+
|
|
136
|
+
def __post_init__(self):
|
|
137
|
+
if not self.title:
|
|
138
|
+
self.title = f"Timer: {self.variable_name}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class TimerBaseDef:
|
|
143
|
+
"""Timer tick variable definition"""
|
|
144
|
+
variable_name: str = "g_system_tick"
|
|
145
|
+
unit: str = "1ms"
|
|
146
|
+
data_type: str = "volatile uint32_t"
|
|
147
|
+
derived: List[TimerDerivedDef] = field(default_factory=list)
|
|
148
|
+
title: str = ""
|
|
149
|
+
interrupt_name: str = ""
|
|
150
|
+
|
|
151
|
+
def __post_init__(self):
|
|
152
|
+
if not self.title:
|
|
153
|
+
self.title = f"Timer base: {self.variable_name}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class EventQueueDef:
|
|
158
|
+
"""Event queue definition"""
|
|
159
|
+
name: str
|
|
160
|
+
size: int
|
|
161
|
+
element_type: str
|
|
162
|
+
event_ids: List[str] = field(default_factory=list)
|
|
163
|
+
priority_enabled: bool = False
|
|
164
|
+
interrupt_safe: bool = True
|
|
165
|
+
rtos_enabled: bool = False
|
|
166
|
+
description: str = ""
|
|
167
|
+
title: str = ""
|
|
168
|
+
|
|
169
|
+
def __post_init__(self):
|
|
170
|
+
if not self.title:
|
|
171
|
+
self.title = f"Queue: {self.name}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class GlobalDefinitions:
|
|
175
|
+
"""Management class for global variables, event flags, interrupt handlers, device resources, timer settings, event queues, and user-defined types"""
|
|
176
|
+
|
|
177
|
+
def __init__(self):
|
|
178
|
+
self.variables: List[SystemVariable] = []
|
|
179
|
+
self.flags: List[EventFlag] = []
|
|
180
|
+
self.interrupts: List[InterruptHandlerDef] = []
|
|
181
|
+
self.placeholders: List[DevicePlaceholderDef] = []
|
|
182
|
+
self.timer_base: TimerBaseDef = TimerBaseDef()
|
|
183
|
+
self.extra_timers: List[TimerBaseDef] = []
|
|
184
|
+
self.event_queues: List[EventQueueDef] = []
|
|
185
|
+
self.custom_types: List[CustomTypeDef] = []
|
|
186
|
+
|
|
187
|
+
def add_timer_variables(self):
|
|
188
|
+
"""Timer base variables and derived timer variables are registered as global variables.
|
|
189
|
+
|
|
190
|
+
- Existing variables (loaded from XML or edited in GUI) preserve description / title
|
|
191
|
+
- Only type / unit are synchronized with the timer definition
|
|
192
|
+
- Only non-existing variables are newly added
|
|
193
|
+
|
|
194
|
+
This ensures description / title are preserved across round-trips.
|
|
195
|
+
"""
|
|
196
|
+
existing_by_name = {v.name: v for v in self.variables}
|
|
197
|
+
|
|
198
|
+
def _sync_or_add(name, type_, unit, default, group,
|
|
199
|
+
default_desc, default_title):
|
|
200
|
+
if name in existing_by_name:
|
|
201
|
+
v = existing_by_name[name]
|
|
202
|
+
# Treat timer definition as authoritative (type, unit)
|
|
203
|
+
v.type = type_
|
|
204
|
+
v.unit = unit
|
|
205
|
+
# Preserve existing description / title
|
|
206
|
+
if not v.description:
|
|
207
|
+
v.description = default_desc
|
|
208
|
+
if not v.title:
|
|
209
|
+
v.title = default_title
|
|
210
|
+
# If group is unset, fill in
|
|
211
|
+
if not v.group:
|
|
212
|
+
v.group = group
|
|
213
|
+
else:
|
|
214
|
+
self.variables.append(SystemVariable(
|
|
215
|
+
name=name,
|
|
216
|
+
type=type_,
|
|
217
|
+
unit=unit,
|
|
218
|
+
default_value=default,
|
|
219
|
+
group=group,
|
|
220
|
+
description=default_desc,
|
|
221
|
+
title=default_title,
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
all_timers = [self.timer_base] + self.extra_timers
|
|
225
|
+
for timer in all_timers:
|
|
226
|
+
# Base timer variable
|
|
227
|
+
_sync_or_add(
|
|
228
|
+
name=timer.variable_name,
|
|
229
|
+
type_=timer.data_type,
|
|
230
|
+
unit=timer.unit,
|
|
231
|
+
default="0",
|
|
232
|
+
group="Timer",
|
|
233
|
+
default_desc="Timer base variable",
|
|
234
|
+
default_title=timer.title or f"Timer base: {timer.variable_name}",
|
|
235
|
+
)
|
|
236
|
+
# Derived timer variable
|
|
237
|
+
for d in timer.derived:
|
|
238
|
+
_sync_or_add(
|
|
239
|
+
name=d.variable_name,
|
|
240
|
+
type_=d.data_type,
|
|
241
|
+
unit=d.period_name,
|
|
242
|
+
default="0",
|
|
243
|
+
group="Timer",
|
|
244
|
+
default_desc=f"Derived timer variable ({d.period_name})",
|
|
245
|
+
default_title=d.title or f"Timer: {d.variable_name}",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Get group name
|
|
249
|
+
def variable_groups(self) -> List[str]:
|
|
250
|
+
return sorted({v.group for v in self.variables if v.group})
|
|
251
|
+
|
|
252
|
+
def flag_groups(self) -> List[str]:
|
|
253
|
+
return sorted({f.group for f in self.flags if f.group})
|
|
254
|
+
|
|
255
|
+
def custom_type_names(self) -> List[str]:
|
|
256
|
+
return sorted({t.name for t in self.custom_types})
|
statable/mermaid_gen.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from .state_machine import StateMachine
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# ============================================================
|
|
5
|
+
# Mermaid label sanitization (v2.0 / v2.2)
|
|
6
|
+
# All replacements use ASCII to satisfy the English-only policy.
|
|
7
|
+
# ============================================================
|
|
8
|
+
_MERMAID_LABEL_REPLACEMENTS = (
|
|
9
|
+
(":", "-"), # colon -> dash
|
|
10
|
+
("[", "("), # opening bracket -> paren
|
|
11
|
+
("]", ")"), # closing bracket -> paren
|
|
12
|
+
('"', "'"),
|
|
13
|
+
("`", "'"),
|
|
14
|
+
("\n", " "),
|
|
15
|
+
("\r", " "),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sanitize_label(text: str) -> str:
|
|
20
|
+
"""Sanitize the Mermaid label string."""
|
|
21
|
+
if not text:
|
|
22
|
+
return ""
|
|
23
|
+
for src, dst in _MERMAID_LABEL_REPLACEMENTS:
|
|
24
|
+
text = text.replace(src, dst)
|
|
25
|
+
text = " ".join(text.split())
|
|
26
|
+
return text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _truncate_condition(condition: str, max_chars: int = 50) -> str:
|
|
30
|
+
"""Show long state transition conditions abbreviated."""
|
|
31
|
+
if not condition:
|
|
32
|
+
return ""
|
|
33
|
+
lines = condition.split('\n')
|
|
34
|
+
first_line = lines[0].strip() if lines else ""
|
|
35
|
+
if not first_line:
|
|
36
|
+
return ""
|
|
37
|
+
if len(first_line) > max_chars:
|
|
38
|
+
return first_line[:max_chars].rstrip() + "..."
|
|
39
|
+
if len(lines) > 1:
|
|
40
|
+
return first_line + " ..."
|
|
41
|
+
return first_line
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mode_suffix(trans) -> str:
|
|
45
|
+
"""
|
|
46
|
+
v2.2: Return a short mode suffix for the transition label.
|
|
47
|
+
|
|
48
|
+
"C" -> Commit (early_return=True)
|
|
49
|
+
"T" -> Tentative (early_return=False)
|
|
50
|
+
Empty string is returned when early_return is not set (backward compat).
|
|
51
|
+
"""
|
|
52
|
+
er = getattr(trans, 'early_return', None)
|
|
53
|
+
if er is True:
|
|
54
|
+
return " [Commit]"
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_label(trans, *, short_mode: bool = True) -> str:
|
|
59
|
+
"""Build the Mermaid label for a single transition."""
|
|
60
|
+
label_parts = []
|
|
61
|
+
|
|
62
|
+
# ---- Title ----
|
|
63
|
+
title_raw = getattr(trans, 'title', '') or ""
|
|
64
|
+
title_safe = _sanitize_label(title_raw)
|
|
65
|
+
if title_safe and title_safe != "(untitled transition)":
|
|
66
|
+
label_parts.append(title_safe)
|
|
67
|
+
else:
|
|
68
|
+
if getattr(trans, 'target', ''):
|
|
69
|
+
label_parts.append(_sanitize_label(trans.target))
|
|
70
|
+
else:
|
|
71
|
+
label_parts.append("(internal)")
|
|
72
|
+
|
|
73
|
+
# ---- Event ----
|
|
74
|
+
ev = getattr(trans, 'event', '') or ""
|
|
75
|
+
if ev:
|
|
76
|
+
ev_safe = _sanitize_label(ev)
|
|
77
|
+
if ev_safe:
|
|
78
|
+
label_parts.append(f"({ev_safe})")
|
|
79
|
+
|
|
80
|
+
# ---- Condition ----
|
|
81
|
+
cond = getattr(trans, 'condition', '') or ""
|
|
82
|
+
if cond:
|
|
83
|
+
cond_text = _sanitize_label(_truncate_condition(cond))
|
|
84
|
+
if cond_text:
|
|
85
|
+
label_parts.append(f"[{cond_text}]")
|
|
86
|
+
|
|
87
|
+
# ---- Mode (v2.2) ----
|
|
88
|
+
mode = _mode_suffix(trans)
|
|
89
|
+
if mode:
|
|
90
|
+
label_parts.append(mode)
|
|
91
|
+
|
|
92
|
+
return " ".join(label_parts).strip()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate_mermaid(sm: StateMachine) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Generate a Mermaid stateDiagram-v2 string from a StateMachine.
|
|
98
|
+
|
|
99
|
+
[v2.2 changes]
|
|
100
|
+
- Each transition in a cell becomes its own edge.
|
|
101
|
+
- else_target is emitted as a separate edge (dashed style not
|
|
102
|
+
available in stateDiagram-v2, so we use label "else").
|
|
103
|
+
- early_return=True adds " [Commit]" to the label.
|
|
104
|
+
- entry / exit are NOT rendered (metadata only).
|
|
105
|
+
"""
|
|
106
|
+
lines = ["stateDiagram-v2", " direction LR"]
|
|
107
|
+
|
|
108
|
+
if sm.initial_state:
|
|
109
|
+
lines.append(f" [*] --> {sm.initial_state}")
|
|
110
|
+
|
|
111
|
+
for t in sm.transitions:
|
|
112
|
+
# ---- Primary edge (target) ----
|
|
113
|
+
if t.target:
|
|
114
|
+
label = _build_label(t)
|
|
115
|
+
lines.append(f" {t.source} --> {t.target} : {label}")
|
|
116
|
+
else:
|
|
117
|
+
# No target: internal action only -> attach a note
|
|
118
|
+
label = _build_label(t)
|
|
119
|
+
lines.append(f" note right of {t.source} : internal: {label}")
|
|
120
|
+
|
|
121
|
+
# ---- Secondary edge (else_target) ----
|
|
122
|
+
else_target = getattr(t, 'else_target', '') or ''
|
|
123
|
+
has_else = getattr(t, 'has_else', True)
|
|
124
|
+
if has_else and else_target:
|
|
125
|
+
else_label_parts = []
|
|
126
|
+
# Show event name (if any) so the edge is identifiable
|
|
127
|
+
ev = getattr(t, 'event', '') or ""
|
|
128
|
+
if ev:
|
|
129
|
+
ev_safe = _sanitize_label(ev)
|
|
130
|
+
if ev_safe:
|
|
131
|
+
else_label_parts.append(f"({ev_safe})")
|
|
132
|
+
else:
|
|
133
|
+
else_label_parts.append("(completion)")
|
|
134
|
+
else_label_parts.append("else")
|
|
135
|
+
# Add mode suffix
|
|
136
|
+
mode = _mode_suffix(t)
|
|
137
|
+
if mode:
|
|
138
|
+
else_label_parts.append(mode)
|
|
139
|
+
else_label = " ".join(else_label_parts)
|
|
140
|
+
lines.append(
|
|
141
|
+
f" {t.source} --> {else_target} : {else_label}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return "\n".join(lines)
|