can-api-generator 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/PKG-INFO +25 -1
  2. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/README.md +24 -0
  3. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/__about__.py +1 -1
  4. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/code_generator.py +18 -5
  5. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/main.py +17 -2
  6. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/msg_processor.py +9 -4
  7. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/declaration_macros.jinja +0 -3
  8. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/header_template.jinja +6 -6
  9. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/rx_macros.jinja +7 -7
  10. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/source_template.jinja +3 -3
  11. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/tx_macros.jinja +7 -7
  12. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/utils.py +7 -0
  13. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_cli.py +144 -9
  14. can_api_generator-2.1.0/tests/test_utils.py +15 -0
  15. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/.gitignore +0 -0
  16. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/LICENSE.txt +0 -0
  17. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/manual_cag_test.py +0 -0
  18. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/pyproject.toml +0 -0
  19. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/__init__.py +0 -0
  20. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/cbdb_generator.py +0 -0
  21. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/canframe.jinja +0 -0
  22. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/dyndec_header.jinja +0 -0
  23. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/dyndec_source.jinja +0 -0
  24. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/templates/encoding_macros.jinja +0 -0
  25. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/src/can_api_generator/types.py +0 -0
  26. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/__init__.py +0 -0
  27. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/cantool_test_utils.jinja +0 -0
  28. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/compat_shim.jinja +0 -0
  29. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/dyndec_test_harness.c +0 -0
  30. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/multiple_busses_test.kcd +0 -0
  31. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_1.kcd +0 -0
  32. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_basic_multiplex.kcd +0 -0
  33. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_big_endian.kcd +0 -0
  34. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_can_db_model_dep.py +0 -0
  35. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_cbdb_generator.py +0 -0
  36. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_cross_interface_compat.jinja +0 -0
  37. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_cross_interface_compat.py +0 -0
  38. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_dyndec_round_trip.py +0 -0
  39. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_float_wrong_length.kcd +0 -0
  40. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_message_parser.py +0 -0
  41. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_missing_consumer.kcd +0 -0
  42. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_missing_producer.kcd +0 -0
  43. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_mux_group_count_out_of_order.kcd +0 -0
  44. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_mux_with_labeled_index.kcd +0 -0
  45. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_mux_with_partially_labled_index.kcd +0 -0
  46. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_mux_with_same_signal_names.kcd +0 -0
  47. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_mux_with_same_signal_names_labeled_mux_variants.kcd +0 -0
  48. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_mux_with_scale_and_offset.kcd +0 -0
  49. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_non_encodable_0.kcd +0 -0
  50. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_overlapping_signals.kcd +0 -0
  51. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_periodic_tx_over_can.jinja +0 -0
  52. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_procedures.jinja +0 -0
  53. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_prog_source_template.jinja +0 -0
  54. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_rx_over_can.jinja +0 -0
  55. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_direction.kcd +0 -0
  56. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/double_plain.kcd +0 -0
  57. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/double_scaled.kcd +0 -0
  58. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/double_scaled_offset.kcd +0 -0
  59. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/float_plain.kcd +0 -0
  60. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/float_scaled.kcd +0 -0
  61. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/float_scaled_offset.kcd +0 -0
  62. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_matching_fractional_scaled.kcd +0 -0
  63. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_matching_fractional_scaled_offset.kcd +0 -0
  64. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_matching_plain.kcd +0 -0
  65. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_matching_scaled.kcd +0 -0
  66. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_matching_scaled_offset.kcd +0 -0
  67. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_nonmatching_fractional_scaled.kcd +0 -0
  68. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_nonmatching_plain.kcd +0 -0
  69. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_nonmatching_scaled.kcd +0 -0
  70. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/sint_nonmatching_scaled_offset.kcd +0 -0
  71. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/test_decoding_numerical_value.jinja +0 -0
  72. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/test_encoding_numerical_value.jinja +0 -0
  73. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/test_signal_encoding_wrong_float_size.kcd +0 -0
  74. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_matching_fractional_scaled.kcd +0 -0
  75. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_matching_plain.kcd +0 -0
  76. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_matching_scaled.kcd +0 -0
  77. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_matching_scaled_offset.kcd +0 -0
  78. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_nonmatching_fractional_scaled.kcd +0 -0
  79. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_nonmatching_plain.kcd +0 -0
  80. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_nonmatching_scaled.kcd +0 -0
  81. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_encoding/uint_nonmatching_scaled_offset.kcd +0 -0
  82. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_signal_info.kcd +0 -0
  83. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_too_large_message.kcd +0 -0
  84. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_tx_over_can.jinja +0 -0
  85. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_vcan_round_trip.py +0 -0
  86. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_yaml_cbdb.py +0 -0
  87. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_yaml_input.py +0 -0
  88. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/tests/test_yaml_kcd_equivalence.py +0 -0
  89. {can_api_generator-2.0.0 → can_api_generator-2.1.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: can-api-generator
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Program to generate a 'signal' style CAN API in C
5
5
  Project-URL: Documentation, https://sr.ht/~laplace/can-api-generator#readme
6
6
  Project-URL: Issues, https://sr.ht/~laplace/can-api-generator/issues
@@ -283,6 +283,26 @@ translation unit. This is sufficient for a single-core MCU where the dispatcher
283
283
  case. On weakly-ordered multicore hosts, user code is responsible for inserting the appropriate memory barrier between the decode writes and the pointer
284
284
  publish, and between reading the pointer and dereferencing it — the generator does not emit any barriers itself.
285
285
 
286
+ ### Coexisting APIs and struct-name prefixes
287
+
288
+ The per-message signal structs (`struct <Message>`) are emitted **without** an include-guard. Two headers that share a message name therefore cannot be included in
289
+ one translation unit unless their struct names are made distinct. (An earlier release briefly wrapped these structs in a `#ifndef STRUCT_<NAME>` guard; that was never
290
+ an intended feature — it silently reused the first header's struct layout when two headers defined the same struct name, hiding real layout mismatches — and has been
291
+ removed.)
292
+
293
+ To include two generated headers that share message names in one translation unit — for example the API for two versions of the same database — generate them with
294
+ `--prefix-signal-struct-names` and `--prefix-context-struct-names`. Each prefixes the relevant struct type names (and, for the context structs, their include-guard
295
+ tokens) with the CamelCase API prefix already used for `<Prefix>RxSignals` / `<Prefix>TxSignals`, so the two APIs get non-colliding struct names:
296
+
297
+ ```c
298
+ /* generated with LIBRARY_NAME "battery_v2" and both prefix flags */
299
+ struct BatteryV2Message1 { /* ... */ };
300
+ struct BatteryV2MsgRxContext { /* ... */ }; /* guard: #ifndef BATTERYV2MSGRXCONTEXT */
301
+ struct BatteryV2MsgTxContext { /* ... */ }; /* guard: #ifndef BATTERYV2MSGTXCONTEXT */
302
+ ```
303
+
304
+ Both flags are off by default and reuse the existing API prefix — they change struct type *names* only, never layout or field order.
305
+
286
306
  ### RX/TX callback context
287
307
 
288
308
  Both the [Global RX](#global-rx) and [Periodic TX](#periodic-tx) paths let you register a per-message callback. Without a context flag those callbacks take no arguments
@@ -565,6 +585,10 @@ Options:
565
585
  generated and decoded signals reach the application only through the RX callback. See [Global RX](#global-rx). Only meaningful together with `-r`.
566
586
  * `--with-rx-valid` — in callback-only mode, add a `valid` field to `MsgRxContext` and set it on each matched frame, for struct-layout compatibility with
567
587
  store-based APIs. No effect with `--rx-signal-store`. Only meaningful together with `-r`.
588
+ * `--prefix-signal-struct-names` — prefix every per-message signal struct type name with the CamelCase API prefix (the same prefix used for `<Prefix>RxSignals` /
589
+ `<Prefix>TxSignals`). Lets two similar APIs (e.g. two versions of one database) coexist in a single translation unit with distinct struct names. Off by default.
590
+ * `--prefix-context-struct-names` — prefix the `MsgRxContext` / `MsgTxContext` type names and their include-guard tokens with the CamelCase API prefix, so the
591
+ context structs of two coexisting APIs do not share a single definition. Off by default.
568
592
  * `-im`, `--id-member-name TEXT` — override the name of the `CanFrame` member that carries the arbitration id (default `arbitration_id`). Used to adapt the API
569
593
  to an existing project's frame struct naming.
570
594
  * `-lm`, `--length-member-name TEXT` — override the name of the `CanFrame` length member (default `length`).
@@ -258,6 +258,26 @@ translation unit. This is sufficient for a single-core MCU where the dispatcher
258
258
  case. On weakly-ordered multicore hosts, user code is responsible for inserting the appropriate memory barrier between the decode writes and the pointer
259
259
  publish, and between reading the pointer and dereferencing it — the generator does not emit any barriers itself.
260
260
 
261
+ ### Coexisting APIs and struct-name prefixes
262
+
263
+ The per-message signal structs (`struct <Message>`) are emitted **without** an include-guard. Two headers that share a message name therefore cannot be included in
264
+ one translation unit unless their struct names are made distinct. (An earlier release briefly wrapped these structs in a `#ifndef STRUCT_<NAME>` guard; that was never
265
+ an intended feature — it silently reused the first header's struct layout when two headers defined the same struct name, hiding real layout mismatches — and has been
266
+ removed.)
267
+
268
+ To include two generated headers that share message names in one translation unit — for example the API for two versions of the same database — generate them with
269
+ `--prefix-signal-struct-names` and `--prefix-context-struct-names`. Each prefixes the relevant struct type names (and, for the context structs, their include-guard
270
+ tokens) with the CamelCase API prefix already used for `<Prefix>RxSignals` / `<Prefix>TxSignals`, so the two APIs get non-colliding struct names:
271
+
272
+ ```c
273
+ /* generated with LIBRARY_NAME "battery_v2" and both prefix flags */
274
+ struct BatteryV2Message1 { /* ... */ };
275
+ struct BatteryV2MsgRxContext { /* ... */ }; /* guard: #ifndef BATTERYV2MSGRXCONTEXT */
276
+ struct BatteryV2MsgTxContext { /* ... */ }; /* guard: #ifndef BATTERYV2MSGTXCONTEXT */
277
+ ```
278
+
279
+ Both flags are off by default and reuse the existing API prefix — they change struct type *names* only, never layout or field order.
280
+
261
281
  ### RX/TX callback context
262
282
 
263
283
  Both the [Global RX](#global-rx) and [Periodic TX](#periodic-tx) paths let you register a per-message callback. Without a context flag those callbacks take no arguments
@@ -540,6 +560,10 @@ Options:
540
560
  generated and decoded signals reach the application only through the RX callback. See [Global RX](#global-rx). Only meaningful together with `-r`.
541
561
  * `--with-rx-valid` — in callback-only mode, add a `valid` field to `MsgRxContext` and set it on each matched frame, for struct-layout compatibility with
542
562
  store-based APIs. No effect with `--rx-signal-store`. Only meaningful together with `-r`.
563
+ * `--prefix-signal-struct-names` — prefix every per-message signal struct type name with the CamelCase API prefix (the same prefix used for `<Prefix>RxSignals` /
564
+ `<Prefix>TxSignals`). Lets two similar APIs (e.g. two versions of one database) coexist in a single translation unit with distinct struct names. Off by default.
565
+ * `--prefix-context-struct-names` — prefix the `MsgRxContext` / `MsgTxContext` type names and their include-guard tokens with the CamelCase API prefix, so the
566
+ context structs of two coexisting APIs do not share a single definition. Off by default.
543
567
  * `-im`, `--id-member-name TEXT` — override the name of the `CanFrame` member that carries the arbitration id (default `arbitration_id`). Used to adapt the API
544
568
  to an existing project's frame struct naming.
545
569
  * `-lm`, `--length-member-name TEXT` — override the name of the `CanFrame` length member (default `length`).
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Alexande Becker <nabla.becker@mailbox.org>
2
2
  #
3
3
  # SPDX-License-Identifier: LGPL-3.0-only
4
- __version__ = "2.0.0"
4
+ __version__ = "2.1.0"
@@ -1,10 +1,12 @@
1
1
  from can_api_generator.msg_processor import CAPIMessage
2
+ from can_api_generator.utils import to_struct_prefix
2
3
  from pathlib import Path
3
4
  import can_db_model
4
5
 
5
6
 
6
7
  def build_msgs_data_dicts_from_kcd(
7
- db_path: Path, api_name: str, node_name: str):
8
+ db_path: Path, api_name: str, node_name: str,
9
+ prefix_signal_struct_names: bool = False):
8
10
  """Load a KCD or YAML database and build CAPIMessage dicts for NODE_NAME.
9
11
 
10
12
  Returns (list-of-message-dicts, CanDb). The path may be .kcd or .yaml/.yml;
@@ -16,7 +18,8 @@ def build_msgs_data_dicts_from_kcd(
16
18
  msg,
17
19
  api_name=api_name,
18
20
  node_name=node_name,
19
- bus_name=bus_name)
21
+ bus_name=bus_name,
22
+ prefix_signal_struct_names=prefix_signal_struct_names)
20
23
  for msg in db.messages]
21
24
  for msg in msgs:
22
25
  for sig in msg.signals:
@@ -42,12 +45,18 @@ def generate_api_implementation(msgs: list[dict],
42
45
  rx_signal_store: bool = False,
43
46
  with_rx_valid: bool = False,
44
47
  msg_as_tx_callback_arg: bool = False,
45
- tx_signal_store: bool = False):
48
+ tx_signal_store: bool = False,
49
+ prefix_context_struct_names: bool = False):
46
50
  header_template = jinja_env.get_template("header_template.jinja")
47
51
  source_template = jinja_env.get_template("source_template.jinja")
48
52
  canframe_template = jinja_env.get_template("canframe.jinja")
49
- lib_name_struct_prefix = ''.join(map(lambda s: s.capitalize(),
50
- api_name.split('_')))
53
+ lib_name_struct_prefix = to_struct_prefix(api_name)
54
+ if prefix_context_struct_names:
55
+ rx_context_struct_name = lib_name_struct_prefix + "MsgRxContext"
56
+ tx_context_struct_name = lib_name_struct_prefix + "MsgTxContext"
57
+ else:
58
+ rx_context_struct_name = "MsgRxContext"
59
+ tx_context_struct_name = "MsgTxContext"
51
60
  # Callback-only global RX (no store) can only deliver signals through the
52
61
  # callback, so the message-as-callback-argument behavior is implied.
53
62
  if not rx_signal_store:
@@ -72,6 +81,8 @@ def generate_api_implementation(msgs: list[dict],
72
81
  with_rx_valid=with_rx_valid,
73
82
  msg_as_tx_callback_arg=msg_as_tx_callback_arg,
74
83
  tx_signal_store=tx_signal_store,
84
+ rx_context_struct_name=rx_context_struct_name,
85
+ tx_context_struct_name=tx_context_struct_name,
75
86
  )
76
87
  source_code = source_template.render(
77
88
  messages=msgs,
@@ -95,6 +106,8 @@ def generate_api_implementation(msgs: list[dict],
95
106
  with_rx_valid=with_rx_valid,
96
107
  msg_as_tx_callback_arg=msg_as_tx_callback_arg,
97
108
  tx_signal_store=tx_signal_store,
109
+ rx_context_struct_name=rx_context_struct_name,
110
+ tx_context_struct_name=tx_context_struct_name,
98
111
  )
99
112
  canframe_header_code = canframe_template.render(
100
113
  arb_id_member_name=arb_id_member_name,
@@ -100,6 +100,17 @@ def main():
100
100
  "periodic TX is callback-only: each scheduled message's "
101
101
  "callback returns the struct to encode, and a NULL return or "
102
102
  "missing callback sends nothing. Only meaningful with -p.")
103
+ @click.option("--prefix-signal-struct-names", is_flag=True, default=False,
104
+ help="Prefix every per-message signal struct type name with the "
105
+ "CamelCase API prefix (the same prefix used for "
106
+ "<Prefix>RxSignals / <Prefix>TxSignals), so two similar APIs "
107
+ "(e.g. two versions of a database) can coexist in one "
108
+ "translation unit with distinct struct names.")
109
+ @click.option("--prefix-context-struct-names", is_flag=True, default=False,
110
+ help="Prefix the MsgRxContext / MsgTxContext struct type names (and "
111
+ "their include-guard tokens) with the CamelCase API prefix, so "
112
+ "context structs from two coexisting APIs do not share one "
113
+ "definition.")
103
114
  @click.option("-v", "--verbose", is_flag=True, default=False,
104
115
  help="Print the Data that is passed to the template that "
105
116
  "generates the code")
@@ -108,7 +119,8 @@ def api(db_file, library_name, node_name, force, encode_all_signals_func,
108
119
  periodic_tx, ti_compatible, send_now, double_buffered,
109
120
  rx_callback_context, tx_callback_context, msg_as_rx_callback_arg,
110
121
  rx_signal_store, with_rx_valid, msg_as_tx_callback_arg,
111
- tx_signal_store, verbose):
122
+ tx_signal_store, prefix_signal_struct_names,
123
+ prefix_context_struct_names, verbose):
112
124
  """
113
125
  Generate the static C API for NODE_NAME from DB_FILE (.kcd or .yaml)
114
126
  """
@@ -127,7 +139,9 @@ def api(db_file, library_name, node_name, force, encode_all_signals_func,
127
139
  except FileExistsError as e:
128
140
  click.echo(e)
129
141
  try:
130
- msgs, kcd_db = build_msgs_data_dicts_from_kcd(db_path, library_name, node_name)
142
+ msgs, kcd_db = build_msgs_data_dicts_from_kcd(
143
+ db_path, library_name, node_name,
144
+ prefix_signal_struct_names=prefix_signal_struct_names)
131
145
  except MessageProcessingException as e:
132
146
  click.echo(f"Got Error while processing message {e.can_message_name}"
133
147
  f" for {e.node_name}: {e}")
@@ -164,6 +178,7 @@ def api(db_file, library_name, node_name, force, encode_all_signals_func,
164
178
  with_rx_valid=with_rx_valid,
165
179
  msg_as_tx_callback_arg=msg_as_tx_callback_arg,
166
180
  tx_signal_store=tx_signal_store,
181
+ prefix_context_struct_names=prefix_context_struct_names,
167
182
  )
168
183
  with (header_path.parent / 'canframe.h').open('w+') as canframe_header_file:
169
184
  canframe_header_file.write(canframe_header)
@@ -10,7 +10,7 @@ import operator
10
10
  import copy
11
11
  import random
12
12
  from can_db_model.model import MuxMember
13
- from .utils import is_valid_c_identifier, converrt_to_c_identifier
13
+ from .utils import is_valid_c_identifier, converrt_to_c_identifier, to_struct_prefix
14
14
  from .utils import convert_to_cantools_signal_name
15
15
  from .utils import get_signal_type_from_range, get_bit_length_from_type
16
16
  from .types import SignalType, SignalDirection, ByteOrder, SignalProcessingException, MessageProcessingException
@@ -18,7 +18,8 @@ from .types import SignalType, SignalDirection, ByteOrder, SignalProcessingExcep
18
18
 
19
19
  class CAPIMessage:
20
20
  def __init__(self, msg, api_name: str, node_name: str,
21
- bus_name: str, require_endpoints: bool = True):
21
+ bus_name: str, require_endpoints: bool = True,
22
+ prefix_signal_struct_names: bool = False):
22
23
  """
23
24
  msg is a can_db_model.model.Message. bus_name is the database's bus
24
25
  name (the model carries the bus at the top level, not per-message).
@@ -34,6 +35,7 @@ class CAPIMessage:
34
35
  self.is_extended = msg.extended
35
36
  # prefix for the struct names
36
37
  self.api_prefix = api_name
38
+ self.prefix_signal_struct_names = prefix_signal_struct_names
37
39
  if msg.length > 8:
38
40
  raise MessageProcessingException(
39
41
  "Message too large for v2.0B spec",
@@ -115,9 +117,12 @@ class CAPIMessage:
115
117
 
116
118
  def struct_name(self):
117
119
  if ' ' not in self.name:
118
- return ''.join(map(lambda s: s.capitalize(), self.name.split('_')))
120
+ base = ''.join(map(lambda s: s.capitalize(), self.name.split('_')))
119
121
  else:
120
- return ''.join(map(lambda s: s.capitalize(), self.name.split()))
122
+ base = ''.join(map(lambda s: s.capitalize(), self.name.split()))
123
+ if self.prefix_signal_struct_names:
124
+ return to_struct_prefix(self.api_prefix) + base
125
+ return base
121
126
 
122
127
  def to_dict(self):
123
128
  return {
@@ -1,12 +1,9 @@
1
1
  {% macro declare_message_struct(message) -%}
2
- #ifndef STRUCT_{{ message.struct_name | upper }}
3
- #define STRUCT_{{ message.struct_name | upper }}
4
2
  struct {{ message.struct_name }} {
5
3
  {%- for signal in message.all_signals %}
6
4
  {{ signal.api_data_type }} {{ signal.name }};
7
5
  {%- endfor %}
8
6
  };
9
- #endif
10
7
  {%- endmacro %}
11
8
 
12
9
  {% macro generate_msg_defines(message) -%}
@@ -35,9 +35,9 @@
35
35
  {{ generate_label_defines(message) }}{{ declare_message_struct(message) }}
36
36
  {% endfor %}
37
37
  {%- if global_rx %}
38
- {{ define_rx_context_struct(true, 'uint64_t', rx_callback_context=rx_callback_context, msg_as_rx_callback_arg=msg_as_rx_callback_arg, rx_signal_store=rx_signal_store, with_rx_valid=with_rx_valid) }}
38
+ {{ define_rx_context_struct(true, 'uint64_t', rx_callback_context=rx_callback_context, msg_as_rx_callback_arg=msg_as_rx_callback_arg, rx_signal_store=rx_signal_store, with_rx_valid=with_rx_valid, ctx_name=rx_context_struct_name) }}
39
39
 
40
- {{ generate_global_rx_struct(messages, api_name_struct_prefix, double_buffered, rx_signal_store=rx_signal_store) }}
40
+ {{ generate_global_rx_struct(messages, api_name_struct_prefix, double_buffered, rx_signal_store=rx_signal_store, ctx_name=rx_context_struct_name) }}
41
41
 
42
42
  {{ declare_global_rx_frame_process_function(api_name , api_name_struct_prefix, 'CanFrame', 'uint16_t', 'uint64_t') }};
43
43
  {%- if double_buffered and rx_signal_store %}
@@ -46,15 +46,15 @@
46
46
  {%- endif %}
47
47
  {% endif %}
48
48
  {%- if periodic_tx %}
49
- {{ define_tx_context_struct(true, 'uint16_t', 'uint64_t', send_now, tx_callback_context=tx_callback_context, msg_as_tx_callback_arg=msg_as_tx_callback_arg) }}
49
+ {{ define_tx_context_struct(true, 'uint16_t', 'uint64_t', send_now, tx_callback_context=tx_callback_context, msg_as_tx_callback_arg=msg_as_tx_callback_arg, ctx_name=tx_context_struct_name) }}
50
50
 
51
- {{ generate_global_tx_struct(messages, api_name_struct_prefix, double_buffered, tx_signal_store=tx_signal_store) }}
51
+ {{ generate_global_tx_struct(messages, api_name_struct_prefix, double_buffered, tx_signal_store=tx_signal_store, ctx_name=tx_context_struct_name) }}
52
52
 
53
53
  {{ declare_periodic_tx_init_function(api_name, api_name_struct_prefix, 'uint64_t') }};
54
54
 
55
- {{ declare_get_msg_tx_context_by_id(messages, api_name, api_name_struct_prefix, 'struct MsgTxContext') }};
55
+ {{ declare_get_msg_tx_context_by_id(messages, api_name, api_name_struct_prefix, 'struct ' ~ tx_context_struct_name) }};
56
56
 
57
- {{ declare_period_update_function(api_name, 'struct MsgTxContext') }}
57
+ {{ declare_period_update_function(api_name, 'struct ' ~ tx_context_struct_name) }}
58
58
 
59
59
  {{ declare_periodic_tx_function(api_name, api_name_struct_prefix, 'CanFrame', 'uint16_t', 'uint64_t', 'uint16_t') }};
60
60
  {%- if double_buffered and tx_signal_store %}
@@ -1,9 +1,9 @@
1
1
  {% from "encoding_macros.jinja" import message_match_function_call %}
2
2
  {% from "encoding_macros.jinja" import message_decoding_function_call %}
3
- {%- macro define_rx_context_struct(rx_callback, time_type, rx_callback_context=false, msg_as_rx_callback_arg=false, rx_signal_store=false, with_rx_valid=false) -%}
4
- #ifndef MSGRXCTX
5
- #define MSGRXCTX
6
- struct MsgRxContext {
3
+ {%- macro define_rx_context_struct(rx_callback, time_type, rx_callback_context=false, msg_as_rx_callback_arg=false, rx_signal_store=false, with_rx_valid=false, ctx_name='MsgRxContext') -%}
4
+ #ifndef {{ ctx_name | upper }}
5
+ #define {{ ctx_name | upper }}
6
+ struct {{ ctx_name }} {
7
7
  uint16_t ignore;
8
8
  {%- if rx_signal_store or with_rx_valid %}
9
9
  uint16_t valid;
@@ -26,11 +26,11 @@ struct MsgRxContext {
26
26
  #endif
27
27
  {%- endmacro %}
28
28
 
29
- {% macro generate_global_rx_struct(messages, api_struct_prefix, double_buffered=False, rx_signal_store=False) -%}
29
+ {% macro generate_global_rx_struct(messages, api_struct_prefix, double_buffered=False, rx_signal_store=False, ctx_name='MsgRxContext') -%}
30
30
  struct {{ api_struct_prefix }}RxSignals {
31
31
  {%- for message in messages %}
32
32
  {%- if message.direction == 'rx' or message.direction == 'rt' %}
33
- struct MsgRxContext {{ message.name }}_context;
33
+ struct {{ ctx_name }} {{ message.name }}_context;
34
34
  {%- if rx_signal_store %}
35
35
  {%- if double_buffered %}
36
36
  struct {{ message.struct_name }} {{ message.name }}_signals_storage[2];
@@ -73,7 +73,7 @@ struct {{ api_struct_prefix }}RxSignals {{ var_name }};
73
73
  {{ api_name }}_process_received_frame({{ rx_signals_ptr_name }}, {{ can_frame_ptr_name }}, {{ time_var_name }})
74
74
  {% endmacro %}
75
75
 
76
- {% macro generate_global_rx_frame_process_function(messages, api_name, api_struct_prefix, can_frame_type, return_type, time_type, rx_callback, double_buffered=False, rx_callback_context=false, msg_as_rx_callback_arg=false, rx_signal_store=false, with_rx_valid=false) -%}
76
+ {% macro generate_global_rx_frame_process_function(messages, api_name, api_struct_prefix, can_frame_type, return_type, time_type, rx_callback, double_buffered=False, rx_callback_context=false, msg_as_rx_callback_arg=false, rx_signal_store=false, with_rx_valid=false, ctx_name='MsgRxContext') -%}
77
77
  {{ declare_global_rx_frame_process_function(api_name, api_struct_prefix, can_frame_type, return_type, time_type) }} {
78
78
  {%- for message in messages %}
79
79
  {%- if message.direction == 'rx' or message.direction == 'rt' %}
@@ -14,7 +14,7 @@
14
14
  #include <string.h>
15
15
  {%- if global_rx %}
16
16
 
17
- {{ generate_global_rx_frame_process_function(messages, api_name, api_name_struct_prefix, 'CanFrame', 'uint16_t', 'uint64_t', true, double_buffered, rx_callback_context=rx_callback_context, msg_as_rx_callback_arg=msg_as_rx_callback_arg, rx_signal_store=rx_signal_store, with_rx_valid=with_rx_valid) }}
17
+ {{ generate_global_rx_frame_process_function(messages, api_name, api_name_struct_prefix, 'CanFrame', 'uint16_t', 'uint64_t', true, double_buffered, rx_callback_context=rx_callback_context, msg_as_rx_callback_arg=msg_as_rx_callback_arg, rx_signal_store=rx_signal_store, with_rx_valid=with_rx_valid, ctx_name=rx_context_struct_name) }}
18
18
  {%- if double_buffered and rx_signal_store %}
19
19
 
20
20
  {{ define_global_rx_init_function(messages, api_name, api_name_struct_prefix) }}
@@ -22,9 +22,9 @@
22
22
  {%- endif %}
23
23
  {%- if periodic_tx %}
24
24
 
25
- {{ define_periodic_tx_function(messages, api_name, api_name_struct_prefix, 'CanFrame', 'uint16_t', 'uint64_t', 'uint16_t', true, send_now, double_buffered, tx_callback_context=tx_callback_context, msg_as_tx_callback_arg=msg_as_tx_callback_arg, tx_signal_store=tx_signal_store) }}
25
+ {{ define_periodic_tx_function(messages, api_name, api_name_struct_prefix, 'CanFrame', 'uint16_t', 'uint64_t', 'uint16_t', true, send_now, double_buffered, tx_callback_context=tx_callback_context, msg_as_tx_callback_arg=msg_as_tx_callback_arg, tx_signal_store=tx_signal_store, ctx_name=tx_context_struct_name) }}
26
26
 
27
- {{ define_get_msg_tx_context_by_id(messages, api_name, api_name_struct_prefix, 'struct MsgTxContext') }}
27
+ {{ define_get_msg_tx_context_by_id(messages, api_name, api_name_struct_prefix, 'struct ' ~ tx_context_struct_name) }}
28
28
 
29
29
  {{ define_periodic_tx_init_function(api_name, api_name_struct_prefix, 'uint64_t', messages, '1000', double_buffered, tx_signal_store=tx_signal_store) }}
30
30
  {%- if double_buffered and tx_signal_store %}
@@ -1,10 +1,10 @@
1
1
  {% from "encoding_macros.jinja" import message_encoding_function_call %}
2
2
  {% from "encoding_macros.jinja" import rotate_multiplex_func_call %}
3
3
 
4
- {% macro define_tx_context_struct(tx_callback, flag_type, time_type, send_now, tx_callback_context=false, msg_as_tx_callback_arg=false) -%}
5
- #ifndef MSGTXCTX
6
- #define MSGTXCTX
7
- struct MsgTxContext {
4
+ {% macro define_tx_context_struct(tx_callback, flag_type, time_type, send_now, tx_callback_context=false, msg_as_tx_callback_arg=false, ctx_name='MsgTxContext') -%}
5
+ #ifndef {{ ctx_name | upper }}
6
+ #define {{ ctx_name | upper }}
7
+ struct {{ ctx_name }} {
8
8
  uint32_t arb_id;
9
9
  {{ flag_type }} enable;
10
10
  {{ time_type }} last_tx;
@@ -58,11 +58,11 @@ static inline void {{ api_name }}_update_tx_period({{ context_type }} *txctx, ui
58
58
  }
59
59
  {%- endmacro %}
60
60
 
61
- {% macro generate_global_tx_struct(messages, api_struct_prefix, double_buffered=False, tx_signal_store=False) -%}
61
+ {% macro generate_global_tx_struct(messages, api_struct_prefix, double_buffered=False, tx_signal_store=False, ctx_name='MsgTxContext') -%}
62
62
  struct {{ api_struct_prefix }}TxSignals {
63
63
  {%- for message in messages %}
64
64
  {%- if message.direction == 'tx' or message.direction == 'rt' %}
65
- struct MsgTxContext {{ message.name }}_context;
65
+ struct {{ ctx_name }} {{ message.name }}_context;
66
66
  {%- if tx_signal_store %}
67
67
  {%- if double_buffered %}
68
68
  struct {{ message.struct_name }} {{ message.name }}_signals_storage[2];
@@ -93,7 +93,7 @@ void {{ api_name }}_{{ message.name }}_commit(struct {{ api_struct_prefix }}TxSi
93
93
  {{ return_type }} {{ api_name }}_periodic_tx(struct {{ api_struct_prefix }}TxSignals *tx_signals, {{ can_frame_type }} *output_frame, {{ time_type }} now)
94
94
  {%- endmacro %}
95
95
 
96
- {% macro define_periodic_tx_function(messages, api_name, api_struct_prefix, can_frame_type, return_type, time_type, flag_type, tx_callback, send_now, double_buffered=False, tx_callback_context=false, msg_as_tx_callback_arg=false, tx_signal_store=false) -%}
96
+ {% macro define_periodic_tx_function(messages, api_name, api_struct_prefix, can_frame_type, return_type, time_type, flag_type, tx_callback, send_now, double_buffered=False, tx_callback_context=false, msg_as_tx_callback_arg=false, tx_signal_store=false, ctx_name='MsgTxContext') -%}
97
97
  {{ declare_periodic_tx_function(api_name, api_struct_prefix, can_frame_type, return_type, time_type, flag_type) }} {
98
98
  {%- for message in messages %}
99
99
  {%- if message.direction == 'tx' or message.direction == 'rt' %}
@@ -24,6 +24,13 @@ def is_valid_c_identifier(s: str) -> bool:
24
24
  def converrt_to_c_identifier(input_str: str) -> str:
25
25
  return '_'.join(map(lambda s: s.lower(), input_str.split()))
26
26
 
27
+ def to_struct_prefix(api_name: str) -> str:
28
+ """CamelCase an api/library name for use as a C struct-name prefix.
29
+
30
+ Mirrors the transform already applied to <Prefix>RxSignals / TxSignals.
31
+ """
32
+ return ''.join(map(lambda s: s.capitalize(), api_name.split('_')))
33
+
27
34
  def convert_to_cantools_signal_name(c_signal_identifier: str) -> str:
28
35
  """
29
36
  Take a C identifier and adapt it to match the cantools name
@@ -622,7 +622,9 @@ def _gen_api_files(tmp_path, api_name, node="DUT", kcd="tests/test_1.kcd",
622
622
  here = Path(__file__).resolve().parent
623
623
  env = Environment(loader=FileSystemLoader(
624
624
  [str(here.parent / "src" / "can_api_generator" / "templates")]))
625
- msgs, _ = build_msgs_data_dicts_from_kcd(Path(kcd), api_name, node)
625
+ msgs, _ = build_msgs_data_dicts_from_kcd(
626
+ Path(kcd), api_name, node,
627
+ prefix_signal_struct_names=flags.get("prefix_signal_struct_names", False))
626
628
  header, source, canframe = generate_api_implementation(
627
629
  msgs, env, api_name, True,
628
630
  generate_everything=False,
@@ -635,6 +637,7 @@ def _gen_api_files(tmp_path, api_name, node="DUT", kcd="tests/test_1.kcd",
635
637
  tx_signal_store=flags.get("tx_signal_store", False),
636
638
  msg_as_tx_callback_arg=flags.get("msg_as_tx_callback_arg", False),
637
639
  tx_callback_context=flags.get("tx_callback_context", False),
640
+ prefix_context_struct_names=flags.get("prefix_context_struct_names", False),
638
641
  )
639
642
  (tmp_path / f"{api_name}.h").write_text(header)
640
643
  (tmp_path / f"{api_name}.c").write_text(source)
@@ -661,20 +664,152 @@ def test_callback_only_rx_zeroes_decode_temp(tmp_path):
661
664
  assert decl < memz < decode, "decode temp must be memset before decode"
662
665
 
663
666
 
664
- def test_message_struct_is_include_guarded(tmp_path):
665
- """Two APIs generated from one KCD with different names must be includable
666
- in one translation unit without redefining the shared message structs."""
667
- import os
668
- import subprocess
667
+ def test_signal_struct_has_no_include_guard(tmp_path):
668
+ """The per-message signal struct is emitted bare (no STRUCT_ guard)."""
669
669
  header, _ = _gen_api_files(tmp_path, "prod", global_rx=True,
670
670
  periodic_tx=True, rx_signal_store=True,
671
671
  tx_signal_store=True)
672
+ assert "struct Message1 {" in header
673
+ assert "STRUCT_MESSAGE1" not in header
674
+ assert "#ifndef STRUCT_" not in header
675
+
676
+
677
+ def test_prefix_signal_struct_names_prefixes_struct_type(tmp_path):
678
+ header, source = _gen_api_files(
679
+ tmp_path, "prod", global_rx=True, periodic_tx=True,
680
+ rx_signal_store=True, tx_signal_store=True,
681
+ prefix_signal_struct_names=True)
682
+ # struct definition and RX/TX storage members use the prefixed type
683
+ assert "struct ProdMessage1 {" in header
684
+ assert "struct ProdMessage2 {" in header
685
+ assert "struct ProdMessage2 message_2_signals;" in header
686
+ # encode/decode signatures use the prefixed type
687
+ assert "struct ProdMessage2 *msg" in source or "struct ProdMessage2 * msg" in source
688
+ # function names are NOT prefixed with the struct prefix (still api_prefix)
689
+ assert "prod_message_2_decode" in source
690
+ assert "struct Message1 {" not in header
691
+
692
+
693
+ def test_prefix_signal_struct_names_absent_by_default(tmp_path):
694
+ header, _ = _gen_api_files(
695
+ tmp_path, "prod", global_rx=True, periodic_tx=True,
696
+ rx_signal_store=True, tx_signal_store=True)
697
+ assert "struct Message1 {" in header
698
+ assert "ProdMessage1" not in header
699
+
700
+
701
+ def test_prefix_signal_struct_names_cli_flag(tmp_path):
702
+ runner = CliRunner()
703
+ lib_base = tmp_path / "prod"
704
+ result = runner.invoke(main, [
705
+ "api", "tests/test_1.kcd", str(lib_base), "DUT",
706
+ "-r", "-p", "--rx-signal-store", "--tx-signal-store",
707
+ "--prefix-signal-struct-names",
708
+ ])
709
+ assert result.exit_code == 0, result.output
710
+ header = (tmp_path / "prod.h").read_text()
711
+ # The CLI passes the (tmp) library path as api_name, so the exact prefix is
712
+ # path-derived; assert the flag applied a prefix rather than a specific one.
713
+ assert "Message1 {" in header # prefixed struct still ends in Message1
714
+ assert "struct Message1 {" not in header # ...but not the bare name
715
+
716
+
717
+ def test_prefix_context_struct_names_prefixes_both_contexts(tmp_path):
718
+ header, source = _gen_api_files(
719
+ tmp_path, "prod", global_rx=True, periodic_tx=True,
720
+ rx_signal_store=True, tx_signal_store=True,
721
+ prefix_context_struct_names=True)
722
+ # struct definitions + guard tokens follow the prefixed names
723
+ assert "struct ProdMsgRxContext {" in header
724
+ assert "struct ProdMsgTxContext {" in header
725
+ assert "#ifndef PRODMSGRXCONTEXT" in header
726
+ assert "#ifndef PRODMSGTXCONTEXT" in header
727
+ # members and signatures use the prefixed context types
728
+ assert "struct ProdMsgRxContext message_2_context;" in header
729
+ assert "struct ProdMsgTxContext message_1_context;" in header
730
+ assert "struct ProdMsgTxContext *prod_get_msg_tx_context_by_id" in header
731
+ assert "struct MsgRxContext {" not in header
732
+
733
+
734
+ def test_prefix_context_struct_names_absent_by_default(tmp_path):
735
+ header, _ = _gen_api_files(
736
+ tmp_path, "prod", global_rx=True, periodic_tx=True,
737
+ rx_signal_store=True, tx_signal_store=True)
738
+ assert "struct MsgRxContext {" in header
739
+ assert "struct MsgTxContext {" in header
740
+ assert "#ifndef MSGRXCONTEXT" in header
741
+ assert "#ifndef MSGTXCONTEXT" in header
742
+ assert "ProdMsgRxContext" not in header
743
+
744
+
745
+ def test_prefix_context_struct_names_cli_flag(tmp_path):
746
+ runner = CliRunner()
747
+ lib_base = tmp_path / "prod"
748
+ result = runner.invoke(main, [
749
+ "api", "tests/test_1.kcd", str(lib_base), "DUT",
750
+ "-r", "-p", "--rx-signal-store", "--tx-signal-store",
751
+ "--prefix-context-struct-names",
752
+ ])
753
+ assert result.exit_code == 0, result.output
754
+ header = (tmp_path / "prod.h").read_text()
755
+ # api_name is the tmp path, so assert a prefix was applied, not a specific one.
756
+ assert "MsgRxContext {" in header
757
+ assert "struct MsgRxContext {" not in header
758
+ assert "MsgTxContext {" in header
759
+ assert "struct MsgTxContext {" not in header
760
+
761
+
762
+ # These compile checks pin -std=c11: the generator targets pre-C23 / embedded
763
+ # compilers (e.g. TI C2000), where two same-tag struct definitions in one TU are
764
+ # a redefinition error. C23 (the host gcc default) legalized identical same-tag
765
+ # redefinitions, which would mask the collision the prefixes exist to prevent.
766
+ def test_two_prefixed_builds_coexist_in_one_tu(tmp_path):
767
+ """Two APIs from one KCD, different names, both prefix flags set, compile
768
+ together in one translation unit with no redefinition."""
769
+ import os
770
+ import subprocess
771
+ _gen_api_files(tmp_path, "prod", global_rx=True, periodic_tx=True,
772
+ rx_signal_store=True, tx_signal_store=True,
773
+ prefix_signal_struct_names=True,
774
+ prefix_context_struct_names=True)
775
+ _gen_api_files(tmp_path, "cons", global_rx=True, periodic_tx=True,
776
+ rx_signal_store=True, tx_signal_store=True,
777
+ prefix_signal_struct_names=True,
778
+ prefix_context_struct_names=True)
779
+ (tmp_path / "both.c").write_text(
780
+ '#include "prod.h"\n#include "cons.h"\nint main(void) { return 0; }\n')
781
+ c = subprocess.run(["gcc", "-std=c11", "-c", "-o", os.devnull, "both.c"],
782
+ cwd=tmp_path, capture_output=True, text=True)
783
+ assert c.returncode == 0, c.stderr
784
+
785
+
786
+ def test_unprefixed_signal_structs_collide_in_one_tu(tmp_path):
787
+ """Guard removal is intentional: without --prefix-signal-struct-names, two
788
+ headers that share a message name no longer coexist in one TU."""
789
+ import os
790
+ import subprocess
791
+ _gen_api_files(tmp_path, "prod", global_rx=True, periodic_tx=True,
792
+ rx_signal_store=True, tx_signal_store=True)
672
793
  _gen_api_files(tmp_path, "cons", global_rx=True, periodic_tx=True,
673
794
  rx_signal_store=True, tx_signal_store=True)
674
- assert "#ifndef STRUCT_MESSAGE1" in header
675
- assert "#define STRUCT_MESSAGE1" in header
676
795
  (tmp_path / "both.c").write_text(
677
796
  '#include "prod.h"\n#include "cons.h"\nint main(void) { return 0; }\n')
678
- c = subprocess.run(["gcc", "-c", "-o", os.devnull, "both.c"],
797
+ c = subprocess.run(["gcc", "-std=c11", "-c", "-o", os.devnull, "both.c"],
798
+ cwd=tmp_path, capture_output=True, text=True)
799
+ assert c.returncode != 0
800
+ assert "redefinition" in c.stderr and "Message1" in c.stderr
801
+
802
+
803
+ def test_prefix_flags_do_not_change_layout_callback_only(tmp_path):
804
+ """Prefix flags only rename types: a callback-only RX/TX build still
805
+ compiles and keeps the same field-access shape."""
806
+ import os
807
+ import subprocess
808
+ _gen_api_files(tmp_path, "prod", global_rx=True, periodic_tx=True,
809
+ prefix_signal_struct_names=True,
810
+ prefix_context_struct_names=True)
811
+ (tmp_path / "u.c").write_text(
812
+ '#include "prod.h"\nint main(void) { return 0; }\n')
813
+ c = subprocess.run(["gcc", "-std=c11", "-c", "-o", os.devnull, "u.c"],
679
814
  cwd=tmp_path, capture_output=True, text=True)
680
815
  assert c.returncode == 0, c.stderr
@@ -0,0 +1,15 @@
1
+ from src.can_api_generator.utils import to_struct_prefix
2
+
3
+
4
+ def test_to_struct_prefix_camelcases_underscore_parts():
5
+ assert to_struct_prefix("battery_v2") == "BatteryV2"
6
+
7
+
8
+ def test_to_struct_prefix_single_token():
9
+ assert to_struct_prefix("prod") == "Prod"
10
+
11
+
12
+ def test_to_struct_prefix_matches_legacy_inline_transform():
13
+ api_name = "dut_can_a"
14
+ legacy = "".join(map(lambda s: s.capitalize(), api_name.split("_")))
15
+ assert to_struct_prefix(api_name) == legacy