diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 532934d..ce57b0f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -74,7 +74,7 @@ jobs:
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- token: ${{ secrets.GITHUB_TOKEN }}
+ token: ${{ secrets.LINTERS_PAT }}
- name: Install clang-format
run: |
@@ -150,6 +150,7 @@ jobs:
call "%VSDIR%\VC\Auxiliary\Build\vcvars64.bat"
cl /std:c11 /W4 /WX /O2 /Isrc /Fe:build\J2735_run_tests.exe ^
tests\J2735_internal_DE_VehicleEventFlags_test.c ^
+ tests\J2735_internal_DF_ApproachOrLane_test.c ^
tests\J2735_internal_DF_BSMcoreData_test.c ^
tests\J2735_internal_DF_IntersectionReferenceID_test.c ^
tests\J2735_internal_DF_PathPrediction_test.c ^
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index 92eaec4..e8b7e8d 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -90,7 +90,7 @@ jobs:
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- token: ${{ secrets.GITHUB_TOKEN }}
+ token: ${{ secrets.LINTERS_PAT }}
- name: Set up Python
uses: actions/setup-python@v5
@@ -216,6 +216,10 @@ jobs:
# Build report
lines = []
lines.append(f"## Coverage: {total_pct:.0f}%\n")
+ lines.append("")
+ lines.append("")
+ lines.append(f"Per-file coverage ({len(modules)} modules)
")
+ lines.append("")
lines.append("| Module | Cover |")
lines.append("|--------|-------|")
@@ -228,6 +232,8 @@ jobs:
icon = "❌"
lines.append(f"| {name} | {pct:.0f}% {icon} |")
+ lines.append("")
+ lines.append(" ")
lines.append("")
lines.append("")
lines.append(f"Details ({total_stmts} statements, {total_miss} missed)
")
diff --git a/src/J2735_internal_DF_ApproachOrLane.h b/src/J2735_internal_DF_ApproachOrLane.h
new file mode 100644
index 0000000..66e0d60
--- /dev/null
+++ b/src/J2735_internal_DF_ApproachOrLane.h
@@ -0,0 +1,225 @@
+/**
+ * Copyright 2026 Yogev Neumann
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * SPDX-FileCopyrightText: 2026 Yogev Neumann
+ */
+/**
+ * @file
+ * @author Yogev Neumann
+ * @brief J2735 ApproachOrLane Definition and Access Macros.
+ *
+ * @par ApproachOrLane Wire Format (UPER):
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits (J2735_BW_APPROACH_ID)
+ * lane LaneID -- 8 bits (J2735_BW_LANE_ID)
+ * }
+ * @endcode
+ *
+ * This is a non-extensible CHOICE with 2 alternatives.
+ * Per ITU-T X.691 §23, the choice index uses ceil(log2(2)) = 1 bit.
+ *
+ * @par Wire Format (approach selected, 5 bits total):
+ * @code
+ * ┌───────────┬───────────────────────────┐
+ * │ Bit 0 │ Bits 1-4 │
+ * ├───────────┼───────────────────────────┤
+ * │ Index = 0 │ ApproachID value (4 bits) │
+ * └───────────┴───────────────────────────┘
+ * @endcode
+ *
+ * @par Wire Format (lane selected, 9 bits total):
+ * @code
+ * ┌───────────┬───────────────────────┐
+ * │ Bit 0 │ Bits 1-8 │
+ * ├───────────┼───────────────────────┤
+ * │ Index = 1 │ LaneID value (8 bits) │
+ * └───────────┴───────────────────────┘
+ * @endcode
+ *
+ * Performance Rationale (Single I/O Pattern):
+ * Reading 9 bits unconditionally (even when smaller alternatives use less) is faster than
+ * reading the index first, then conditionally reading the value bits:
+ * - Single I/O: ~9-10 instructions, 1 memory load, 0 branches
+ * - Two-Step: ~13-14 instructions, 2 memory loads, 1 branch (misprediction risk)
+ * The "garbage" bits (when smaller alternative selected) cost nothing - already in register.
+ * This pattern applies when max_variant ≤ 56 bits (J2735_READ_BITS limit).
+ *
+ * Usage Pattern (single I/O, O(1)):
+ * @code
+ * uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(buf); [9 bits in uint16_t]
+ *
+ * switch (J2735_APPROACH_OR_LANE_WHICH(raw9)) {
+ * case J2735_CHOICE_APPROACH_OR_LANE_APPROACH:
+ * use(J2735_APPROACH_OR_LANE_GET_APPROACH(raw9)); [uint8_t: 4 bits]
+ * break;
+ * case J2735_CHOICE_APPROACH_OR_LANE_LANE:
+ * use(J2735_APPROACH_OR_LANE_GET_LANE(raw9)); [uint8_t: 8 bits]
+ * break;
+ * }
+ *
+ * buf += J2735_BIT_TO_BYTE(J2735_APPROACH_OR_LANE_SIZE(raw9)); [uint8_t: 5 or 9]
+ * @endcode
+ */
+#ifndef J2735_INTERNAL_DF_APPROACHORLANE_H
+#define J2735_INTERNAL_DF_APPROACHORLANE_H
+
+#include "J2735_internal_common.h"
+#include "J2735_internal_constants.h"
+
+/* ============================================================================================== */
+/* INTERNAL: CHOICE Index Bits */
+/* ============================================================================================== */
+/**
+ * @internal
+ * @brief Number of bits for ApproachOrLane choice index.
+ *
+ * ApproachOrLane has 2 alternatives, so index = ceil(log2(2)) = 1 bit.
+ * This is a non-extensible CHOICE.
+ */
+#define J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE 1U
+
+/* ============================================================================================== */
+/* INTERNAL: Structure Metadata */
+/* ============================================================================================== */
+/**
+ * @internal
+ * @brief Maximum wire size in bits for ApproachOrLane encoding.
+ *
+ * This is the larger of the alternatives: 1 (index) + 8 (lane) = 9 bits.
+ *
+ * We always read MAX bits (9) unconditionally rather than reading the index first
+ * then conditionally reading the value bits. Assembly-level analysis:
+ *
+ * Single I/O (read max): ~9-10 instructions, 1 memory load, 0 branches
+ * Two-Step (conditional): ~13-14 instructions, 2 memory loads, 1 branch
+ *
+ * The "garbage" bits when a smaller alternative is selected cost nothing - they're already
+ * loaded into the register and simply get shifted/masked away.
+ */
+#define J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE \
+ (J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + J2735_BW_LANE_ID)
+
+/* ============================================================================================== */
+/* PUBLIC API: Alternative Indices */
+/* ============================================================================================== */
+/**
+ * @brief Alternative index for 'approach' in ApproachOrLane CHOICE.
+ */
+#define J2735_CHOICE_APPROACH_OR_LANE_APPROACH 0U
+
+/**
+ * @brief Alternative index for 'lane' in ApproachOrLane CHOICE.
+ */
+#define J2735_CHOICE_APPROACH_OR_LANE_LANE 1U
+
+/* ============================================================================================== */
+/* PUBLIC API: Raw Read (Single I/O) */
+/* ============================================================================================== */
+/**
+ * @brief Read raw 9 bits for ApproachOrLane encoding (single I/O operation).
+ *
+ * Reads 9 bits (maximum wire size) into a uint16_t. All other ApproachOrLane
+ * macros operate on this pre-read 9-bit value for O(1) performance.
+ *
+ * Raw value bit layout (9 bits in uint16_t, right-justified):
+ * @code
+ * uint16_t bit: 8..0
+ * [I][Value bits...]
+ *
+ * Where:
+ * I = CHOICE index
+ * Value bits = Alternative-specific data
+ * @endcode
+ *
+ * @param[in] buf Pointer to the ApproachOrLane encoding (const uint8_t*).
+ * @return uint16_t containing the raw 9-bit encoding.
+ * @warning J2735_READ_BITS loads 8 bytes for efficient bit extraction. Caller must ensure
+ * buffer has at least 8 bytes of readable memory (2 data + 6 padding, or more).
+ *
+ * @note Store result in `uint16_t raw9` variable, then pass to other macros.
+ */
+#define J2735_APPROACH_OR_LANE_RAW_READ(buf) \
+ ((uint16_t)J2735_READ_BITS((buf), 0U, J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE))
+
+/* ============================================================================================== */
+/* PUBLIC API: Which-Checker (Pure Computation on Raw Value) */
+/* ============================================================================================== */
+/**
+ * @brief Get the CHOICE alternative index (1 bit) from pre-read raw value.
+ *
+ * Extracts the index bits from the MSB position of the raw value.
+ *
+ * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ().
+ * @return uint8_t: Alternative index (0-1).
+ * Use J2735_CHOICE_APPROACH_OR_LANE_* constants for comparison in switch statements.
+ */
+#define J2735_APPROACH_OR_LANE_WHICH(raw9) \
+ ((uint8_t)((((uint32_t)(raw9)) >> (J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE - 1U)) & \
+ ((1U << J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE) - 1U)))
+
+/* ============================================================================================== */
+/* PUBLIC API: Getters (Pure Computation on Raw Value) */
+/* ============================================================================================== */
+/**
+ * @brief Get 'approach' value (ApproachID, 4 bits) from pre-read raw value.
+ *
+ * Extracts the 4-bit value after shifting right by 4 bits.
+ *
+ * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ().
+ * @return uint8_t: ApproachID value (4 bits).
+ * @pre J2735_APPROACH_OR_LANE_WHICH(raw9) == J2735_CHOICE_APPROACH_OR_LANE_APPROACH.
+ */
+#define J2735_APPROACH_OR_LANE_GET_APPROACH(raw9) \
+ ((uint8_t)((((uint32_t)(raw9)) >> \
+ (J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE - 1U - J2735_BW_APPROACH_ID)) & \
+ ((1UL << J2735_BW_APPROACH_ID) - 1UL)))
+
+/**
+ * @brief Get 'lane' value (LaneID, 8 bits) from pre-read raw value.
+ *
+ * Extracts the 8-bit value from the lowest bits (no shift needed).
+ *
+ * Generic pattern: ((raw >> (MAX - INDEX_BITS - BW)) & MASK)
+ * Because: shift = 9 - 1 - 8 = 0, we omit the shift (MISRA 12.2 compliant).
+ *
+ * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ().
+ * @return uint8_t: LaneID value (8 bits).
+ * @pre J2735_APPROACH_OR_LANE_WHICH(raw9) == J2735_CHOICE_APPROACH_OR_LANE_LANE.
+ */
+#define J2735_APPROACH_OR_LANE_GET_LANE(raw9) \
+ ((uint8_t)((raw9) & ((1UL << J2735_BW_LANE_ID) - 1UL)))
+
+/* ============================================================================================== */
+/* PUBLIC API: Size Calculation (Pure Computation on Raw Value) */
+/* ============================================================================================== */
+/**
+ * @brief Calculate the total wire size in bits from pre-read raw value.
+ *
+ * The size depends on which alternative is selected:
+ * - approach: 1 (index) + 4 (value) = 5 bits
+ * - lane: 1 (index) + 8 (value) = 9 bits
+ *
+ * @param[in] raw9 9-bit value previously returned by J2735_APPROACH_OR_LANE_RAW_READ().
+ * @return uint8_t: Total encoding size in bits (5 or 9).
+ */
+#define J2735_APPROACH_OR_LANE_SIZE(raw9) \
+ ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + \
+ ((J2735_APPROACH_OR_LANE_WHICH(raw9) == J2735_CHOICE_APPROACH_OR_LANE_APPROACH) \
+ ? (J2735_BW_APPROACH_ID) \
+ : (J2735_BW_LANE_ID))))
+
+#endif /* J2735_INTERNAL_DF_APPROACHORLANE_H */
diff --git a/tests/J2735_internal_DF_ApproachOrLane_test.c b/tests/J2735_internal_DF_ApproachOrLane_test.c
new file mode 100644
index 0000000..9daee20
--- /dev/null
+++ b/tests/J2735_internal_DF_ApproachOrLane_test.c
@@ -0,0 +1,432 @@
+/**
+ * Copyright 2026 Yogev Neumann
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * SPDX-FileCopyrightText: 2026 Yogev Neumann
+ */
+/**
+ * @file
+ * @author Yogev Neumann
+ * @brief Sanity tests for ApproachOrLane CHOICE type.
+ *
+ * ApproachOrLane is a non-extensible CHOICE with 2 alternatives.
+ * This validates CHOICE index reading and alternative value extraction.
+ *
+ * All tests use the efficient single-I/O pattern:
+ * @code
+ * uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(buf);
+ * switch (J2735_APPROACH_OR_LANE_WHICH(raw9)) { ... }
+ * @endcode
+ */
+
+#include
+#include
+#include
+
+#include "unity.h"
+#include "unity_internals.h"
+
+#include "J2735_internal_DF_ApproachOrLane.h"
+#include "J2735_internal_DF_ApproachOrLane_test.h"
+
+/* ============================================================================================== */
+/* Happy Path Tests */
+/* ============================================================================================== */
+
+/**
+ * @brief Test ApproachOrLane with 'approach' alternative selected (typical value).
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: approach (index = 0)
+ * - Value: 5 (0x05) - typical mid-range value
+ *
+ * @par Wire Format (5 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|----------|-------|
+ * | 0 | 1 | index | 0 |
+ * | 1 | 4 | approach | 0101 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|----------------------------------|
+ * | 0 | 0x28 | 00101000 | index(0) + approach(0101) + pad |
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_approach_typical(void) {
+ static const uint8_t payload[] = {
+ 0x28, /* index(0) + approach(0101) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* Single I/O read (9 bits) - all subsequent operations are pure computation */
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload);
+
+ /* Verify WHICH returns the correct alternative index (1 bit) */
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which,
+ "WHICH should return 0 for approach");
+
+ /* Verify GET_APPROACH returns the correct value (4 bits) */
+ uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, approach_value, "approach value should be 5");
+
+ /* Verify SIZE calculation (returns 5 or 9) */
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits (1 index + 4 value)");
+}
+
+/**
+ * @brief Test ApproachOrLane with 'lane' alternative selected (typical value).
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: lane (index = 1)
+ * - Value: 171 (0xAB) - typical mid-range value with alternating bits
+ *
+ * @par Wire Format (9 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|-------|----------|
+ * | 0 | 1 | index | 1 |
+ * | 1 | 8 | lane | 10101011 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|--------------------------------|
+ * | 0 | 0xD5 | 11010101 | index(1) + lane[7:1](1010101) |
+ * | 1 | 0x80 | 10000000 | lane[0](1) + padding(0000000) |
+ *
+ * Note: The lane value is 0xAB = 10101011b. Under UPER bit-packing, the MSB 7 bits
+ * (1010101) are placed after the index bit in byte 0, while the LSB (1) becomes the
+ * MSB of byte 1. This split explains why lane[7:1] are in byte 0 and lane[0] is in byte 1.
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_lane_typical(void) {
+ static const uint8_t payload[] = {
+ 0xD5, /* index(1) + lane[7:1](1010101) */
+ 0x80, /* lane[0](1) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* Single I/O read (9 bits) - all subsequent operations are pure computation */
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload);
+
+ /* Verify WHICH returns the correct alternative index (1 bit) */
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_LANE, which,
+ "WHICH should return 1 for lane");
+
+ /* Verify GET_LANE returns the correct value (8 bits) */
+ uint8_t const lane_value = J2735_APPROACH_OR_LANE_GET_LANE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(0xABU, lane_value, "lane value should be 0xAB");
+
+ /* Verify SIZE calculation (returns 5 or 9) */
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(9U, size, "size should be 9 bits (1 index + 8 value)");
+}
+
+/* ============================================================================================== */
+/* Boundary Value Tests - Approach */
+/* ============================================================================================== */
+
+/**
+ * @brief Test ApproachOrLane boundary: approach with minimum value 0.
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: approach (index = 0)
+ * - Value: 0 (minimum) - per J2735 spec, 0 means "unknown"
+ *
+ * @par Wire Format (5 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|----------|-------|
+ * | 0 | 1 | index | 0 |
+ * | 1 | 4 | approach | 0000 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|----------------------------------|
+ * | 0 | 0x00 | 00000000 | index(0) + approach(0000) + pad |
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_approach_boundary_min(void) {
+ static const uint8_t payload[] = {
+ 0x00, /* index(0) + approach(0000) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload);
+
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which,
+ "WHICH should return 0 for approach");
+
+ uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(0U, approach_value, "approach value should be 0 (minimum)");
+
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits for approach");
+}
+
+/**
+ * @brief Test ApproachOrLane boundary: approach with maximum value 15.
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: approach (index = 0)
+ * - Value: 15 (0x0F) - maximum for 4-bit field
+ *
+ * @par Wire Format (5 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|----------|-------|
+ * | 0 | 1 | index | 0 |
+ * | 1 | 4 | approach | 1111 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|----------------------------------|
+ * | 0 | 0x78 | 01111000 | index(0) + approach(1111) + pad |
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_approach_boundary_max(void) {
+ static const uint8_t payload[] = {
+ 0x78, /* index(0) + approach(1111) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload);
+
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which,
+ "WHICH should return 0 for approach");
+
+ uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(15U, approach_value, "approach value should be 15 (maximum)");
+
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits for approach");
+}
+
+/* ============================================================================================== */
+/* Boundary Value Tests - Lane */
+/* ============================================================================================== */
+
+/**
+ * @brief Test ApproachOrLane boundary: lane with minimum value 0.
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: lane (index = 1)
+ * - Value: 0 (minimum) - per J2735 spec, 0 means "unavailable/unknown"
+ *
+ * @par Wire Format (9 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|-------|----------|
+ * | 0 | 1 | index | 1 |
+ * | 1 | 8 | lane | 00000000 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|--------------------------------|
+ * | 0 | 0x80 | 10000000 | index(1) + lane[7:1](0000000) |
+ * | 1 | 0x00 | 00000000 | lane[0](0) + padding(0000000) |
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_lane_boundary_min(void) {
+ static const uint8_t payload[] = {
+ 0x80, /* index(1) + lane[7:1](0000000) */
+ 0x00, /* lane[0](0) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload);
+
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_LANE, which,
+ "WHICH should return 1 for lane");
+
+ uint8_t const lane_value = J2735_APPROACH_OR_LANE_GET_LANE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(0U, lane_value, "lane value should be 0 (minimum)");
+
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(9U, size, "size should be 9 bits for lane");
+}
+
+/**
+ * @brief Test ApproachOrLane boundary: lane with maximum value 255.
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: lane (index = 1)
+ * - Value: 255 (0xFF) - maximum for 8-bit field (reserved per spec)
+ *
+ * @par Wire Format (9 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|-------|----------|
+ * | 0 | 1 | index | 1 |
+ * | 1 | 8 | lane | 11111111 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|--------------------------------|
+ * | 0 | 0xFF | 11111111 | index(1) + lane[7:1](1111111) |
+ * | 1 | 0x80 | 10000000 | lane[0](1) + padding(0000000) |
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_lane_boundary_max(void) {
+ static const uint8_t payload[] = {
+ 0xFF, /* index(1) + lane[7:1](1111111) */
+ 0x80, /* lane[0](1) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(payload);
+
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_LANE, which,
+ "WHICH should return 1 for lane");
+
+ uint8_t const lane_value = J2735_APPROACH_OR_LANE_GET_LANE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(255U, lane_value, "lane value should be 255 (maximum)");
+
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(9U, size, "size should be 9 bits for lane");
+}
+
+/* ============================================================================================== */
+/* Misalignment Tests */
+/* ============================================================================================== */
+
+/**
+ * @brief Test ApproachOrLane with misaligned buffer access (approach alternative).
+ *
+ * Since this is an embedded library, we must verify correct operation when
+ * the buffer is not aligned to a natural boundary. This tests the packed-cast
+ * optimization used by J2735_READ_BITS.
+ *
+ * @par ASN.1 Definition:
+ * @code
+ * ApproachOrLane ::= CHOICE {
+ * approach ApproachID, -- 4 bits, INTEGER (0..15)
+ * lane LaneID -- 8 bits, INTEGER (0..255)
+ * }
+ * @endcode
+ *
+ * @par Test Vector:
+ * - Alternative: approach (index = 0)
+ * - Value: 10 (0x0A)
+ *
+ * @par Wire Format (5 bits total):
+ * | Offset (bits) | Width | Field | Value |
+ * |---------------|-------|----------|-------|
+ * | 0 | 1 | index | 0 |
+ * | 1 | 4 | approach | 1010 |
+ *
+ * @par Byte Encoding:
+ * | Byte | Hex | Binary | Fields |
+ * |------|------|----------|----------------------------------|
+ * | 0 | 0x50 | 01010000 | index(0) + approach(1010) + pad |
+ */
+/* cppcheck-suppress misra-c2012-8.7 ; Unity RUN_TEST requires external linkage */
+void test_approach_or_lane_misaligned_access(void) {
+ /* Deliberately misalign buffer by placing padding byte at start */
+ static const uint8_t payload[] = {
+ 0xFF, /* padding byte to force misalignment */
+ 0x50, /* index(0) + approach(1010) + padding */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* safety padding */
+ };
+
+ /* Offset pointer by 1 byte to force misalignment */
+ const uint8_t *unaligned_ptr = &payload[1];
+
+ /* cppcheck-suppress misra-c2012-11.3 ; Zero-copy macro uses packed-cast */
+ uint16_t const raw9 = J2735_APPROACH_OR_LANE_RAW_READ(unaligned_ptr);
+
+ uint8_t const which = J2735_APPROACH_OR_LANE_WHICH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(J2735_CHOICE_APPROACH_OR_LANE_APPROACH, which,
+ "WHICH should return 0 for approach (misaligned)");
+
+ uint8_t const approach_value = J2735_APPROACH_OR_LANE_GET_APPROACH(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(10U, approach_value,
+ "approach value should be 10 (misaligned access)");
+
+ uint8_t const size = J2735_APPROACH_OR_LANE_SIZE(raw9);
+ TEST_ASSERT_EQUAL_UINT8_MESSAGE(5U, size, "size should be 5 bits for approach (misaligned)");
+}
+
+void run_testsuite_approach_or_lane(void) {
+ /* Happy path tests */
+ RUN_TEST(test_approach_or_lane_approach_typical);
+ RUN_TEST(test_approach_or_lane_lane_typical);
+
+ /* Boundary value tests - approach */
+ RUN_TEST(test_approach_or_lane_approach_boundary_min);
+ RUN_TEST(test_approach_or_lane_approach_boundary_max);
+
+ /* Boundary value tests - lane */
+ RUN_TEST(test_approach_or_lane_lane_boundary_min);
+ RUN_TEST(test_approach_or_lane_lane_boundary_max);
+
+ /* Misalignment tests */
+ RUN_TEST(test_approach_or_lane_misaligned_access);
+}
diff --git a/tests/J2735_internal_DF_ApproachOrLane_test.h b/tests/J2735_internal_DF_ApproachOrLane_test.h
new file mode 100644
index 0000000..b8bfc91
--- /dev/null
+++ b/tests/J2735_internal_DF_ApproachOrLane_test.h
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2026 Yogev Neumann
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * SPDX-FileCopyrightText: 2026 Yogev Neumann
+ */
+/**
+ * @file
+ * @author Yogev Neumann
+ * @brief Sanity tests for ApproachOrLane CHOICE type.
+ *
+ * ApproachOrLane is a non-extensible CHOICE with 2 alternatives.
+ * This validates CHOICE index reading and alternative value extraction.
+ */
+
+#ifndef J2735_INTERNAL_DF_APPROACHORLANE_TEST_H
+#define J2735_INTERNAL_DF_APPROACHORLANE_TEST_H
+
+/* Happy path tests */
+void test_approach_or_lane_approach_typical(void);
+void test_approach_or_lane_lane_typical(void);
+
+/* Boundary value tests - approach */
+void test_approach_or_lane_approach_boundary_min(void);
+void test_approach_or_lane_approach_boundary_max(void);
+
+/* Boundary value tests - lane */
+void test_approach_or_lane_lane_boundary_min(void);
+void test_approach_or_lane_lane_boundary_max(void);
+
+/* Misalignment tests */
+void test_approach_or_lane_misaligned_access(void);
+
+void run_testsuite_approach_or_lane(void);
+
+#endif /* J2735_INTERNAL_DF_APPROACHORLANE_TEST_H */
diff --git a/tests/J2735_run_tests.c b/tests/J2735_run_tests.c
index e5d0f3d..d04da7d 100644
--- a/tests/J2735_run_tests.c
+++ b/tests/J2735_run_tests.c
@@ -30,6 +30,7 @@
#include "J2735_UPER_test.h"
#include "J2735_internal_DE_VehicleEventFlags_test.h"
+#include "J2735_internal_DF_ApproachOrLane_test.h"
#include "J2735_internal_DF_BSMcoreData_test.h"
#include "J2735_internal_DF_IntersectionReferenceID_test.h"
#include "J2735_internal_DF_PathPrediction_test.h"
@@ -42,6 +43,7 @@ void tearDown(void) {}
int main(void) {
UNITY_BEGIN();
+ run_testsuite_approach_or_lane();
run_testsuite_bsm_core_data();
run_testsuite_intersection_reference_id();
run_testsuite_path_prediction();
diff --git a/tools/j2735_c_generator_data_frame.py b/tools/j2735_c_generator_data_frame.py
index a3f3dac..fa1fd2e 100644
--- a/tools/j2735_c_generator_data_frame.py
+++ b/tools/j2735_c_generator_data_frame.py
@@ -17,7 +17,7 @@
"""
J2735 Data Frame C Code Generator.
-Generates zero-copy C header files for J2735 Data Frame types (SEQUENCE).
+Generates zero-copy C header files for J2735 Data Frame types (SEQUENCE and CHOICE).
Both type classes use the DF_ prefix in J2735 as they represent composite structures.
Example usage:
@@ -28,14 +28,24 @@
# SEQUENCE types
code = generate_data_frame("BSMcoreData", spec)
+
+ # CHOICE types
+ code = generate_data_frame("ApproachOrLane", spec)
"""
from .j2735_c_generator_jinja import (
create_jinja_env,
get_template,
)
-from .j2735_c_generator_wire_format import get_sequence_variants
-from .j2735_spec_constraints import SequenceType
+from .j2735_c_generator_wire_format import (
+ ChoiceAlternativeDict,
+ get_choice_variants,
+ get_sequence_variants,
+)
+from .j2735_spec_constraints import (
+ ChoiceType,
+ SequenceType,
+)
from .j2735_spec_parser import (
ASN1TypeClass,
ASN1TypeDefinition,
@@ -43,6 +53,7 @@
)
_SEQUENCE_TEMPLATE_NAME = "assemble_df_sequence.j2"
+_CHOICE_TEMPLATE_NAME = "assemble_df_choice.j2"
# Maximum wire bits for single I/O pattern (J2735_READ_BITS limit at worst alignment)
_MAX_SINGLE_IO_BITS = 57
@@ -56,6 +67,7 @@ def generate_data_frame(type_name: str, spec: J2735Specification) -> str:
Supported types:
- SEQUENCE: Struct container with field access macros.
+ - CHOICE: Single I/O macros for optimal performance.
Args:
type_name: Name of the type (e.g., "BSMcoreData", "ApproachOrLane").
@@ -67,6 +79,7 @@ def generate_data_frame(type_name: str, spec: J2735Specification) -> str:
Raises:
ValueError: If type_name is not found.
ValueError: If type is not a supported Data Frame type.
+ ValueError: For CHOICE: if extensible or max_wire_bits > 57.
"""
typedef = spec.lookup_type(type_name)
if typedef is None:
@@ -74,9 +87,11 @@ def generate_data_frame(type_name: str, spec: J2735Specification) -> str:
if typedef.type_class == ASN1TypeClass.SEQUENCE:
return _generate_sequence(typedef)
+ if typedef.type_class == ASN1TypeClass.CHOICE:
+ return _generate_choice(typedef, spec)
raise ValueError(
f"Type '{type_name}' is {typedef.type_class.name}, which is not a supported "
- f"Data Frame type. Supported types: SEQUENCE"
+ f"Data Frame type. Supported types: SEQUENCE, CHOICE"
)
@@ -112,13 +127,99 @@ def _generate_sequence(typedef: ASN1TypeDefinition) -> str:
f"Expected SequenceType for SEQUENCE type, " f"got {type(sequence).__name__}"
)
- # Wire format variants for documentation tables
- variants = get_sequence_variants(sequence)
-
template = get_template(create_jinja_env(), _SEQUENCE_TEMPLATE_NAME)
return template.render(
typedef=typedef,
- variants=variants,
+ sequence_variants=get_sequence_variants(sequence),
opt_count=sequence.optional_count,
)
+
+
+def _generate_choice(typedef: ASN1TypeDefinition, spec: J2735Specification) -> str:
+ """Generate C code for a CHOICE type.
+
+ Internal helper for generate_data_frame().
+
+ Args:
+ typedef: The ASN.1 type definition for the CHOICE.
+ spec: The parsed J2735 specification (for resolving alternative types).
+
+ Returns:
+ Complete C header file content as a string.
+
+ Raises:
+ TypeError: If constraint is not ChoiceType.
+
+ Examples:
+ >>> from tools.tests.conftest import SPEC_FILE_PATH
+ >>> from tools.j2735_spec_parser import parse_spec_file
+ >>> spec = parse_spec_file(SPEC_FILE_PATH)
+ >>> typedef = spec.lookup_type("ApproachOrLane")
+ >>> code = _generate_choice(typedef, spec)
+ >>> "J2735_APPROACH_OR_LANE_RAW_READ" in code
+ True
+ >>> "J2735_APPROACH_OR_LANE_WHICH" in code
+ True
+ """
+ # Type narrowing for mypy - validate constraint type
+ choice = typedef.constraint
+ if not isinstance(choice, ChoiceType):
+ raise TypeError(f"Expected ChoiceType for CHOICE type, " f"got {type(choice).__name__}")
+
+ # Get index_bits from ChoiceType.uper_bit_width
+ index_bits = choice.uper_bit_width
+ if index_bits is None:
+ raise ValueError(
+ f"Extensible CHOICE type '{typedef.name}' is not yet supported. "
+ "Phase 3b will add extensible CHOICE support."
+ )
+
+ # Resolve each alternative's bit-width by looking up the type reference
+ # Build list of dicts with resolved info for template
+ # This MUST be done in Python because it requires spec lookups
+ alternatives: list[ChoiceAlternativeDict] = []
+ max_alt_bits = 0
+ for idx, (alt_name, type_ref) in enumerate(choice.alternatives.items()):
+ alt_typedef = spec.lookup_type(type_ref)
+ if alt_typedef is None:
+ raise ValueError(f"Alternative '{alt_name}' references unknown type '{type_ref}'")
+ if alt_typedef.constraint is None:
+ raise ValueError(f"Alternative '{alt_name}' type '{type_ref}' has no constraint")
+ alt_bit_width = alt_typedef.constraint.uper_bit_width
+ if alt_bit_width is None:
+ raise ValueError(f"Alternative '{alt_name}' type '{type_ref}' has variable bit-width")
+
+ alternatives.append(
+ {
+ "name": alt_name,
+ "type_ref": type_ref,
+ "bit_width": alt_bit_width,
+ "index": idx,
+ "shift": 0,
+ "needs_shift": False,
+ }
+ )
+ max_alt_bits = max(max_alt_bits, alt_bit_width)
+
+ # Compute final max_wire_bits and derive each alternative's shift
+ max_wire_bits = index_bits + max_alt_bits
+ for alt in alternatives:
+ alt["shift"] = max_wire_bits - index_bits - alt["bit_width"]
+ alt["needs_shift"] = alt["shift"] > 0
+
+ # Validate single I/O pattern is possible
+ if max_wire_bits > _MAX_SINGLE_IO_BITS:
+ raise ValueError(
+ f"CHOICE type '{typedef.name}' has max_wire_bits={max_wire_bits}, "
+ f"which exceeds single I/O limit of {_MAX_SINGLE_IO_BITS} bits. "
+ "Two-step pattern required (not yet implemented)."
+ )
+
+ template = get_template(create_jinja_env(), _CHOICE_TEMPLATE_NAME)
+
+ return template.render(
+ typedef=typedef,
+ alternatives=alternatives,
+ choice_variants=get_choice_variants(alternatives, index_bits),
+ )
diff --git a/tools/j2735_c_generator_wire_format.py b/tools/j2735_c_generator_wire_format.py
index 2e6b794..e82fb2c 100644
--- a/tools/j2735_c_generator_wire_format.py
+++ b/tools/j2735_c_generator_wire_format.py
@@ -21,18 +21,20 @@
All complex formatting logic lives in Jinja templates + filters.
"""
+from collections.abc import Sequence
from dataclasses import dataclass
+from typing import TypedDict
from .j2735_spec_constraints import SequenceField, SequenceType
# =============================================================================
-# Wire Variant Helper (the ONE thing that needs Python)
+# SEQUENCE Wire Format
# =============================================================================
@dataclass(frozen=True, slots=True)
-class WireVariant:
- """A wire format variant for rendering.
+class SequenceWireVariant:
+ """A wire format variant for a SEQUENCE type.
For fixed types: single variant with all fields.
For OPTIONAL types: variant with fields to include.
@@ -82,14 +84,14 @@ def _sum_field_bits(fields: tuple[SequenceField, ...]) -> int:
return sum(f.type.uper_bit_width or 0 for f in fields)
-def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]:
+def get_sequence_variants(constraint: SequenceType) -> list[SequenceWireVariant]:
"""Generate wire format variants for a SEQUENCE.
Args:
constraint: The SequenceType constraint.
Returns:
- List of WireVariant objects to render.
+ List of SequenceWireVariant objects to render.
Examples:
>>> from tools.j2735_spec_constraints import SequenceType, SequenceField
@@ -117,7 +119,7 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]:
if not is_ext and opt_count == 0:
total = _sum_field_bits(all_fields)
return [
- WireVariant(
+ SequenceWireVariant(
name=_pluralize_bits(total),
fields=all_fields,
ext_bit=None,
@@ -130,14 +132,14 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]:
if is_ext and opt_count == 0:
total_no_ext = 1 + _sum_field_bits(all_fields) # 1 for ext bit
return [
- WireVariant(
+ SequenceWireVariant(
name=f"no extensions, {_pluralize_bits(total_no_ext)}",
fields=all_fields,
ext_bit=0,
opt_bitmap="",
total_bits=total_no_ext,
),
- WireVariant(
+ SequenceWireVariant(
name="with extensions, variable",
fields=all_fields,
ext_bit=1,
@@ -164,14 +166,14 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]:
)
return [
- WireVariant(
+ SequenceWireVariant(
name=f"{absent_name}, {_pluralize_bits(absent_bits)}",
fields=required_fields,
ext_bit=0 if is_ext else None,
opt_bitmap=absent_opt,
total_bits=absent_bits,
),
- WireVariant(
+ SequenceWireVariant(
name=f"{present_name}, {_pluralize_bits(present_bits)}",
fields=all_fields,
ext_bit=0 if is_ext else None,
@@ -179,3 +181,84 @@ def get_sequence_variants(constraint: SequenceType) -> list[WireVariant]:
total_bits=present_bits,
),
]
+
+
+# =============================================================================
+# CHOICE Wire Format
+# =============================================================================
+
+
+class ChoiceAlternativeDict(TypedDict):
+ """Type definition for alternative entries in CHOICE generation."""
+
+ name: str
+ type_ref: str
+ bit_width: int
+ index: int
+ shift: int
+ needs_shift: bool
+
+
+@dataclass(frozen=True, slots=True)
+class ChoiceWireVariant:
+ """A wire format variant for a CHOICE alternative.
+
+ Each variant represents one possible alternative selection.
+ """
+
+ name: str
+ index: int
+ index_bits: int
+ type_ref: str
+ value_bits: int
+ total_bits: int
+
+
+def get_choice_variants(
+ alternatives: Sequence[ChoiceAlternativeDict],
+ index_bits: int,
+) -> list[ChoiceWireVariant]:
+ """Generate wire format variants for a CHOICE type.
+
+ Produces one variant per alternative, carrying domain data for the
+ template to render (parallel to ``get_sequence_variants``).
+
+ Args:
+ alternatives: List of alternative dicts with keys ``name``,
+ ``type_ref``, ``bit_width``, and ``index``.
+ index_bits: Number of bits for the CHOICE index.
+
+ Returns:
+ List of ChoiceWireVariant objects, one per alternative.
+
+ Examples:
+ >>> alts = [
+ ... {"name": "a", "type_ref": "TypeA", "bit_width": 4, "index": 0},
+ ... {"name": "b", "type_ref": "TypeB", "bit_width": 8, "index": 1},
+ ... ]
+ >>> variants = get_choice_variants(alts, index_bits=1)
+ >>> len(variants)
+ 2
+ >>> variants[0].name
+ 'a selected, 5 bits total'
+ >>> variants[0].total_bits
+ 5
+ >>> variants[0].index
+ 0
+ >>> variants[0].type_ref
+ 'TypeA'
+ >>> variants[0].value_bits
+ 4
+ """
+ return [
+ ChoiceWireVariant(
+ name=f"{alt['name']} selected, "
+ f"{_pluralize_bits(index_bits + alt['bit_width'])} total",
+ index=alt["index"],
+ index_bits=index_bits,
+ type_ref=alt["type_ref"],
+ value_bits=alt["bit_width"],
+ total_bits=index_bits + alt["bit_width"],
+ )
+ for alt in alternatives
+ ]
diff --git a/tools/templates/assemble_df_choice.j2 b/tools/templates/assemble_df_choice.j2
new file mode 100644
index 0000000..be1fbcb
--- /dev/null
+++ b/tools/templates/assemble_df_choice.j2
@@ -0,0 +1,122 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Type Assembly Template
+
+ Assembles all sub-templates into a complete C header file for a CHOICE type.
+
+ Template Context (from generator):
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index, shift, needs_shift
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+ - c_type: bit-width to C type
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%}
+{%- set raw_type = max_wire_bits | c_type -%}
+{%- set min_alt_wire = index_bits + (alternatives | map(attribute='bit_width') | min) -%}
+{%- set max_alt_wire = index_bits + (alternatives | map(attribute='bit_width') | max) -%}
+{% include 'license.j2' %}
+/**
+ * @file
+ * @author Yogev Neumann
+ * @brief J2735 {{ typedef_name }} Definition and Access Macros.
+ *
+ * @par {{ typedef_name }} Wire Format (UPER):
+ * @code
+ * {{ typedef_name }} ::= CHOICE {
+{% for alt in alternatives %}
+ * {{ alt.name }} {{ alt.type_ref }}{{ "," if not loop.last else "" }} -- {{ alt.bit_width }} bits (J2735_BW_{{ alt.type_ref | screaming_snake }})
+{% endfor %}
+ * }
+ * @endcode
+ *
+ * This is a {{ "extensible" if typedef.constraint.is_extensible else "non-extensible" }} CHOICE with {{ alternatives | length }} alternatives.
+ * Per ITU-T X.691 §23, the choice index uses ceil(log2({{ alternatives | length }})) = {{ index_bits }} bit{{ "s" if index_bits != 1 else "" }}.
+{% include 'wire_format_choice_section.j2' %}
+ *
+ * Performance Rationale (Single I/O Pattern):
+ * Reading {{ max_wire_bits }} bits unconditionally (even when smaller alternatives use less) is faster than
+ * reading the index first, then conditionally reading the value bits:
+ * - Single I/O: ~9-10 instructions, 1 memory load, 0 branches
+ * - Two-Step: ~13-14 instructions, 2 memory loads, 1 branch (misprediction risk)
+ * The "garbage" bits (when smaller alternative selected) cost nothing - already in register.
+ * This pattern applies when max_variant ≤ 56 bits (J2735_READ_BITS limit).
+ *
+ * Usage Pattern (single I/O, O(1)):
+ * @code
+ * {{ raw_type }} const raw{{ max_wire_bits }} = J2735_{{ typedef_name_upper }}_RAW_READ(buf); [{{ max_wire_bits }} bits in {{ raw_type }}]
+ *
+ * switch (J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }})) {
+{% for alt in alternatives %}
+ * case J2735_CHOICE_{{ typedef_name_upper }}_{{ alt.name | screaming_snake }}:
+ * use(J2735_{{ typedef_name_upper }}_GET_{{ alt.name | screaming_snake }}(raw{{ max_wire_bits }})); [{{ alt.bit_width | c_type }}: {{ alt.bit_width }} bits]
+ * break;
+{% endfor %}
+ * }
+ *
+ * buf += J2735_BIT_TO_BYTE(J2735_{{ typedef_name_upper }}_SIZE(raw{{ max_wire_bits }})); [uint8_t: {{ min_alt_wire }}{% if min_alt_wire != max_alt_wire %} or {{ max_alt_wire }}{% endif %}]
+ * @endcode
+ */
+#ifndef J2735_INTERNAL_DF_{{ typedef_name | upper }}_H
+#define J2735_INTERNAL_DF_{{ typedef_name | upper }}_H
+
+#include "J2735_internal_common.h"
+#include "J2735_internal_constants.h"
+
+/* ============================================================================================== */
+/* INTERNAL: CHOICE Index Bits */
+/* ============================================================================================== */
+{% include 'choice/choice_internal_choice_index_bits.j2' %}
+
+/* ============================================================================================== */
+/* INTERNAL: Structure Metadata */
+/* ============================================================================================== */
+{% include 'choice/choice_internal_max_wire_bits.j2' %}
+
+/* ============================================================================================== */
+/* PUBLIC API: Alternative Indices */
+/* ============================================================================================== */
+{% include 'choice/choice_alt_indices.j2' %}
+
+/* ============================================================================================== */
+/* PUBLIC API: Raw Read (Single I/O) */
+/* ============================================================================================== */
+{% include 'choice/choice_raw_read.j2' %}
+
+/* ============================================================================================== */
+/* PUBLIC API: Which-Checker (Pure Computation on Raw Value) */
+/* ============================================================================================== */
+{% include 'choice/choice_which.j2' %}
+
+/* ============================================================================================== */
+/* PUBLIC API: Getters (Pure Computation on Raw Value) */
+/* ============================================================================================== */
+{% include 'choice/choice_get.j2' %}
+
+/* ============================================================================================== */
+/* PUBLIC API: Size Calculation (Pure Computation on Raw Value) */
+/* ============================================================================================== */
+{% include 'choice/choice_size.j2' %}
+
+#endif /* J2735_INTERNAL_DF_{{ typedef_name | upper }}_H */
diff --git a/tools/templates/assemble_df_sequence.j2 b/tools/templates/assemble_df_sequence.j2
index ec4ea37..64d5774 100644
--- a/tools/templates/assemble_df_sequence.j2
+++ b/tools/templates/assemble_df_sequence.j2
@@ -24,7 +24,7 @@
Template Context (from generator):
- typedef: ASN1TypeDefinition for the SEQUENCE type
- - variants: list[WireVariant] from get_sequence_variants()
+ - sequence_variants: list[SequenceWireVariant] from get_sequence_variants()
- opt_count: Number of OPTIONAL fields (for bitmap width)
-#}
{%- set has_optional = typedef.constraint.optional_count > 0 -%}
@@ -36,7 +36,7 @@
* @brief J2735 {{ typedef_name }} Definition and Access Macros.
*
* @par {{ typedef_name }} Wire Format (UPER):
-{% include 'wire_format_section.j2' %}
+{% include 'wire_format_sequence_section.j2' %}
*/
#ifndef J2735_INTERNAL_DF_{{ typedef_name | upper }}_H
#define J2735_INTERNAL_DF_{{ typedef_name | upper }}_H
diff --git a/tools/templates/choice/choice_alt_indices.j2 b/tools/templates/choice/choice_alt_indices.j2
new file mode 100644
index 0000000..2ae9910
--- /dev/null
+++ b/tools/templates/choice/choice_alt_indices.j2
@@ -0,0 +1,42 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Alternative Indices Template
+
+ Generates C constants for CHOICE alternative indices (public API).
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+
+ Output format:
+ #define J2735_CHOICE_APPROACH_OR_LANE_APPROACH 0U
+ #define J2735_CHOICE_APPROACH_OR_LANE_LANE 1U
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- for alt in alternatives %}
+/**
+ * @brief Alternative index for '{{ alt.name }}' in {{ typedef_name }} CHOICE.
+ */
+#define J2735_CHOICE_{{ typedef_name_upper }}_{{ alt.name | screaming_snake }} {{ alt.index }}U{{ "\n" if not loop.last }}
+{% endfor %}
diff --git a/tools/templates/choice/choice_get.j2 b/tools/templates/choice/choice_get.j2
new file mode 100644
index 0000000..bfe733d
--- /dev/null
+++ b/tools/templates/choice/choice_get.j2
@@ -0,0 +1,70 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Get Template
+
+ Generates C macros for extracting alternative values from the raw value.
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index, shift, needs_shift
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+ - c_type: bit-width to C type
+
+ Output format:
+ #define J2735_APPROACH_OR_LANE_GET_APPROACH(raw9) \
+ ((uint8_t)((((uint32_t)(raw9)) >> ...) & ...))
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%}
+{% for alt in alternatives %}
+{%- set alt_upper = alt.name | screaming_snake -%}
+{%- set type_ref_upper = alt.type_ref | screaming_snake -%}
+{%- set return_type = alt.bit_width | c_type -%}
+/**
+ * @brief Get '{{ alt.name }}' value ({{ alt.type_ref }}, {{ alt.bit_width }} bits) from pre-read raw value.
+ *
+{% if alt.needs_shift %}
+ * Extracts the {{ alt.bit_width }}-bit value after shifting right by {{ alt.shift }} bits.
+{% else %}
+ * Extracts the {{ alt.bit_width }}-bit value from the lowest bits (no shift needed).
+ *
+ * Generic pattern: ((raw >> (MAX - INDEX_BITS - BW)) & MASK)
+ * Because: shift = {{ max_wire_bits }} - {{ index_bits }} - {{ alt.bit_width }} = 0, we omit the shift (MISRA 12.2 compliant).
+{% endif %}
+ *
+ * @param[in] raw{{ max_wire_bits }} {{ max_wire_bits }}-bit value previously returned by J2735_{{ typedef_name_upper }}_RAW_READ().
+ * @return {{ return_type }}: {{ alt.type_ref }} value ({{ alt.bit_width }} bits).
+ * @pre J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) == J2735_CHOICE_{{ typedef_name_upper }}_{{ alt_upper }}.
+ */
+{% if alt.needs_shift %}
+#define J2735_{{ typedef_name_upper }}_GET_{{ alt_upper }}(raw{{ max_wire_bits }}) \
+ (({{ return_type }})((((uint32_t)(raw{{ max_wire_bits }})) >> \
+ (J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }} - {{ index_bits }}U - J2735_BW_{{ type_ref_upper }})) & \
+ ((1UL << J2735_BW_{{ type_ref_upper }}) - 1UL)))
+{% else %}
+#define J2735_{{ typedef_name_upper }}_GET_{{ alt_upper }}(raw{{ max_wire_bits }}) \
+ (({{ return_type }})((raw{{ max_wire_bits }}) & ((1UL << J2735_BW_{{ type_ref_upper }}) - 1UL)))
+{% endif -%}
+{{ "\n" if not loop.last }}
+{%- endfor %}
diff --git a/tools/templates/choice/choice_internal_choice_index_bits.j2 b/tools/templates/choice/choice_internal_choice_index_bits.j2
new file mode 100644
index 0000000..236ca5d
--- /dev/null
+++ b/tools/templates/choice/choice_internal_choice_index_bits.j2
@@ -0,0 +1,44 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Index Bits Template
+
+ Generates C constant for CHOICE type index bit width (internal).
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+
+ Output format:
+ #define J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE 1U
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+/**
+ * @internal
+ * @brief Number of bits for {{ typedef_name }} choice index.
+ *
+ * {{ typedef_name }} has {{ alternatives | length }} alternatives, so index = ceil(log2({{ alternatives | length }})) = {{ index_bits }} bit{{ "s" if index_bits != 1 else "" }}.
+ * This is a {{ "extensible" if typedef.constraint.is_extensible else "non-extensible" }} CHOICE.
+ */
+#define J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} {{ index_bits }}U
diff --git a/tools/templates/choice/choice_internal_max_wire_bits.j2 b/tools/templates/choice/choice_internal_max_wire_bits.j2
new file mode 100644
index 0000000..a8e8106
--- /dev/null
+++ b/tools/templates/choice/choice_internal_max_wire_bits.j2
@@ -0,0 +1,57 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Max Wire Bits Template
+
+ Generates C constant for the maximum wire size of a CHOICE type.
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+
+ Output format:
+ #define J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE \
+ (J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + J2735_BW_LANE_ID)
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+{%- set max_alternative = alternatives | selectattr('bit_width', 'eq', (alternatives | map(attribute='bit_width') | max)) | first -%}
+{%- set max_wire_bits = index_bits + max_alternative.bit_width -%}
+{%- set max_type_upper = max_alternative.type_ref | screaming_snake -%}
+/**
+ * @internal
+ * @brief Maximum wire size in bits for {{ typedef_name }} encoding.
+ *
+ * This is the larger of the alternatives: {{ index_bits }} (index) + {{ max_alternative.bit_width }} ({{ max_alternative.name }}) = {{ max_wire_bits }} bits.
+ *
+ * We always read MAX bits ({{ max_wire_bits }}) unconditionally rather than reading the index first
+ * then conditionally reading the value bits. Assembly-level analysis:
+ *
+ * Single I/O (read max): ~9-10 instructions, 1 memory load, 0 branches
+ * Two-Step (conditional): ~13-14 instructions, 2 memory loads, 1 branch
+ *
+ * The "garbage" bits when a smaller alternative is selected cost nothing - they're already
+ * loaded into the register and simply get shifted/masked away.
+ */
+#define J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }} \
+ (J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} + J2735_BW_{{ max_type_upper }})
diff --git a/tools/templates/choice/choice_raw_read.j2 b/tools/templates/choice/choice_raw_read.j2
new file mode 100644
index 0000000..ddc0997
--- /dev/null
+++ b/tools/templates/choice/choice_raw_read.j2
@@ -0,0 +1,67 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Raw Read Template
+
+ Generates C macro for reading raw bits from buffer (single I/O operation).
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+ - c_type: bit-width to C type
+ - bytes_from_bits: ceil(bits / 8)
+
+ Output format:
+ #define J2735_APPROACH_OR_LANE_RAW_READ(buf) \
+ ((uint16_t)J2735_READ_BITS((buf), 0U, J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE))
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%}
+{%- set raw_type = max_wire_bits | c_type -%}
+{%- set raw_bytes = max_wire_bits | bytes_from_bits -%}
+/**
+ * @brief Read raw {{ max_wire_bits }} bits for {{ typedef_name }} encoding (single I/O operation).
+ *
+ * Reads {{ max_wire_bits }} bits (maximum wire size) into a {{ raw_type }}. All other {{ typedef_name }}
+ * macros operate on this pre-read {{ max_wire_bits }}-bit value for O(1) performance.
+ *
+ * Raw value bit layout ({{ max_wire_bits }} bits in {{ raw_type }}, right-justified):
+ * @code
+ * {{ raw_type }} bit: {{ max_wire_bits - 1 }}..0
+ * [I][Value bits...]
+ *
+ * Where:
+ * I = CHOICE index
+ * Value bits = Alternative-specific data
+ * @endcode
+ *
+ * @param[in] buf Pointer to the {{ typedef_name }} encoding (const uint8_t*).
+ * @return {{ raw_type }} containing the raw {{ max_wire_bits }}-bit encoding.
+ * @warning J2735_READ_BITS loads 8 bytes for efficient bit extraction. Caller must ensure
+ * buffer has at least 8 bytes of readable memory ({{ raw_bytes }} data + {{ 8 - raw_bytes }} padding, or more).
+ *
+ * @note Store result in `{{ raw_type }} raw{{ max_wire_bits }}` variable, then pass to other macros.
+ */
+#define J2735_{{ typedef_name_upper }}_RAW_READ(buf) \
+ (({{ raw_type }})J2735_READ_BITS((buf), 0U, J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }}))
diff --git a/tools/templates/choice/choice_size.j2 b/tools/templates/choice/choice_size.j2
new file mode 100644
index 0000000..9c17f2a
--- /dev/null
+++ b/tools/templates/choice/choice_size.j2
@@ -0,0 +1,75 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Size Template
+
+ Generates C macro for calculating the wire size based on selected alternative.
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+
+ Output format:
+ #define J2735_APPROACH_OR_LANE_SIZE(raw9) \
+ ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE + ...))
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%}
+{%- set min_alt_wire = index_bits + (alternatives | map(attribute='bit_width') | min) -%}
+/**
+ * @brief Calculate the total wire size in bits from pre-read raw value.
+ *
+ * The size depends on which alternative is selected:
+{% for alt in alternatives %}
+ * - {{ alt.name }}: {{ index_bits }} (index) + {{ alt.bit_width }} (value) = {{ index_bits + alt.bit_width }} bits
+{% endfor %}
+ *
+ * @param[in] raw{{ max_wire_bits }} {{ max_wire_bits }}-bit value previously returned by J2735_{{ typedef_name_upper }}_RAW_READ().
+ * @return uint8_t: Total encoding size in bits ({{ min_alt_wire }}{% if min_alt_wire != max_wire_bits %} or {{ max_wire_bits }}{% endif %}).
+ */
+{% if alternatives | length == 2 %}
+{#- Two alternatives: use ternary operator -#}
+{%- set alt0 = alternatives[0] -%}
+{%- set alt1 = alternatives[1] -%}
+{%- set alt0_upper = alt0.name | screaming_snake -%}
+{%- set type_ref0_upper = alt0.type_ref | screaming_snake -%}
+{%- set type_ref1_upper = alt1.type_ref | screaming_snake -%}
+#define J2735_{{ typedef_name_upper }}_SIZE(raw{{ max_wire_bits }}) \
+ ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} + \
+ ((J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) == J2735_CHOICE_{{ typedef_name_upper }}_{{ alt0_upper }}) \
+ ? (J2735_BW_{{ type_ref0_upper }}) \
+ : (J2735_BW_{{ type_ref1_upper }}))))
+{% else %}
+{#- More than two alternatives: use switch-style nested ternary or comment for manual impl -#}
+{#- For now, generate a lookup array or nested ternary -#}
+#define J2735_{{ typedef_name_upper }}_SIZE(raw{{ max_wire_bits }}) \
+ ((uint8_t)(J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }} + \
+{% for alt in alternatives %}
+{%- set alt_upper = alt.name | screaming_snake -%}
+{%- set type_ref_upper = alt.type_ref | screaming_snake -%}
+ {{ "(" if not loop.first else "" }}(J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) == J2735_CHOICE_{{ typedef_name_upper }}_{{ alt_upper }}) \
+ ? (J2735_BW_{{ type_ref_upper }}) \
+ : {{ "0U" if loop.last else "" }}{{ "))" if loop.last else "" }}{{ " \\" if not loop.last else "" }}
+{% endfor %}
+{% endif %}
diff --git a/tools/templates/choice/choice_which.j2 b/tools/templates/choice/choice_which.j2
new file mode 100644
index 0000000..90bcfc5
--- /dev/null
+++ b/tools/templates/choice/choice_which.j2
@@ -0,0 +1,50 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ CHOICE Which Template
+
+ Generates C macro for extracting the choice index from the raw value.
+
+ Context variables:
+ - typedef: ASN1TypeDefinition for the CHOICE type
+ - alternatives: List of dicts with name, type_ref, bit_width, index
+
+ Filters used:
+ - screaming_snake: Converts camelCase to SCREAMING_SNAKE_CASE
+
+ Output format:
+ #define J2735_APPROACH_OR_LANE_WHICH(raw9) \
+ ((uint8_t)((((uint32_t)(raw9)) >> (J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE - 1U)) & ...))
+-#}
+{%- set typedef_name = typedef.name -%}
+{%- set typedef_name_upper = typedef_name | screaming_snake -%}
+{%- set index_bits = typedef.constraint.uper_bit_width -%}
+{%- set max_wire_bits = index_bits + (alternatives | map(attribute='bit_width') | max) -%}
+/**
+ * @brief Get the CHOICE alternative index ({{ index_bits }} bit{{ "s" if index_bits != 1 else "" }}) from pre-read raw value.
+ *
+ * Extracts the index bits from the MSB position of the raw value.
+ *
+ * @param[in] raw{{ max_wire_bits }} {{ max_wire_bits }}-bit value previously returned by J2735_{{ typedef_name_upper }}_RAW_READ().
+ * @return uint8_t: Alternative index (0-{{ (2 ** index_bits) - 1 }}).
+ * Use J2735_CHOICE_{{ typedef_name_upper }}_* constants for comparison in switch statements.
+ */
+#define J2735_{{ typedef_name_upper }}_WHICH(raw{{ max_wire_bits }}) \
+ ((uint8_t)((((uint32_t)(raw{{ max_wire_bits }})) >> (J2735_INTERNAL_MAX_WIRE_BITS_{{ typedef_name_upper }} - {{ index_bits }}U)) & \
+ ((1U << J2735_INTERNAL_CHOICE_INDEX_BITS_{{ typedef_name_upper }}) - 1U)))
diff --git a/tools/templates/dataframe_struct.j2 b/tools/templates/dataframe_struct.j2
index bb8f3a8..c3d38e8 100644
--- a/tools/templates/dataframe_struct.j2
+++ b/tools/templates/dataframe_struct.j2
@@ -28,7 +28,7 @@
- typedef: ASN1TypeDefinition for the SEQUENCE type with:
- name: Original type name (e.g., "BSMcoreData")
- uper_bit_width: Total bit-width of the SEQUENCE
- - variants: list[WireVariant] from get_sequence_variants()
+ - sequence_variants: list[SequenceWireVariant] from get_sequence_variants()
- opt_count: Number of OPTIONAL fields (for bitmap width)
Filters used:
@@ -51,7 +51,7 @@
* @brief Container for {{ typedef.name }} ({{ typedef.uper_bit_width | bytes_from_bits }} bytes / {{ typedef.uper_bit_width }} bits).
*
* @par {{ typedef.name }} Wire Format (UPER):
-{% include 'wire_format_section.j2' %}
+{% include 'wire_format_sequence_section.j2' %}
*/
typedef struct J2735_{{ typedef.name }} J2735_{{ typedef.name }}_t; /* MISRA 8.4: Forward declaration */
typedef struct J2735_{{ typedef.name }} {
diff --git a/tools/templates/wire_format_choice_section.j2 b/tools/templates/wire_format_choice_section.j2
new file mode 100644
index 0000000..0c20519
--- /dev/null
+++ b/tools/templates/wire_format_choice_section.j2
@@ -0,0 +1,83 @@
+{#-
+ Copyright 2026 Yogev Neumann
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ SPDX-License-Identifier: Apache-2.0
+ SPDX-FileCopyrightText: 2026 Yogev Neumann
+-#}
+{#-
+ J2735 CHOICE Wire Format Documentation Section (Partial Template)
+
+ Renders wire format tables for all CHOICE alternatives.
+ Builds segments from domain data in ChoiceWireVariant, mirroring
+ how wire_format_table.j2 builds segments from SequenceWireVariant.fields.
+
+ Context variables:
+ - choice_variants: list[ChoiceWireVariant] from get_choice_variants()
+
+ Each ChoiceWireVariant carries domain data:
+ - index: alternative index value
+ - index_bits: number of bits for the CHOICE index
+ - type_ref: type reference name (e.g., "ApproachID")
+ - value_bits: value bit width
+
+ Output (rendered inside an existing Doxygen comment block):
+ * @par Wire Format (approach selected, 5 bits):
+ * @code
+ * ┌──────────┬──────────────────────────────┐
+ * │ Bit 0 │ Bits 1-4 │
+ * ├──────────┼──────────────────────────────┤
+ * │ Index = 0│ ApproachID value (4 bits) │
+ * └──────────┴──────────────────────────────┘
+ * @endcode
+-#}
+{% for variant in choice_variants %}
+ *
+ * @par Wire Format ({{ variant.name }}):
+ * @code
+{# Build segments from domain data (header, content, min_width) -#}
+{%- set segments = [] -%}
+{#- Index segment -#}
+{%- if variant.index_bits == 1 -%}
+{%- set idx_header = "Bit 0" -%}
+{%- else -%}
+{%- set idx_header = "Bits 0-" ~ (variant.index_bits - 1) -%}
+{%- endif -%}
+{%- set idx_content = "Index = " ~ variant.index -%}
+{%- set _ = segments.append((idx_header, idx_content, [idx_header | length, idx_content | length, 7] | max)) -%}
+{#- Value segment -#}
+{%- set val_start = variant.index_bits -%}
+{%- set val_end = variant.index_bits + variant.value_bits - 1 -%}
+{%- if val_start == val_end -%}
+{%- set val_header = "Bit " ~ val_start -%}
+{%- else -%}
+{%- set val_header = "Bits " ~ val_start ~ "-" ~ val_end -%}
+{%- endif -%}
+{%- set val_content = variant.type_ref ~ " value (" ~ variant.value_bits ~ " bits)" -%}
+{%- set _ = segments.append((val_header, val_content, [val_header | length, val_content | length, 12] | max)) -%}
+{#- Calculate column widths -#}
+{%- set widths = [] -%}
+{%- for seg in segments -%}
+{%- set w = [seg[0] | length, seg[1] | length, seg[2]] | max -%}
+{%- set _ = widths.append(w) -%}
+{%- endfor %}
+ * ┌{% for w in widths %}{{ "─" * (w + 2) }}{% if not loop.last %}┬{% endif %}{% endfor %}┐
+ * │{% for i in range(segments | length) %} {{ "%-*s" | format(widths[i], segments[i][0]) }} │{% endfor %}
+
+ * ├{% for w in widths %}{{ "─" * (w + 2) }}{% if not loop.last %}┼{% endif %}{% endfor %}┤
+ * │{% for i in range(segments | length) %} {{ "%-*s" | format(widths[i], segments[i][1]) }} │{% endfor %}
+
+ * └{% for w in widths %}{{ "─" * (w + 2) }}{% if not loop.last %}┴{% endif %}{% endfor %}┘
+ * @endcode
+{% endfor %}
diff --git a/tools/templates/wire_format_compact.j2 b/tools/templates/wire_format_compact.j2
index 44641bb..9df1f38 100644
--- a/tools/templates/wire_format_compact.j2
+++ b/tools/templates/wire_format_compact.j2
@@ -21,10 +21,10 @@
Renders a wire format variant as a compact row-based table.
Used for large types (>6 fields) where column layout is too wide.
- Works directly with WireVariant containing SequenceField objects.
+ Works directly with SequenceWireVariant containing SequenceField objects.
Context variables:
- - variant: WireVariant object (from j2735_c_generator_wire_format)
+ - variant: SequenceWireVariant object (from j2735_c_generator_wire_format)
- opt_count: Number of optional fields (for bitmap width)
Output format (row-based):
diff --git a/tools/templates/wire_format_section.j2 b/tools/templates/wire_format_sequence_section.j2
similarity index 93%
rename from tools/templates/wire_format_section.j2
rename to tools/templates/wire_format_sequence_section.j2
index 622e99c..90c12e2 100644
--- a/tools/templates/wire_format_section.j2
+++ b/tools/templates/wire_format_sequence_section.j2
@@ -25,7 +25,7 @@
Context variables:
- typedef: ASN1TypeDefinition object
- - variants: list[WireVariant] from get_sequence_variants()
+ - sequence_variants: list[SequenceWireVariant] from get_sequence_variants()
- opt_count: Number of OPTIONAL fields (for bitmap width)
Required filters (registered in create_jinja_env):
@@ -42,7 +42,7 @@
* @code
{% include 'asn1_definition.j2' %}
* @endcode
-{% for variant in variants %}
+{% for variant in sequence_variants %}
*
* @par Wire Format ({{ variant.name }}):
* @code
diff --git a/tools/templates/wire_format_table.j2 b/tools/templates/wire_format_table.j2
index 9466345..0728fcf 100644
--- a/tools/templates/wire_format_table.j2
+++ b/tools/templates/wire_format_table.j2
@@ -20,10 +20,10 @@
J2735 Wire Format Table Template
Renders a single wire format variant as a Unicode box-drawing table.
- Works directly with WireVariant containing SequenceField objects.
+ Works directly with SequenceWireVariant containing SequenceField objects.
Context variables:
- - variant: WireVariant object (from j2735_c_generator_wire_format)
+ - variant: SequenceWireVariant object (from j2735_c_generator_wire_format)
- opt_count: Number of optional fields (for bitmap width)
Output format:
diff --git a/tools/tests/c_generator/test_choice_type.py b/tools/tests/c_generator/test_choice_type.py
new file mode 100644
index 0000000..bda0a6c
--- /dev/null
+++ b/tools/tests/c_generator/test_choice_type.py
@@ -0,0 +1,141 @@
+# Copyright 2026 Yogev Neumann
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: 2026 Yogev Neumann
+"""Tests for CHOICE type code generation via generate_data_frame()."""
+
+from typing import ClassVar
+from unittest import TestCase
+
+from tools.j2735_c_generator_data_frame import generate_data_frame
+from tools.j2735_spec_parser import J2735Specification, parse_spec_file
+from tools.tests.conftest import SPEC_FILE_PATH
+
+
+class TestGenerateDataframeChoice(TestCase):
+ """Tests for generate_data_frame() with CHOICE types."""
+
+ spec: ClassVar[J2735Specification]
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ """Load spec once for all tests."""
+ cls.spec = parse_spec_file(SPEC_FILE_PATH)
+
+ def test_generates_header_guard(self) -> None:
+ """Generated code includes proper header guard."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("#ifndef J2735_INTERNAL_DF_APPROACHORLANE_H", code)
+ self.assertIn("#define J2735_INTERNAL_DF_APPROACHORLANE_H", code)
+ self.assertIn("#endif /* J2735_INTERNAL_DF_APPROACHORLANE_H */", code)
+
+ def test_generates_includes(self) -> None:
+ """Generated code includes required headers."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn('#include "J2735_internal_common.h"', code)
+ self.assertIn('#include "J2735_internal_constants.h"', code)
+
+ def test_generates_index_bits_constant(self) -> None:
+ """Generated code includes CHOICE index bits constant."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_INTERNAL_CHOICE_INDEX_BITS_APPROACH_OR_LANE", code)
+ self.assertIn("1U", code) # 2 alternatives -> 1 bit
+
+ def test_generates_alternative_constants(self) -> None:
+ """Generated code includes alternative index constants."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_CHOICE_APPROACH_OR_LANE_APPROACH 0U", code)
+ self.assertIn("J2735_CHOICE_APPROACH_OR_LANE_LANE 1U", code)
+
+ def test_generates_max_wire_bits(self) -> None:
+ """Generated code includes MAX_WIRE_BITS constant."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_INTERNAL_MAX_WIRE_BITS_APPROACH_OR_LANE", code)
+ self.assertIn("J2735_BW_LANE_ID", code) # Max alternative
+
+ def test_generates_raw_read_macro(self) -> None:
+ """Generated code includes RAW_READ macro."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_APPROACH_OR_LANE_RAW_READ(buf)", code)
+ self.assertIn("J2735_READ_BITS", code)
+
+ def test_generates_which_macro(self) -> None:
+ """Generated code includes WHICH macro."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_APPROACH_OR_LANE_WHICH(raw9)", code)
+
+ def test_generates_get_approach_macro(self) -> None:
+ """Generated code includes GET_APPROACH macro."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_APPROACH_OR_LANE_GET_APPROACH(raw9)", code)
+ self.assertIn("J2735_BW_APPROACH_ID", code)
+
+ def test_generates_get_lane_macro(self) -> None:
+ """Generated code includes GET_LANE macro."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_APPROACH_OR_LANE_GET_LANE(raw9)", code)
+ self.assertIn("J2735_BW_LANE_ID", code)
+
+ def test_generates_size_macro(self) -> None:
+ """Generated code includes SIZE macro."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("J2735_APPROACH_OR_LANE_SIZE(raw9)", code)
+
+ def test_generates_license_header(self) -> None:
+ """Generated code includes Apache 2.0 license header."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("Copyright 2026 Yogev Neumann", code)
+ self.assertIn("Apache License, Version 2.0", code)
+ self.assertIn("SPDX-License-Identifier: Apache-2.0", code)
+
+ def test_generates_wire_format_docs(self) -> None:
+ """Generated code includes wire format documentation."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("Wire Format", code)
+ self.assertIn("approach selected", code)
+ self.assertIn("lane selected", code)
+
+ def test_generates_usage_example(self) -> None:
+ """Generated code includes usage example in documentation."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ self.assertIn("Usage Pattern", code)
+ self.assertIn("switch", code)
+
+ def test_misra_compliant_no_shift_by_zero(self) -> None:
+ """GET_LANE macro uses mask-only pattern (no shift by 0)."""
+ code = generate_data_frame("ApproachOrLane", self.spec)
+ # Lane is the max alternative, so shift = 9 - 1 - 8 = 0
+ # Should NOT have a shift in the GET_LANE macro
+ get_lane_line = [
+ line
+ for line in code.split("\n")
+ if "J2735_APPROACH_OR_LANE_GET_LANE" in line and "#define" in line
+ ]
+ self.assertTrue(len(get_lane_line) > 0)
+ # The mask-only pattern should have a comment indicating no shift needed
+ self.assertIn("no shift needed", code.lower())
+
+ def test_unknown_type_raises(self) -> None:
+ """generate_data_frame raises ValueError for unknown type."""
+ with self.assertRaises(ValueError) as cm:
+ generate_data_frame("NonExistentType", self.spec)
+ self.assertIn("not found", str(cm.exception))
+
+ def test_non_choice_or_sequence_type_raises(self) -> None:
+ """generate_data_frame raises ValueError for non-composite type."""
+ # ApproachID is an INTEGER, not a SEQUENCE or CHOICE
+ with self.assertRaises(ValueError) as cm:
+ generate_data_frame("ApproachID", self.spec)
+ self.assertIn("not a supported Data Frame type", str(cm.exception))
diff --git a/tools/tests/c_generator/test_wire_format_templates.py b/tools/tests/c_generator/test_wire_format_templates.py
index d4fde80..16c475f 100644
--- a/tools/tests/c_generator/test_wire_format_templates.py
+++ b/tools/tests/c_generator/test_wire_format_templates.py
@@ -28,7 +28,7 @@
from tools.j2735_c_generator_jinja import create_jinja_env, get_template
from tools.j2735_c_generator_wire_format import (
- WireVariant,
+ SequenceWireVariant,
get_sequence_variants,
)
from tools.j2735_spec_constraints import (
@@ -76,7 +76,7 @@ def _render_asn1(typedef: ASN1TypeDefinition) -> str:
def _render_wire_format(
- variant: WireVariant,
+ variant: SequenceWireVariant,
opt_count: int,
*,
compact: bool = False,
@@ -84,7 +84,7 @@ def _render_wire_format(
"""Render a wire format table template.
Args:
- variant: The WireVariant to render.
+ variant: The SequenceWireVariant to render.
opt_count: Number of optional fields.
compact: If True, use compact (row-based) template.
@@ -382,11 +382,11 @@ def test_doxygen_prefix(self) -> None:
class TestWireFormatTableTemplate(TestCase):
"""Tests for wire_format_table.j2 rendering."""
- def _make_fixed_variant(self) -> tuple[WireVariant, int]:
+ def _make_fixed_variant(self) -> tuple[SequenceWireVariant, int]:
"""Create a simple 2-field fixed variant for testing.
Returns:
- Tuple of (WireVariant, opt_count).
+ Tuple of (SequenceWireVariant, opt_count).
"""
seq = make_sequence(
fields=(
@@ -541,11 +541,11 @@ def test_extension_variant_placeholder(self) -> None:
class TestWireFormatCompactTemplate(TestCase):
"""Tests for wire_format_compact.j2 rendering."""
- def _make_large_variant(self) -> tuple[WireVariant, int]:
+ def _make_large_variant(self) -> tuple[SequenceWireVariant, int]:
"""Create a 10-field variant (triggers compact mode).
Returns:
- Tuple of (WireVariant, opt_count).
+ Tuple of (SequenceWireVariant, opt_count).
"""
fields = tuple(make_integer_field(f"field{i}", f"Type{i}", 0, 255) for i in range(10))
seq = make_sequence(fields=fields)
diff --git a/tools/tests/c_generator/test_wire_format_variants.py b/tools/tests/c_generator/test_wire_format_variants.py
index 1193f49..47f0cdd 100644
--- a/tools/tests/c_generator/test_wire_format_variants.py
+++ b/tools/tests/c_generator/test_wire_format_variants.py
@@ -19,13 +19,13 @@
Tests cover:
- _pluralize_bits(): singular/plural grammar for bit counts
- get_sequence_variants(): all 4 SEQUENCE cases + edge cases
- - WireVariant: dataclass field correctness
+ - SequenceWireVariant: dataclass field correctness
"""
from unittest import TestCase
from tools.j2735_c_generator_wire_format import (
- WireVariant,
+ SequenceWireVariant,
_pluralize_bits, # pyright: ignore[reportPrivateUsage]
get_sequence_variants,
)
@@ -42,7 +42,7 @@
)
-def _get_variants(type_name: str, spec: J2735Specification) -> list[WireVariant]:
+def _get_variants(type_name: str, spec: J2735Specification) -> list[SequenceWireVariant]:
"""Look up a SEQUENCE type and return its wire format variants.
Args:
@@ -50,7 +50,7 @@ def _get_variants(type_name: str, spec: J2735Specification) -> list[WireVariant]
spec: The parsed J2735 specification.
Returns:
- List of WireVariant objects for the type.
+ List of SequenceWireVariant objects for the type.
Raises:
ValueError: If type_name is not found or not a SEQUENCE.
@@ -500,12 +500,12 @@ def test_bitstring_field_width(self) -> None:
self.assertEqual(variants[0].total_bits, 21)
-class TestWireVariantImmutability(TestCase):
- """WireVariant is a frozen dataclass — verify immutability."""
+class TestSequenceWireVariantImmutability(TestCase):
+ """SequenceWireVariant is a frozen dataclass — verify immutability."""
def test_frozen(self) -> None:
- """WireVariant raises on attribute assignment."""
- variant = WireVariant(
+ """SequenceWireVariant raises on attribute assignment."""
+ variant = SequenceWireVariant(
name="test",
fields=(),
ext_bit=None,