can-api-generator 1.0.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.
- can_api_generator/__about__.py +4 -0
- can_api_generator/__init__.py +3 -0
- can_api_generator/cbdb_generator.py +208 -0
- can_api_generator/code_generator.py +148 -0
- can_api_generator/main.py +216 -0
- can_api_generator/msg_processor.py +385 -0
- can_api_generator/templates/canframe.jinja +12 -0
- can_api_generator/templates/declaration_macros.jinja +23 -0
- can_api_generator/templates/dyndec_header.jinja +73 -0
- can_api_generator/templates/dyndec_source.jinja +277 -0
- can_api_generator/templates/encoding_macros.jinja +304 -0
- can_api_generator/templates/header_template.jinja +87 -0
- can_api_generator/templates/rx_macros.jinja +92 -0
- can_api_generator/templates/source_template.jinja +60 -0
- can_api_generator/templates/tx_macros.jinja +149 -0
- can_api_generator/types.py +39 -0
- can_api_generator/utils.py +64 -0
- can_api_generator-1.0.0.dist-info/METADATA +349 -0
- can_api_generator-1.0.0.dist-info/RECORD +22 -0
- can_api_generator-1.0.0.dist-info/WHEEL +4 -0
- can_api_generator-1.0.0.dist-info/entry_points.txt +2 -0
- can_api_generator-1.0.0.dist-info/licenses/LICENSE.txt +166 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cbdb_generator
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
Compiles a KCD database into the binary descriptor cbdb consumed by the
|
|
6
|
+
dynamic decoder running on a CAN-to-BLE bridge, plus a name -> signal_id
|
|
7
|
+
map for clients that need to reference signals symbolically (e.g. the
|
|
8
|
+
Android dashboard app sidecar).
|
|
9
|
+
|
|
10
|
+
Unlike the static API path, the cbdb path has no node: the bridge is a bus
|
|
11
|
+
observer, so every signal of every message is included and no
|
|
12
|
+
direction filtering is applied.
|
|
13
|
+
|
|
14
|
+
Cbdb format v1 (all little-endian):
|
|
15
|
+
|
|
16
|
+
header (48 B): magic 4s = b"CDB1", format_version u16 = 1,
|
|
17
|
+
msg_count u16, signal_count u16, reserved u16 = 0,
|
|
18
|
+
cbdb_size u32 (total bytes incl. header),
|
|
19
|
+
kcd_sha256 32s (hash of the KCD file bytes)
|
|
20
|
+
per message (8 B + signals):
|
|
21
|
+
arb_id u32 (bit31 = extended-id flag), dlc u8,
|
|
22
|
+
n_signals u8, reserved u16 = 0
|
|
23
|
+
per signal (20 B):
|
|
24
|
+
signal_id u32 (FNV-1a of "<msg>.<sig>" resolved names),
|
|
25
|
+
start_bit u8, bit_length u8, flags u8,
|
|
26
|
+
mux_sel_idx u8 (index into this message's signal list,
|
|
27
|
+
0xFF = not muxed), mux_value u32,
|
|
28
|
+
scale f32, offset f32
|
|
29
|
+
|
|
30
|
+
Messages are sorted ascending by the stored u32 arb_id key so the decoder
|
|
31
|
+
can binary-search. Within a message, mux selector signals come first, then
|
|
32
|
+
plain signals, then muxed signals, so a single decode pass has selector raw
|
|
33
|
+
values available before evaluating mux membership.
|
|
34
|
+
"""
|
|
35
|
+
import hashlib
|
|
36
|
+
import struct
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
import can_db_model
|
|
40
|
+
|
|
41
|
+
from .msg_processor import CAPIMessage
|
|
42
|
+
from .utils import converrt_to_c_identifier
|
|
43
|
+
|
|
44
|
+
CBDB_MAGIC = b"CDB1"
|
|
45
|
+
CBDB_FORMAT_VERSION = 1
|
|
46
|
+
|
|
47
|
+
HEADER_FMT = "<4sHHHHI32s"
|
|
48
|
+
MSG_FMT = "<IBBH"
|
|
49
|
+
SIG_FMT = "<IBBBBIff"
|
|
50
|
+
|
|
51
|
+
FLAG_SIGNED = 1 << 0
|
|
52
|
+
FLAG_WIRE_FLOAT = 1 << 1
|
|
53
|
+
FLAG_MUXED = 1 << 2
|
|
54
|
+
|
|
55
|
+
EXT_ID_FLAG = 1 << 31
|
|
56
|
+
NO_MUX_SEL = 0xFF
|
|
57
|
+
|
|
58
|
+
FNV1A32_OFFSET_BASIS = 0x811C9DC5
|
|
59
|
+
FNV1A32_PRIME = 0x01000193
|
|
60
|
+
|
|
61
|
+
# Node name used when constructing CAPIMessage objects for the cbdb path.
|
|
62
|
+
# It matches no real node, so no direction filtering takes place and every
|
|
63
|
+
# signal stays in the message.
|
|
64
|
+
_OBSERVER_NODE = "__cbdb_observer__"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CbdbGenerationException(Exception):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def fnv1a32(text: str) -> int:
|
|
72
|
+
"""FNV-1a 32-bit hash of the UTF-8 encoding of `text`."""
|
|
73
|
+
digest = FNV1A32_OFFSET_BASIS
|
|
74
|
+
for byte in text.encode("utf-8"):
|
|
75
|
+
digest ^= byte
|
|
76
|
+
digest = (digest * FNV1A32_PRIME) & 0xFFFFFFFF
|
|
77
|
+
return digest
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _ordered_signals(msg: CAPIMessage) -> list:
|
|
81
|
+
"""All signals of a message in cbdb order.
|
|
82
|
+
|
|
83
|
+
Mux selectors first, then plain signals, then muxed signals; original
|
|
84
|
+
KCD order is preserved within each group.
|
|
85
|
+
"""
|
|
86
|
+
selectors = [s for s in msg.all_signals if s.is_multiplex]
|
|
87
|
+
plain = [s for s in msg.all_signals
|
|
88
|
+
if not s.is_multiplex and s.multiplexer is None]
|
|
89
|
+
muxed = [s for s in msg.all_signals
|
|
90
|
+
if not s.is_multiplex and s.multiplexer is not None]
|
|
91
|
+
return selectors + plain + muxed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _signal_flags(sig) -> int:
|
|
95
|
+
flags = 0
|
|
96
|
+
if sig.is_signed and not sig.is_float:
|
|
97
|
+
flags |= FLAG_SIGNED
|
|
98
|
+
if sig.is_float:
|
|
99
|
+
flags |= FLAG_WIRE_FLOAT
|
|
100
|
+
if sig.multiplexer is not None:
|
|
101
|
+
flags |= FLAG_MUXED
|
|
102
|
+
return flags
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _arb_key(msg: CAPIMessage) -> int:
|
|
106
|
+
return msg.arb_id | (EXT_ID_FLAG if msg.is_extended else 0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_capi_messages(db_path: Path) -> list[CAPIMessage]:
|
|
110
|
+
"""Load a KCD/YAML database and build node-less CAPIMessage objects."""
|
|
111
|
+
db = can_db_model.load(Path(db_path))
|
|
112
|
+
bus_name = db.bus.name if db.bus is not None else ""
|
|
113
|
+
msgs = [CAPIMessage(msg, api_name="cbdb", node_name=_OBSERVER_NODE,
|
|
114
|
+
bus_name=bus_name, require_endpoints=False)
|
|
115
|
+
for msg in db.messages]
|
|
116
|
+
for msg in msgs:
|
|
117
|
+
for sig in msg.signals:
|
|
118
|
+
sig.resolve_muxed_signal_name()
|
|
119
|
+
return msgs
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def generate_cbdb(kcd_path: Path) -> tuple[bytes, dict[str, int]]:
|
|
123
|
+
"""Compile `kcd_path` into (cbdb bytes, resolved-name -> signal_id map)."""
|
|
124
|
+
kcd_path = Path(kcd_path)
|
|
125
|
+
# Sort by the plain (masked) id: received frames carry no extended-id
|
|
126
|
+
# flag in their arbitration-id member, so the decoder binary-searches on
|
|
127
|
+
# the masked value. The flag bit in the stored key is metadata.
|
|
128
|
+
msgs = sorted(build_capi_messages(kcd_path), key=lambda m: m.arb_id)
|
|
129
|
+
|
|
130
|
+
id_map: dict[str, int] = {}
|
|
131
|
+
body = bytearray()
|
|
132
|
+
signal_count = 0
|
|
133
|
+
for msg in msgs:
|
|
134
|
+
signals = _ordered_signals(msg)
|
|
135
|
+
signal_count += len(signals)
|
|
136
|
+
body += struct.pack(MSG_FMT, _arb_key(msg), msg.length,
|
|
137
|
+
len(signals), 0)
|
|
138
|
+
for sig in signals:
|
|
139
|
+
name = f"{converrt_to_c_identifier(msg.name)}.{sig.api_var_name}"
|
|
140
|
+
signal_id = fnv1a32(name)
|
|
141
|
+
if signal_id in id_map.values():
|
|
142
|
+
raise CbdbGenerationException(
|
|
143
|
+
f"signal_id hash collision on {name}")
|
|
144
|
+
id_map[name] = signal_id
|
|
145
|
+
if sig.multiplexer is not None:
|
|
146
|
+
mux_sel_idx = next(
|
|
147
|
+
i for i, s in enumerate(signals)
|
|
148
|
+
if s.is_multiplex and s.name == sig.multiplexer)
|
|
149
|
+
mux_value = sig.mux_group
|
|
150
|
+
else:
|
|
151
|
+
mux_sel_idx = NO_MUX_SEL
|
|
152
|
+
mux_value = 0
|
|
153
|
+
body += struct.pack(SIG_FMT, signal_id, sig.start_bit,
|
|
154
|
+
sig.length, _signal_flags(sig),
|
|
155
|
+
mux_sel_idx, mux_value,
|
|
156
|
+
float(sig.scale), float(sig.offset))
|
|
157
|
+
|
|
158
|
+
kcd_sha256 = hashlib.sha256(kcd_path.read_bytes()).digest()
|
|
159
|
+
cbdb_size = struct.calcsize(HEADER_FMT) + len(body)
|
|
160
|
+
header = struct.pack(HEADER_FMT, CBDB_MAGIC, CBDB_FORMAT_VERSION,
|
|
161
|
+
len(msgs), signal_count, 0, cbdb_size, kcd_sha256)
|
|
162
|
+
return bytes(header + body), id_map
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_sidecar(kcd_path: Path) -> dict:
|
|
166
|
+
"""The JSON sidecar consumed by client apps: the signal id map plus a
|
|
167
|
+
per-message description (arbitration id, dlc, signal membership) so a
|
|
168
|
+
client can label raw bus traffic against this database."""
|
|
169
|
+
kcd_path = Path(kcd_path)
|
|
170
|
+
_, id_map = generate_cbdb(kcd_path)
|
|
171
|
+
messages = {}
|
|
172
|
+
signal_info = {}
|
|
173
|
+
for msg in sorted(build_capi_messages(kcd_path), key=lambda m: m.arb_id):
|
|
174
|
+
msg_name = converrt_to_c_identifier(msg.name)
|
|
175
|
+
messages[msg_name] = {
|
|
176
|
+
"arb_id": msg.arb_id,
|
|
177
|
+
"extended": msg.is_extended,
|
|
178
|
+
"dlc": msg.length,
|
|
179
|
+
"signals": [f"{msg_name}.{sig.api_var_name}"
|
|
180
|
+
for sig in _ordered_signals(msg)],
|
|
181
|
+
}
|
|
182
|
+
for sig in _ordered_signals(msg):
|
|
183
|
+
info = _signal_info(sig)
|
|
184
|
+
if info:
|
|
185
|
+
signal_info[f"{msg_name}.{sig.api_var_name}"] = info
|
|
186
|
+
return {
|
|
187
|
+
"format_version": CBDB_FORMAT_VERSION,
|
|
188
|
+
"kcd_sha256": hashlib.sha256(kcd_path.read_bytes()).hexdigest(),
|
|
189
|
+
"signals": id_map,
|
|
190
|
+
"messages": messages,
|
|
191
|
+
"signal_info": signal_info,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _signal_info(sig) -> dict:
|
|
196
|
+
"""Presentation info for a signal: unit, physical range, and enum value
|
|
197
|
+
labels — only the fields the KCD actually defines. Additive sidecar data
|
|
198
|
+
(the binary cbdb and the `signals` id map are unchanged)."""
|
|
199
|
+
info = {}
|
|
200
|
+
if sig.unit:
|
|
201
|
+
info["unit"] = sig.unit
|
|
202
|
+
if sig.min is not None:
|
|
203
|
+
info["min"] = sig.min
|
|
204
|
+
if sig.max is not None:
|
|
205
|
+
info["max"] = sig.max
|
|
206
|
+
if sig.labels:
|
|
207
|
+
info["labels"] = {str(value): name for value, name in sig.labels.items()}
|
|
208
|
+
return info
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from can_api_generator.msg_processor import CAPIMessage
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import can_db_model
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_msgs_data_dicts_from_kcd(
|
|
7
|
+
db_path: Path, api_name: str, node_name: str):
|
|
8
|
+
"""Load a KCD or YAML database and build CAPIMessage dicts for NODE_NAME.
|
|
9
|
+
|
|
10
|
+
Returns (list-of-message-dicts, CanDb). The path may be .kcd or .yaml/.yml;
|
|
11
|
+
can_db_model.load dispatches on the extension.
|
|
12
|
+
"""
|
|
13
|
+
db = can_db_model.load(Path(db_path))
|
|
14
|
+
bus_name = db.bus.name if db.bus is not None else ""
|
|
15
|
+
msgs = [CAPIMessage(
|
|
16
|
+
msg,
|
|
17
|
+
api_name=api_name,
|
|
18
|
+
node_name=node_name,
|
|
19
|
+
bus_name=bus_name)
|
|
20
|
+
for msg in db.messages]
|
|
21
|
+
for msg in msgs:
|
|
22
|
+
for sig in msg.signals:
|
|
23
|
+
sig.resolve_muxed_signal_name()
|
|
24
|
+
return ([msg.to_dict() for msg in msgs], db)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_api_implementation(msgs: list[dict],
|
|
28
|
+
jinja_env, api_name: str,
|
|
29
|
+
with_encode_all_func: bool,
|
|
30
|
+
generate_everything: bool,
|
|
31
|
+
arb_id_member_name: str,
|
|
32
|
+
length_member_name: str,
|
|
33
|
+
payload_member_name: str,
|
|
34
|
+
global_rx: bool,
|
|
35
|
+
periodic_tx: bool,
|
|
36
|
+
ti_compatible: bool = False,
|
|
37
|
+
send_now: bool = False,
|
|
38
|
+
double_buffered: bool = False):
|
|
39
|
+
header_template = jinja_env.get_template("header_template.jinja")
|
|
40
|
+
source_template = jinja_env.get_template("source_template.jinja")
|
|
41
|
+
canframe_template = jinja_env.get_template("canframe.jinja")
|
|
42
|
+
lib_name_struct_prefix = ''.join(map(lambda s: s.capitalize(),
|
|
43
|
+
api_name.split('_')))
|
|
44
|
+
header_code = header_template.render(
|
|
45
|
+
messages=msgs,
|
|
46
|
+
api_name=api_name,
|
|
47
|
+
encode_every_signal_func=with_encode_all_func,
|
|
48
|
+
generate_everything=generate_everything,
|
|
49
|
+
arb_id_member_name=arb_id_member_name,
|
|
50
|
+
length_member_name=length_member_name,
|
|
51
|
+
payload_member_name=payload_member_name,
|
|
52
|
+
global_rx=global_rx,
|
|
53
|
+
api_name_struct_prefix=lib_name_struct_prefix,
|
|
54
|
+
periodic_tx=periodic_tx,
|
|
55
|
+
send_now=send_now,
|
|
56
|
+
double_buffered=double_buffered,
|
|
57
|
+
)
|
|
58
|
+
source_code = source_template.render(
|
|
59
|
+
messages=msgs,
|
|
60
|
+
api_name = api_name,
|
|
61
|
+
header_file_name=api_name + ".h",
|
|
62
|
+
encode_every_signal_func=with_encode_all_func,
|
|
63
|
+
generate_everything=generate_everything,
|
|
64
|
+
arb_id_member_name=arb_id_member_name,
|
|
65
|
+
length_member_name=length_member_name,
|
|
66
|
+
payload_member_name=payload_member_name,
|
|
67
|
+
global_rx=global_rx,
|
|
68
|
+
api_name_struct_prefix=lib_name_struct_prefix,
|
|
69
|
+
periodic_tx=periodic_tx,
|
|
70
|
+
ti_compatible=ti_compatible,
|
|
71
|
+
send_now=send_now,
|
|
72
|
+
double_buffered=double_buffered,
|
|
73
|
+
)
|
|
74
|
+
canframe_header_code = canframe_template.render(
|
|
75
|
+
arb_id_member_name=arb_id_member_name,
|
|
76
|
+
length_member_name=length_member_name,
|
|
77
|
+
payload_member_name=payload_member_name
|
|
78
|
+
)
|
|
79
|
+
return header_code, source_code, canframe_header_code
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def generate_dyndec_implementation(jinja_env,
|
|
83
|
+
arb_id_member_name: str,
|
|
84
|
+
length_member_name: str,
|
|
85
|
+
payload_member_name: str,
|
|
86
|
+
header_file_name: str = "dyndec.h"):
|
|
87
|
+
"""Render the KCD-independent dynamic-decoder module.
|
|
88
|
+
|
|
89
|
+
The module decodes frames at runtime against a descriptor cbdb compiled
|
|
90
|
+
by the `cbdb` path; it is generated once per firmware, not per database.
|
|
91
|
+
Returns (header_code, source_code, canframe_header_code).
|
|
92
|
+
"""
|
|
93
|
+
header_template = jinja_env.get_template("dyndec_header.jinja")
|
|
94
|
+
source_template = jinja_env.get_template("dyndec_source.jinja")
|
|
95
|
+
canframe_template = jinja_env.get_template("canframe.jinja")
|
|
96
|
+
header_code = header_template.render()
|
|
97
|
+
source_code = source_template.render(
|
|
98
|
+
header_file_name=header_file_name,
|
|
99
|
+
arb_id_member_name=arb_id_member_name,
|
|
100
|
+
length_member_name=length_member_name,
|
|
101
|
+
payload_member_name=payload_member_name,
|
|
102
|
+
)
|
|
103
|
+
canframe_header_code = canframe_template.render(
|
|
104
|
+
arb_id_member_name=arb_id_member_name,
|
|
105
|
+
length_member_name=length_member_name,
|
|
106
|
+
payload_member_name=payload_member_name,
|
|
107
|
+
)
|
|
108
|
+
return header_code, source_code, canframe_header_code
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def generate_canframe_header(dest_path: Path, canframe_code: str):
|
|
112
|
+
with dest_path.open() as canframe_header:
|
|
113
|
+
canframe_header.write(canframe_code)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_test_files_from_source_str(capi_header, capi_source,
|
|
117
|
+
canframe_header_code,
|
|
118
|
+
cantools_header, cantools_source,
|
|
119
|
+
test_source,
|
|
120
|
+
kcd_stem,
|
|
121
|
+
test_name):
|
|
122
|
+
here = Path('.')
|
|
123
|
+
source_path = here / (test_name + ".c")
|
|
124
|
+
header_path = here / (test_name + ".h")
|
|
125
|
+
canframe_header_path = here / 'canframe.h'
|
|
126
|
+
cantools_source_path = here / (kcd_stem + ".c")
|
|
127
|
+
cantools_header_path = here / (kcd_stem + ".h")
|
|
128
|
+
test_source_path = here / ("test_kcd_api_" + kcd_stem + ".c")
|
|
129
|
+
print(canframe_header_path)
|
|
130
|
+
with canframe_header_path.open('w+') as canframe_header:
|
|
131
|
+
canframe_header.write(canframe_header_code)
|
|
132
|
+
with source_path.open('w+') as source_file:
|
|
133
|
+
source_file.write(capi_source)
|
|
134
|
+
print(header_path)
|
|
135
|
+
with header_path.open('w+') as header_file:
|
|
136
|
+
header_file.write(capi_header)
|
|
137
|
+
print(cantools_source_path)
|
|
138
|
+
with cantools_source_path.open('w+') as cantools_source_file:
|
|
139
|
+
cantools_source_file.write(cantools_source)
|
|
140
|
+
print(cantools_header_path)
|
|
141
|
+
with cantools_header_path.open('w+') as cantools_header_file:
|
|
142
|
+
cantools_header_file.write(cantools_header)
|
|
143
|
+
print(test_source_path)
|
|
144
|
+
with test_source_path.open('w+') as test_source_file:
|
|
145
|
+
test_source_file.write(test_source)
|
|
146
|
+
return (header_path, source_path, canframe_header_path,
|
|
147
|
+
cantools_header_path, cantools_source_path,
|
|
148
|
+
test_source_path)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from can_api_generator.types import SignalDirection
|
|
2
|
+
from importlib.metadata import files
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import click
|
|
6
|
+
import rich
|
|
7
|
+
from can_api_generator.msg_processor import CAPIMessage
|
|
8
|
+
from can_api_generator.msg_processor import MessageProcessingException
|
|
9
|
+
from can_api_generator.msg_processor import SignalProcessingException
|
|
10
|
+
from can_api_generator.code_generator import generate_api_implementation, build_msgs_data_dicts_from_kcd, generate_dyndec_implementation
|
|
11
|
+
from can_api_generator.cbdb_generator import generate_cbdb, build_sidecar, CbdbGenerationException, CBDB_FORMAT_VERSION
|
|
12
|
+
from can_api_generator.utils import converrt_to_c_identifier
|
|
13
|
+
from jinja2 import Environment, FileSystemLoader
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def main():
|
|
19
|
+
"""
|
|
20
|
+
A tool to generate C source code and descriptor cbdbs to interact with
|
|
21
|
+
the signals encoded in CAN frames according to DBC/KCD schemata
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@main.command()
|
|
26
|
+
@click.argument("DB_File", type=click.Path(exists=True, dir_okay=False),
|
|
27
|
+
required=True, nargs=1)
|
|
28
|
+
@click.argument("Library_Name", type=str, required=True)
|
|
29
|
+
@click.argument("Node_Name", type=str, required=True)
|
|
30
|
+
@click.option("-f", "--force", is_flag=True, required=False, default=False,
|
|
31
|
+
help="Overwrite existing output files")
|
|
32
|
+
@click.option("-a", "--encode-all-signals-func", is_flag=True, default=False,
|
|
33
|
+
help="Generate a function for each message that encodes every signal into frames at least once,"
|
|
34
|
+
" this is helpful when some signals are inside of a mux, but all signals need to be sent "
|
|
35
|
+
" regardless of how many frames need to be encoded to send the state of each signal at least"
|
|
36
|
+
" once")
|
|
37
|
+
@click.option("-im", "--id-member-name", type=str, default="arbitration_id",
|
|
38
|
+
help="The name of the CanFrame struct member that holds the "
|
|
39
|
+
"arbitration id as uint32")
|
|
40
|
+
@click.option("-lm", "--length-member-name", type=str, default="length",
|
|
41
|
+
help="The name of the CanFrame struct member that holds the "
|
|
42
|
+
"message length as uint16")
|
|
43
|
+
@click.option("-pm", "--payload-member-name", type=str, default="payload",
|
|
44
|
+
help="The name of the CanFrame struct member that holds the "
|
|
45
|
+
"payload as uint64")
|
|
46
|
+
@click.option("-r", "--global-rx", is_flag=True, default=False,
|
|
47
|
+
help="Generate a function and api-wide struct that updates an"
|
|
48
|
+
" api message struct upon reception of a matching frame")
|
|
49
|
+
@click.option("-p", "--periodic-tx", is_flag=True, default=False,
|
|
50
|
+
help="Generate code that will define a TX struct as well as a function "
|
|
51
|
+
"that periodically sends the messages according to their "
|
|
52
|
+
"periodicity")
|
|
53
|
+
@click.option("--ti-compatible", is_flag=True, default=False,
|
|
54
|
+
help="Generate C code that can be compiled using the TI C2000 compiler"
|
|
55
|
+
" The TI compiler does not handle variable declarations inside of case"
|
|
56
|
+
" blocks so the variable declaration is moved to the top of a switch")
|
|
57
|
+
@click.option("--send-now", is_flag=True, default=False,
|
|
58
|
+
help="Add mechanism in to the periodic tx function that allows to immediately "
|
|
59
|
+
"send a message even if it is not enabled or has just been sent."
|
|
60
|
+
" This is useful for updating critical signals as early as possible after a change")
|
|
61
|
+
@click.option("--double-buffered", is_flag=True, default=False,
|
|
62
|
+
help="Generate double-buffered signal storage so that an ISR can safely decode"
|
|
63
|
+
" RX frames without exposing a partially-decoded struct to readers, and so"
|
|
64
|
+
" that user code can stage TX signal updates without racing the periodic TX"
|
|
65
|
+
" encoder. Swaps are performed via per-message pointer swaps; an explicit"
|
|
66
|
+
" <api>_<msg>_commit call is required to publish staged TX values.")
|
|
67
|
+
@click.option("-v", "--verbose", is_flag=True, default=False,
|
|
68
|
+
help="Print the Data that is passed to the template that "
|
|
69
|
+
"generates the code")
|
|
70
|
+
def api(db_file, library_name, node_name, force, encode_all_signals_func,
|
|
71
|
+
id_member_name, length_member_name, payload_member_name, global_rx,
|
|
72
|
+
periodic_tx, ti_compatible, send_now, double_buffered, verbose):
|
|
73
|
+
"""
|
|
74
|
+
Generate the static C API for NODE_NAME from DB_FILE (.kcd or .yaml)
|
|
75
|
+
"""
|
|
76
|
+
db_path = Path(db_file)
|
|
77
|
+
try:
|
|
78
|
+
basepath = Path(library_name)
|
|
79
|
+
source_path = basepath.parent / f"{basepath.stem}.c"
|
|
80
|
+
header_path = basepath.parent / f"{basepath.stem}.h"
|
|
81
|
+
if not force:
|
|
82
|
+
if source_path.exists():
|
|
83
|
+
raise FileExistsError(
|
|
84
|
+
f"The File {source_path} already exists, aborting")
|
|
85
|
+
if header_path.exists():
|
|
86
|
+
raise FileExistsError(
|
|
87
|
+
f"The File {header_path} already exists, aborting")
|
|
88
|
+
except FileExistsError as e:
|
|
89
|
+
click.echo(e)
|
|
90
|
+
try:
|
|
91
|
+
msgs, kcd_db = build_msgs_data_dicts_from_kcd(db_path, library_name, node_name)
|
|
92
|
+
except MessageProcessingException as e:
|
|
93
|
+
click.echo(f"Got Error while processing message {e.can_message_name}"
|
|
94
|
+
f" for {e.node_name}: {e}")
|
|
95
|
+
return 1
|
|
96
|
+
except SignalProcessingException as e:
|
|
97
|
+
click.echo(f"Got Error while processing signal {e.signal_name}"
|
|
98
|
+
f" in {e.message_name} for {e.node_name}: {e}")
|
|
99
|
+
return 1
|
|
100
|
+
if (verbose):
|
|
101
|
+
rich.print(msgs)
|
|
102
|
+
env = Environment(
|
|
103
|
+
loader=FileSystemLoader(
|
|
104
|
+
[str(Path(__file__).parent / "templates")]
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
header_code, source_code, canframe_header = generate_api_implementation(
|
|
108
|
+
msgs,
|
|
109
|
+
env,
|
|
110
|
+
library_name,
|
|
111
|
+
encode_all_signals_func,
|
|
112
|
+
False,
|
|
113
|
+
id_member_name,
|
|
114
|
+
length_member_name,
|
|
115
|
+
payload_member_name,
|
|
116
|
+
global_rx,
|
|
117
|
+
periodic_tx=periodic_tx,
|
|
118
|
+
ti_compatible=ti_compatible,
|
|
119
|
+
send_now=send_now,
|
|
120
|
+
double_buffered=double_buffered,
|
|
121
|
+
)
|
|
122
|
+
with (header_path.parent / 'canframe.h').open('w+') as canframe_header_file:
|
|
123
|
+
canframe_header_file.write(canframe_header)
|
|
124
|
+
with header_path.open('w+') as header_file:
|
|
125
|
+
header_file.write(header_code)
|
|
126
|
+
with source_path.open('w+') as source_file:
|
|
127
|
+
source_file.write(source_code)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@main.command()
|
|
131
|
+
@click.argument("DB_File", type=click.Path(exists=True, dir_okay=False),
|
|
132
|
+
required=True, nargs=1)
|
|
133
|
+
@click.argument("Output_Base", type=str, required=True)
|
|
134
|
+
@click.option("-f", "--force", is_flag=True, required=False, default=False,
|
|
135
|
+
help="Overwrite existing output files")
|
|
136
|
+
def cbdb(db_file, output_base, force):
|
|
137
|
+
"""
|
|
138
|
+
Compile DB_FILE (.kcd or .yaml) into a dynamic-decoder descriptor cbdb
|
|
139
|
+
|
|
140
|
+
Writes OUTPUT_BASE.cbdb (the binary descriptor table consumed by the
|
|
141
|
+
dyndec C runtime) and OUTPUT_BASE_ids.json (resolved signal name ->
|
|
142
|
+
signal_id map for client applications). The cbdb contains every signal
|
|
143
|
+
of every message: the dynamic decoder is a bus observer, not a node.
|
|
144
|
+
"""
|
|
145
|
+
db_path = Path(db_file)
|
|
146
|
+
base_path = Path(output_base)
|
|
147
|
+
cbdb_path = base_path.parent / f"{base_path.name}.cbdb"
|
|
148
|
+
sidecar_path = base_path.parent / f"{base_path.name}_ids.json"
|
|
149
|
+
if not force:
|
|
150
|
+
for path in (cbdb_path, sidecar_path):
|
|
151
|
+
if path.exists():
|
|
152
|
+
click.echo(f"The File {path} already exists, aborting")
|
|
153
|
+
raise SystemExit(1)
|
|
154
|
+
try:
|
|
155
|
+
cbdb_bytes, id_map = generate_cbdb(db_path)
|
|
156
|
+
sidecar = build_sidecar(db_path)
|
|
157
|
+
except (MessageProcessingException, SignalProcessingException,
|
|
158
|
+
CbdbGenerationException) as e:
|
|
159
|
+
click.echo(f"Got Error while compiling cbdb: {e}")
|
|
160
|
+
raise SystemExit(1)
|
|
161
|
+
cbdb_path.write_bytes(cbdb_bytes)
|
|
162
|
+
with sidecar_path.open('w+') as sidecar_file:
|
|
163
|
+
json.dump(sidecar, sidecar_file, indent=2)
|
|
164
|
+
sidecar_file.write('\n')
|
|
165
|
+
click.echo(f"Wrote {cbdb_path} ({len(cbdb_bytes)} bytes, "
|
|
166
|
+
f"{len(id_map)} signals) and {sidecar_path}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@main.command()
|
|
170
|
+
@click.argument("Library_Name", type=str, required=True)
|
|
171
|
+
@click.option("-f", "--force", is_flag=True, required=False, default=False,
|
|
172
|
+
help="Overwrite existing output files")
|
|
173
|
+
@click.option("-im", "--id-member-name", type=str, default="arbitration_id",
|
|
174
|
+
help="The name of the CanFrame struct member that holds the "
|
|
175
|
+
"arbitration id as uint32")
|
|
176
|
+
@click.option("-lm", "--length-member-name", type=str, default="length",
|
|
177
|
+
help="The name of the CanFrame struct member that holds the "
|
|
178
|
+
"message length as uint16")
|
|
179
|
+
@click.option("-pm", "--payload-member-name", type=str, default="payload",
|
|
180
|
+
help="The name of the CanFrame struct member that holds the "
|
|
181
|
+
"payload as uint64")
|
|
182
|
+
def dyndec(library_name, force, id_member_name, length_member_name,
|
|
183
|
+
payload_member_name):
|
|
184
|
+
"""
|
|
185
|
+
Generate the KCD-independent dynamic-decoder C module
|
|
186
|
+
|
|
187
|
+
The module decodes CAN frames at runtime against a descriptor cbdb
|
|
188
|
+
compiled with the `cbdb` subcommand. It is generated once per firmware,
|
|
189
|
+
not per database: provisioning a new cbdb requires no recompilation.
|
|
190
|
+
"""
|
|
191
|
+
basepath = Path(library_name)
|
|
192
|
+
source_path = basepath.parent / f"{basepath.stem}.c"
|
|
193
|
+
header_path = basepath.parent / f"{basepath.stem}.h"
|
|
194
|
+
if not force:
|
|
195
|
+
for path in (source_path, header_path):
|
|
196
|
+
if path.exists():
|
|
197
|
+
click.echo(f"The File {path} already exists, aborting")
|
|
198
|
+
raise SystemExit(1)
|
|
199
|
+
env = Environment(
|
|
200
|
+
loader=FileSystemLoader(
|
|
201
|
+
[str(Path(__file__).parent / "templates")]
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
header_code, source_code, canframe_header = generate_dyndec_implementation(
|
|
205
|
+
env,
|
|
206
|
+
arb_id_member_name=id_member_name,
|
|
207
|
+
length_member_name=length_member_name,
|
|
208
|
+
payload_member_name=payload_member_name,
|
|
209
|
+
header_file_name=header_path.name,
|
|
210
|
+
)
|
|
211
|
+
with (header_path.parent / 'canframe.h').open('w+') as canframe_header_file:
|
|
212
|
+
canframe_header_file.write(canframe_header)
|
|
213
|
+
with header_path.open('w+') as header_file:
|
|
214
|
+
header_file.write(header_code)
|
|
215
|
+
with source_path.open('w+') as source_file:
|
|
216
|
+
source_file.write(source_code)
|