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.
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2025-present Alexande Becker <nabla.becker@mailbox.org>
2
+ #
3
+ # SPDX-License-Identifier: LGPL-3.0-only
4
+ __version__ = "1.0.0"
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025-present Alexande Becker <nabla.becker@mailbox.org>
2
+ #
3
+ # SPDX-License-Identifier: MIT
@@ -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)