Skip to content

Development#4

Open
mbuckton wants to merge 43 commits intomainfrom
development
Open

Development#4
mbuckton wants to merge 43 commits intomainfrom
development

Conversation

@mbuckton
Copy link
Copy Markdown
Contributor

@mbuckton mbuckton commented Apr 28, 2026

merge development to main for initial release

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for serial CAN devices including SLCAN protocol and Waveshare USB CAN adapters
    • Added CAN Aerospace message parsing with schema registry and data type codec support
    • Added Raspberry Pi dual MCP2515 CAN configuration guide
  • Improvements

    • Enhanced NMEA 2000 codec with improved field-value source handling
    • Improved CAN device capability reporting and frame processing
    • Better CAN identifier parsing for J1939 messages
  • Documentation

    • Added comprehensive Raspberry Pi CAN setup documentation with systemd service configuration

mbuckton and others added 30 commits January 24, 2026 14:50
…s_interface into development

# Conflicts:
#	README.md
#	canbus-core/src/main/java/io/mapsmessaging/canbus/device/InterfaceIndexResolver.java
#	canbus-core/src/main/java/io/mapsmessaging/canbus/device/JnaInterfaceIndexResolver.java
#	canbus-core/src/main/java/io/mapsmessaging/canbus/device/JnaLibCFacade.java
#	canbus-core/src/main/java/io/mapsmessaging/canbus/device/LibCFacade.java
#	pom.xml
…adding constants, validation, and utility methods
…idation, add null check for `interfaceName`, and clean up unused code in `Vcan0ReadWriteDemo`
Stabilise SocketCAN interface resolution and improve CAN bus throughput handling
…e schema and fix redundant mock in `CanaerospaceFrameParserTest`
…e schema and fix redundant mock in `CanaerospaceFrameParserTest`
… serialization and add detailed unit tests for payload round-trips
…ers and frames to reduce allocation overhead
…ding from field value sources in processors

Expanded `N2kMessageParser` to include `encodeFromSource` method and implemented `FieldValueSource`-based unpacking in `StringProcessor`, `NumericProcessor`, and others. Added unit tests for field value source handling.
mbuckton and others added 13 commits April 16, 2026 09:45
…ith corresponding unit tests

Introduced `SerialCanDevice` for CAN communication over serial interfaces. Added `SlcanCanFrameStreamCodec` and related classes, providing encoding/decoding for SLCAN-based CAN frames. Updated `CanFrame` with validation logic and added extensive unit tests for all new components.
Updated `CanFrame` to distinguish between classic and CAN FD frames with new constants and validation rules. Enhanced data length checks to support up to 64 bytes for CAN FD. Adjusted test to validate constraints for extended data lengths.
…nd ISO Address Claim PGNs

Added detailed PGN configurations for `126993` (Heartbeat) and `60928` (ISO Address Claim) in the NMEA XML database, including field definitions and metadata.
…ield handling logic

Introduced `StringLauProcessor` to handle `STRING_LAU` type fields, updated `N2kMessageParser` for dynamic field packing/unpacking, and refined payload length computations. Implemented new test cases for `STRING_FIX` and `STRING_LAU` fields.
…ding CAN frames

Introduced a codec implementation for the Waveshare USB CAN adapter. Added logic for reading and writing CAN frames, including payload and identifier validation, extended frame support, and error handling for invalid packets.
Implemented extensive test cases to validate encoding and decoding of CAN frames, including standard and extended frames, payload handling, and error scenarios.
…vice` and `SerialCanDevice` to implement it
…ement classic CAN capabilities in codecs

Introduced `getCanCapabilities` to the `CanDevice` interface for querying device capabilities. Implemented a default `classicCan` capability in `CanCapabilities` and integrated it with `SerialCanDevice` and `CanFrameStreamCodec`.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

This pull request implements a comprehensive CAN bus abstraction layer with device/codec interfaces, adds extensive CANaerospace schema-driven message parsing capabilities, refactors NMEA2000 field processing to use cursor-based codec patterns, converts CAN frames to immutable records with validation, upgrades Java target to 21, and disables Maven SonarQube analysis.

Changes

Cohort / File(s) Summary
Build Configuration
.buildkite/pipeline.yml, pom.xml
Removes SonarQube/test-collector Maven plugins; adds canbus-events module; upgrades Java target from 17 to 21.
J1939 CAN Identifier
canbus-J1939/src/main/java/.../CanId.java, canbus-J1939/src/main/java/.../CanIdBuilder.java
Refactors CanId with Lombok @RequiredArgsConstructor; adds PDU classification methods and nullable accessor overloads; tightens CanIdBuilder with priority validation and PDU1 byte constraints.
J1939 Tests
canbus-J1939/src/test/java/.../CanIdBuilderTest.java
Updates tests to validate priority range enforcement via IllegalArgumentException instead of masking.
Core CAN Abstractions
canbus-core/src/main/java/io/mapsmessaging/canbus/device/CanDevice.java, canbus-core/src/main/java/io/mapsmessaging/canbus/device/CanCapabilities.java
Introduces CanDevice interface for device abstraction; adds CanCapabilities.classicCan() factory method.
CAN Frame Model
canbus-core/src/main/java/.../frames/CanFrame.java
Refactors CanFrame from mutable class to immutable record with validation, payload normalization, and conversion APIs (fromBytes, getRawData, isCanFd).
SocketCAN Device
canbus-core/src/main/java/.../device/SocketCanDevice.java
Implements CanDevice; optimizes read path with reusable buffer allocation; refactors write validation into validateWriteArgs helper.
Serial CAN Device
canbus-core/src/main/java/.../device/SerialCanDevice.java
New CanDevice implementation using CanFrameStreamCodec for frame I/O with proper resource cleanup via suppressed exceptions.
Codec Abstraction
canbus-core/src/main/java/.../device/codec/CanFrameStreamCodec.java
New interface defining contract for CAN frame streaming codecs with default initialization, capabilities, flush, and close behaviors.
SLCAN Codec
canbus-core/src/main/java/.../codec/SlcanBitRate.java, canbus-core/src/main/java/.../codec/SlcanCanFrameStreamCodec.java
Adds SlcanBitRate enum and SlcanCanFrameStreamCodec implementing SLCAN CR/LF-delimited text framing with open/close/setBitRate commands.
Waveshare USB CAN Codec
canbus-core/src/main/java/.../codec/impl/WaveshareUsbCanAStreamCodec.java
New binary codec for Waveshare USB CAN "A" devices with 0xAA/0x55 packet framing and little-endian identifier encoding.
JNA Interface Resolution
canbus-core/src/main/java/.../device/JnaInterfaceIndexResolver.java, canbus-core/src/main/java/.../device/JnaLibCFacade.java, canbus-core/src/main/java/.../device/LibCFacade.java
Refactors interface index resolution to use if_nametoindex instead of ioctl(SIOCGIFINDEX); adds JNA binding and facade method.
Core Tests
canbus-core/src/test/java/.../device/SocketCanDeviceTest.java, canbus-core/src/test/java/.../device/JnaInterfaceIndexResolverTest.java, canbus-core/src/test/java/.../frames/CanFrameTest.java, canbus-core/src/test/java/.../codec/SlcanCanFrameStreamCodecTest.java, canbus-core/src/test/java/.../codec/WaveshareUsbCanAStreamCodecTest.java
Updates to match new CanFrame record API; adds comprehensive test suites for codec, frame serialization, and JNA resolver logic.
Test Utilities
canbus-core/src/test/java/.../app/CanLoad.java, canbus-core/src/test/java/.../app/Vcan0ReadWriteDemo.java
Simplifies CanLoad method signature; adds new multi-threaded read/write demo with frame statistics and graceful shutdown.
CANaerospace Schema Module
canbus-events/pom.xml, canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/*
New Maven module with 25+ schema model classes (DTOs) using Lombok @Data/@Value/@Builder for representing CANaerospace message definitions, frame layouts, data types, node services, and validation rules.
CANaerospace Parsing
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/CanaerospaceFrameParser.java, canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/DataTypeCodec.java, canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/ParsedCanaerospaceMessage.java, canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/ResolvedIdentifier.java
Implements frame parsing with 8-byte payload extraction, data type codec with 15+ type handlers (floats, integers, composites, strings), registry-backed identifier resolution, and engineering value computation via resolution/offset.
CANaerospace Registry
canbus-events/src/main/java/.../canaerospace/schema/CanaerospaceSchemaRegistry.java
Loads YAML schema, builds indexed maps for fast identifier/datatype lookup, expands identifier ranges into synthetic definitions, and provides query APIs.
CANaerospace Tests
canbus-events/src/test/java/.../canaerospace/parser/*, canbus-events/src/test/java/.../canaerospace/schema/*
Adds frame parser unit tests, comprehensive round-trip tests validating parse/encode consistency, and schema registry loading/dump tests.
NMEA2000 Codec Refactoring
canbus-nmea2000/src/main/java/.../n2k/codec/Processor.java, canbus-nmea2000/src/main/java/.../n2k/codec/NumericProcessor.java, canbus-nmea2000/src/main/java/.../n2k/codec/LookupProcessor.java, canbus-nmea2000/src/main/java/.../n2k/codec/StringProcessor.java, canbus-nmea2000/src/main/java/.../n2k/codec/ReservedProcessor.java
Converts pack/unpack from void to cursor-returning int methods; adds FieldValueSource interface; refactors all field processors to cursor-aware encoding with relative field positioning.
NMEA2000 String Codec
canbus-nmea2000/src/main/java/.../n2k/codec/StringLauProcessor.java
New processor for LAU-encoded strings with length header and UTF-8/ISO-8859-1 fallback.
NMEA2000 Field Value Source
canbus-nmea2000/src/main/java/.../n2k/codec/FieldValueSource.java
New interface supporting typed value queries (has, getLong, getDouble, getString) as alternative to JSON objects.
NMEA2000 Message Parser
canbus-nmea2000/src/main/java/.../n2k/codec/N2kMessageParser.java
Refactors to cursor-based unpacking; changes JSON envelope from {pgn, decoded} to {packet, name}; adds encodeFromSource(int, FieldValueSource) API.
NMEA2000 Compilation
canbus-nmea2000/src/main/java/.../n2k/compile/N2kCompiledField.java, canbus-nmea2000/src/main/java/.../n2k/compile/N2kCompiler.java
Adds compileTimeFixed and order metadata to compiled fields; refactors compiler to compute field fixedness and payload length from per-field processor methods.
NMEA2000 Schema
canbus-nmea2000/src/main/java/.../n2k/schema/N2kJsonSchemaGenerator.java, canbus-nmea2000/src/main/java/.../n2k/schema/N2kSchemaRegistry.java, canbus-nmea2000/src/main/java/.../n2k/framing/FramePacker.java, canbus-nmea2000/src/main/java/.../n2k/framing/message/KnownMessage.java
Updates JSON schema property from "decoded" to "packet"; makes packFastPacket public; updates error messages.
NMEA2000 Database
canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml
Adds three new PGN definitions (isoRequest, heartbeat, isoAddressClaim); reformats existing field layout.
NMEA2000 Tests
canbus-nmea2000/src/test/java/.../n2k/BaseTest.java, canbus-nmea2000/src/test/java/.../n2k/N2kCodecRoundTripTest.java, canbus-nmea2000/src/test/java/.../n2k/N2kJsonSchemaValidationTest.java, canbus-nmea2000/src/test/java/.../n2k/N2kRoundTripAllPgnsTest.java, canbus-nmea2000/src/test/java/.../n2k/N2kRoundTripCanboatLog*.java, canbus-nmea2000/src/test/java/.../n2k/codec/N2kMessageParserFieldValueSourceTest.java, canbus-nmea2000/src/test/java/.../n2k/schema/N2kJsonSchemaGeneratorTest.java
Updates all tests to use "packet" instead of "decoded"; refactors value generation from compiled fields to definitions; adds MapFieldValueSource test helper; validates round-trip codec consistency.
Documentation & Infrastructure
README.md, canbus-core/pi_setup.md, canbus-core/src/main/script/can0.service, canbus-core/src/main/script/can1.service
Adds DeepWiki badge to README; introduces Raspberry Pi CAN HAT setup guide with device tree overlays, can-utils, and systemd service units for dual MCP2515 CAN controllers.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CanaerospaceFrameParser
    participant CanaerospaceSchemaRegistry
    participant DataTypeCodec
    participant ParsedCanaerospaceMessage

    Client->>CanaerospaceSchemaRegistry: findIdentifier(canId)
    CanaerospaceSchemaRegistry-->>CanaerospaceFrameParser: ResolvedIdentifier

    Client->>CanaerospaceFrameParser: parse(canId, payload)
    
    CanaerospaceFrameParser->>CanaerospaceFrameParser: Extract node/code/datatype fields
    
    CanaerospaceFrameParser->>CanaerospaceSchemaRegistry: findMessageType(canId)
    CanaerospaceSchemaRegistry-->>CanaerospaceFrameParser: MessageTypeDefinition
    
    CanaerospaceFrameParser->>CanaerospaceSchemaRegistry: findDataTypeNameByNumber(number)
    CanaerospaceSchemaRegistry-->>CanaerospaceFrameParser: dataTypeName

    CanaerospaceFrameParser->>DataTypeCodec: decode(dataTypeName, bytes)
    DataTypeCodec-->>CanaerospaceFrameParser: rawValue (typed)
    
    CanaerospaceFrameParser->>CanaerospaceFrameParser: Compute engineeringValue = rawValue × resolution
    
    CanaerospaceFrameParser->>ParsedCanaerospaceMessage: populate(canId, nodeId, rawValue, engineeringValue...)
    
    ParsedCanaerospaceMessage-->>CanaerospaceFrameParser: initialized
    CanaerospaceFrameParser-->>Client: ParsedCanaerospaceMessage
Loading
sequenceDiagram
    participant Client
    participant N2kMessageParser
    participant Processor1 as NumericProcessor
    participant Processor2 as StringLauProcessor
    participant Processor3 as LookupProcessor

    Client->>N2kMessageParser: encodeFromSource(pgn, FieldValueSource)
    
    N2kMessageParser->>N2kMessageParser: Initialize cursor=0, payload[]
    
    loop for each compiled field
        N2kMessageParser->>Processor1: pack(field, payload, cursor, source)
        Processor1->>Processor1: Read value from source via fieldId
        Processor1->>Processor1: Scale/validate numeric value
        Processor1->>Processor1: Insert bits at cursor position
        Processor1-->>N2kMessageParser: newCursor
        
        N2kMessageParser->>N2kMessageParser: cursor = newCursor
    end
    
    N2kMessageParser->>N2kMessageParser: Compute final payload length
    N2kMessageParser-->>Client: byte[] payload (encoded)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~110 minutes


🐰 A cursor hops through fields, neat!
Schemas parse the aerospace beat,
Codecs dance with proper grace,
As frames now immutable embrace,
Java twenty-one's complete! 🎉

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

return 0;
}

Integer bitLength = field.getBitLength();
}

private static int computeRelativeBytes(N2kCompiledField field) {
Integer bitLength = field.getBitLength();
}

private static String buildFixedStringValue(N2kCompiledField field, String id) {
Integer bitLength = field.getBitLength();
boolean reserved = fieldType == N2kFieldType.RESERVED;

String id = fieldDefinition.getId();
if (!reserved && id == null || id.isBlank() ||!seenIds.add(id)) {
}

int bytesRead = libC.read(this.socketFileDescriptor, buffer, fdSize);
public CanFrame readFrame() throws IOException {
return canFrameStreamCodec.read(inputStream);
}

public void writeFrame(CanFrame canFrame) throws IOException {
this.canFrameStreamCodec.initialise(this.inputStream, this.outputStream);
}

public CanFrame readFrame() throws IOException {
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/framing/FramePacker.java (1)

74-80: ⚠️ Potential issue | 🟠 Major

Harden public packFastPacket with explicit precondition checks.

After Line 74 made this method public, callers can now pass invalid inputs that previously were screened by pack(...). Add guards for payload == null and payload.length <= 8 to avoid NPEs and invalid fast-packet framing.

Proposed fix
   public List<CanFrame> packFastPacket(
       int pgn,
       int canIdentifier,
       int sourceAddress,
       int destinationAddress,
       byte[] payload
   ) {
+    Objects.requireNonNull(payload, "payload");
+    if (payload.length <= 8) {
+      throw new IllegalArgumentException("Fast packet requires payload length > 8 bytes");
+    }
+
     int sequenceId = sequenceProvider.nextSequenceId(pgn, sourceAddress, destinationAddress);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/framing/FramePacker.java`
around lines 74 - 80, The public method packFastPacket currently lacks input
validation and can receive null or too-short payloads now that it’s exposed; add
explicit precondition checks at the start of packFastPacket (the method
signature with parameters pgn, canIdentifier, sourceAddress, destinationAddress,
byte[] payload) to throw an IllegalArgumentException (or NullPointerException
for null if you prefer consistency) when payload == null or when payload.length
<= 8, with clear messages (e.g., "payload must not be null" and "payload must be
> 8 bytes for fast-packet framing") so the method no longer proceeds with
invalid input and avoids NPEs or invalid framing.
canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/framing/message/KnownMessage.java (1)

34-41: ⚠️ Potential issue | 🟡 Minor

Inconsistency: field name decoded vs error message "packet".

The Objects.requireNonNull error message was changed to "packet" (line 40), but the field is still named decoded (line 34). If a NullPointerException is thrown, the message will reference "packet" while the actual field is decoded, which could confuse debugging.

Either rename the field to packet for consistency with the PR-wide rename, or keep the error message as "decoded" to match the field name.

Option 1: Keep error message consistent with field name
-    this.decoded = Objects.requireNonNull(decoded, "packet");
+    this.decoded = Objects.requireNonNull(decoded, "decoded");
Option 2: Rename field to match new convention
-  private final JsonObject decoded;
+  private final JsonObject packet;

   public KnownMessage(CanId canId, int rawCanIdentifier, byte[] payload, JsonObject decoded) {
     this.canId = Objects.requireNonNull(canId, "canId");
     this.rawCanIdentifier = rawCanIdentifier;
     this.payload = Objects.requireNonNull(payload, "payload");
-    this.decoded = Objects.requireNonNull(decoded, "packet");
+    this.packet = Objects.requireNonNull(decoded, "packet");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/framing/message/KnownMessage.java`
around lines 34 - 41, The constructor in KnownMessage uses
Objects.requireNonNull(decoded, "packet") which mismatches the field name
decoded; update either the error message or the field name for consistency:
either change the requireNonNull call in the KnownMessage constructor to
Objects.requireNonNull(decoded, "decoded") or rename the private final
JsonObject decoded field to packet (and update all usages) so the constructor
message and field name align.
canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kJsonSchemaValidationTest.java (1)

202-204: ⚠️ Potential issue | 🟡 Minor

Remove debug print statement.

The System.err.println("check") statement appears to be leftover debug code and should be removed before merging.

🧹 Remove debug statement
       if (fieldSchema.has("minimum")) {
         double min = fieldSchema.get("minimum").getAsDouble();
-        if(value.getAsDouble() + tolerance < min){
-          System.err.println("check");
-        }
         assertTrue(value.getAsDouble() + tolerance >= min, "Field '" + fieldId + "' below minimum for PGN=" + pgn);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kJsonSchemaValidationTest.java`
around lines 202 - 204, Remove the leftover debug print
System.err.println("check") from the N2kJsonSchemaValidationTest code where the
numeric range check uses value.getAsDouble() + tolerance < min; specifically,
delete that statement inside the test method (the block referencing variables
value, tolerance and min) so the test contains only the intended assertion or
failure handling instead of printing to stderr.
🧹 Nitpick comments (21)
canbus-core/src/main/java/io/mapsmessaging/canbus/device/JnaInterfaceIndexResolver.java (1)

21-37: Make the unused socket parameter explicit to avoid API confusion.

socketFileDescriptor is now ignored. Consider renaming/documenting it as intentionally unused so callers don’t assume descriptor validity affects resolution.

♻️ Proposed minimal clarity patch
-  public int resolveInterfaceIndex(int socketFileDescriptor, String interfaceName) throws IOException {
+  public int resolveInterfaceIndex(int unusedSocketFileDescriptor, String interfaceName) throws IOException {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/main/java/io/mapsmessaging/canbus/device/JnaInterfaceIndexResolver.java`
around lines 21 - 37, The socketFileDescriptor parameter of
resolveInterfaceIndex is unused and causes API confusion; update the method
signature and/or documentation to make this explicit by renaming
socketFileDescriptor to a clearly unused identifier (e.g.,
unusedSocketFileDescriptor or _socketFileDescriptor) and add a Javadoc line on
resolveInterfaceIndex stating the descriptor is ignored, or alternatively
annotate the parameter with `@SuppressWarnings`("unused") and a brief inline
comment; ensure any callers are updated for the rename to avoid compilation
errors.
canbus-core/pi_setup.md (1)

90-118: Unify docs with the versioned systemd units to avoid config drift.

Line 97 has users hand-create canbus.service, while this PR also ships can0.service/can1.service. Keeping both patterns without cross-reference will diverge over time.

Suggested doc direction

Either:

  • document installing the shipped unit files, or
  • keep only the inline canbus.service example and remove/avoid duplicating separate unit definitions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@canbus-core/pi_setup.md` around lines 90 - 118, The docs currently instruct
users to create a hand-written canbus.service while the repo also ships
per-interface units can0.service and can1.service, which risks config drift;
update the pi_setup.md section to either (A) replace the hand-created
canbus.service example with instructions that install and enable the shipped
can0.service and can1.service units (mentioning the unit names can0.service and
can1.service and the enable/start commands), or (B) explicitly state that the
example canbus.service is an alternative and add a cross-reference explaining
the shipped per-interface units (can0.service/can1.service) so maintainers and
users know which approach is recommended and how to install/enable the shipped
units.
canbus-J1939/src/main/java/io/mapsmessaging/canbus/j1939/CanIdBuilder.java (1)

45-53: Add explicit PGN range validation to prevent silent truncation.

pgn is accepted as any int, but only the lower PGN bits are consumed. Rejecting out-of-range/negative values would avoid hidden caller bugs.

♻️ Suggested guard
 public class CanIdBuilder {
 
   private static final int EXTENDED_ID_MASK = 0x1FFFFFFF;
   private static final int PRIORITY_MASK = 0x07;
   private static final int BYTE_MASK = 0xFF;
+  private static final int PGN_MASK = 0x3FFFF; // 18-bit PGN
   private static final int PDU1_MAX_PF = 239;
@@
   public static int build(int pgn, int priority, int sourceAddress, int destinationAddress) {
+    if (pgn < 0 || (pgn & ~PGN_MASK) != 0) {
+      throw new IllegalArgumentException("pgn must be in range 0x00000..0x3FFFF, got 0x" + Integer.toHexString(pgn));
+    }
     if (priority < 0 || priority > PRIORITY_MASK) {
       throw new IllegalArgumentException("priority must be in range 0..7, got " + priority);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@canbus-J1939/src/main/java/io/mapsmessaging/canbus/j1939/CanIdBuilder.java`
around lines 45 - 53, The build(int pgn, int priority, int sourceAddress, int
destinationAddress) method currently accepts any int for pgn but silently
ignores bits outside the PGN field; add explicit validation at the start of
build to reject negative values and values outside the valid J1939 PGN range
(e.g., ensure pgn >= 0 && pgn <= MAX_PGN where MAX_PGN is defined to match the
PGN bit-width used later), and throw an IllegalArgumentException with a clear
message if invalid; reference the pgn parameter and constants/masks (dp, pf
extraction and BYTE_MASK) when adding the check so the guard matches the bit
operations already present.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NumericRange.java (1)

8-10: Add a range invariant guard (min <= max).

Line 9 and Line 10 allow reversed bounds with no fail-fast check. Consider validating this during schema load to prevent invalid metadata entering parsing paths.

Suggested patch
 `@Data`
 public class NumericRange {
   private BigDecimal min;
   private BigDecimal max;
+
+  public void validate() {
+    if (min != null && max != null && min.compareTo(max) > 0) {
+      throw new IllegalArgumentException("NumericRange min must be <= max");
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NumericRange.java`
around lines 8 - 10, The NumericRange class currently allows reversed bounds
because min and max are stored without validation; add a fail-fast invariant
check (min.compareTo(max) <= 0) when the range is created or when setters are
used (e.g., in the NumericRange constructor or any factory
method/withMin/withMax methods) and throw an IllegalArgumentException with a
clear message if min > max so invalid metadata cannot be loaded.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/TimeSlotAllocationExample.java (1)

12-13: Consider validating interval and CAN ID domain constraints.

Line 12 and Line 13 would benefit from a fail-fast check (e.g., positive interval, valid CAN ID range) when schema data is loaded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/TimeSlotAllocationExample.java`
around lines 12 - 13, Add fail-fast validation for TimeSlotAllocationExample by
enforcing transmissionIntervalMs > 0 and a valid CAN ID range when schema data
is loaded: implement a validate() method (or perform checks in the class
constructor or setters setTransmissionIntervalMs and setCanId) that throws
IllegalArgumentException if transmissionIntervalMs is null or <= 0 or if canId
is null or outside the allowed CAN ID domain (e.g., 0..0x7FF for 11-bit standard
IDs or your project’s configured range); call this validate() after
deserialization/creation so invalid schema data fails fast.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/IdRange.java (1)

7-8: Consider validating CAN ID bounds and range ordering.

Line 7 and Line 8 currently accept any Integer values. Adding a guard for valid CAN ID bounds and min <= max would reduce downstream parser ambiguity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/IdRange.java`
around lines 7 - 8, IdRange currently allows any Integer for the fields min and
max; add validation to enforce CAN ID bounds and ordering by checking non-null,
0 <= min <= max <= 0x1FFFFFFF (supporting extended 29-bit IDs) and throwing
IllegalArgumentException when violated. Implement this check in the
IdRange(Integer min, Integer max) constructor and in the setMin(Integer) and
setMax(Integer) mutators (or a private validateRange() helper invoked by them),
and ensure callers cannot create an invalid range by validating inputs before
assignment.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServiceCodeDefinition.java (1)

8-9: Define precedence/one-of rule for serviceCode vs serviceCodeRange.

Allowing both fields without an explicit exclusivity rule can create ambiguous resolution behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServiceCodeDefinition.java`
around lines 8 - 9, NodeServiceCodeDefinition currently allows both serviceCode
and serviceCodeRange to be set which creates ambiguity; update the class
(NodeServiceCodeDefinition) to enforce a one-of rule by validating in the
constructor/factory/builder or a validate() method that exactly one of
serviceCode or serviceCodeRange is non-null (or define explicit precedence and
document it), and throw an IllegalArgumentException (or similar) when the rule
is violated; update any deserialization/creation paths to call this validation
and add unit tests to cover both mutually-exclusive and precedence behaviors.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/FrameDefinition.java (1)

8-8: Use an enum-backed type or add validation for endianness instead of raw String.

The field is declared as String with no validation or parsing logic. Since the codebase already uses ByteOrder enum elsewhere, consider either:

  • Converting endianness to use ByteOrder enum directly, or
  • Adding runtime validation to constrain accepted string values (e.g., "BIG_ENDIAN", "LITTLE_ENDIAN") at schema load time.

This prevents typos and invalid configurations from silently propagating.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/FrameDefinition.java`
at line 8, The endianness field in FrameDefinition is a raw String which risks
invalid values; change it to use the existing ByteOrder enum (replace private
String endianness with private ByteOrder endianness and update getters/setters
and any deserialization logic to map strings to ByteOrder) or, if you must keep
String, add validation in FrameDefinition (e.g., in the setter, constructor, or
schema-load method) that parses the value against ByteOrder.valueOf(...) and
throws a clear exception on invalid input; update any uses of
getEndianness/setEndianness and JSON/XML deserialization code to handle the enum
mapping.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/ResolvedCanDefinition.java (1)

23-23: Consider using @Singular for templateVariables to enforce immutability.

While @Value makes the field reference final, the map contents remain mutable. Using Lombok's @Singular("templateVariable") annotation would wrap the map in an unmodifiable collection during construction, preventing accidental mutations. No mutation patterns were detected in the codebase, but this is a defensive best practice for preventing externally-introduced bugs.

♻️ Suggested change
 import lombok.Builder;
+import lombok.Singular;
 import lombok.Value;
@@
-  Map<String, Object> templateVariables;
+  `@Singular`("templateVariable")
+  Map<String, Object> templateVariables;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/ResolvedCanDefinition.java`
at line 23, The field templateVariables in ResolvedCanDefinition is currently a
mutable Map despite the class being Lombok `@Value`; update the Lombok builder to
use `@Singular`("templateVariable") on the templateVariables declaration so the
generated builder wraps the map as an unmodifiable collection at construction
time, ensuring immutability of the map contents; adjust any existing builder
usages (ResolvedCanDefinition.builder()...) to continue working with the
singular name if needed and remove any direct mutating logic (if present) that
relies on a mutable map.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServicesDefinition.java (1)

5-13: Initialize serviceCodes to an empty list for consistency.

channels and formats are null-safe by default, but serviceCodes is not. Using an empty list here avoids extra null checks in parser/registry code paths.

Proposed diff
 import java.util.LinkedHashMap;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;

 `@Data`
 public class NodeServicesDefinition {
   private Map<String, List<NodeServiceChannelDefinition>> channels = new LinkedHashMap<>();
-  private List<NodeServiceCodeDefinition> serviceCodes;
+  private List<NodeServiceCodeDefinition> serviceCodes = new ArrayList<>();
   private Map<String, NodeServiceFormatDefinition> formats = new LinkedHashMap<>();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServicesDefinition.java`
around lines 5 - 13, NodeServicesDefinition currently leaves the field
serviceCodes uninitialized which can cause NPEs; update the class so the field
serviceCodes is initialized to an empty list (e.g., new ArrayList<>()) to match
channels and formats null-safety. Modify the declaration of serviceCodes in
NodeServicesDefinition to instantiate an empty List (import java.util.ArrayList
if needed) so parser/registry code can avoid extra null checks.
canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/ParsedCanaerospaceMessage.java (1)

35-35: Defensively copy dataBytes to avoid aliasing bugs.

byte[] is mutable; exposing it directly allows callers to mutate message state after parsing.

♻️ Suggested change
 `@Data`
 public class ParsedCanaerospaceMessage {
@@
   private byte[] dataBytes;
@@
+  public byte[] getDataBytes() {
+    return dataBytes == null ? null : dataBytes.clone();
+  }
+
+  public void setDataBytes(byte[] dataBytes) {
+    this.dataBytes = dataBytes == null ? null : dataBytes.clone();
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/ParsedCanaerospaceMessage.java`
at line 35, ParsedCanaerospaceMessage exposes a mutable byte[] field dataBytes
which can be aliased by callers; defensively copy it whenever accepting or
returning the array to prevent external mutation. Update any constructor or
setter that assigns to dataBytes (e.g., ParsedCanaerospaceMessage(...) or
setDataBytes(...)) to assign a copy (clone or Arrays.copyOf) and update the
getter (getDataBytes()) to return a copy instead of the internal array; ensure
all internal assignments use copied arrays to avoid aliasing.
canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/schema/N2kJsonSchemaGenerator.java (1)

48-51: Schema updated to use "packet" property — consider renaming local variable for clarity.

The schema now correctly uses "packet" as the property name (line 107) and requires it (line 113). However, the local variable is still named decoded (line 48), which is slightly confusing. Consider renaming it to packet for consistency.

Optional: Rename local variable for clarity
-    JsonObject decoded = new JsonObject();
-    decoded.addProperty("type", "object");
+    JsonObject packet = new JsonObject();
+    packet.addProperty("type", "object");

-    JsonObject decodedProperties = new JsonObject();
+    JsonObject packetProperties = new JsonObject();
     // ... field processing ...
-    decoded.add("properties", decodedProperties);
-    decoded.addProperty("additionalProperties", false);
+    packet.add("properties", packetProperties);
+    packet.addProperty("additionalProperties", false);

-    properties.add("packet", decoded);
+    properties.add("packet", packet);

Also applies to: 107-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/schema/N2kJsonSchemaGenerator.java`
around lines 48 - 51, The local variable names used when building the JSON
schema are inconsistent with the schema property "packet": rename the JsonObject
variable currently named decoded to packet and decodedProperties to
packetProperties in N2kJsonSchemaGenerator (where the schema sets "packet" as a
property and requires it) so the variable names match the produced JSON
structure; update all references within the method (e.g., where
decoded.addProperty, decoded.add calls, and decodedProperties assignments are
used) to the new names to keep code clear and consistent.
canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java (1)

154-157: Remove unreachable null check on bitLength.

field.getBitLength() is primitive-backed (Lombok auto-generates a primitive int getter), making the null check at line 156 dead code.

♻️ Proposed fix
-    Integer bitLength = field.getBitLength();
-    int lengthBytes = (bitLength == null || bitLength <= 0) ? 8 : (bitLength + 7) >>> 3;
+    int bitLength = field.getBitLength();
+    int lengthBytes = (bitLength <= 0) ? 8 : (bitLength + 7) >>> 3;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java`
around lines 154 - 157, The local variable bitLength should be a primitive int
because N2kCompiledField.getBitLength() returns a primitive; remove the
unreachable null check in buildFixedStringValue and compute lengthBytes directly
from an int bitLength (e.g., int bitLength = field.getBitLength(); int
lengthBytes = (bitLength <= 0) ? 8 : (bitLength + 7) >>> 3), replacing the
Integer declaration and null check with this primitive-based logic.
canbus-core/src/test/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodecTest.java (1)

45-46: Prefer assertTrue/assertFalse over assertEquals for boolean assertions.

Using assertTrue(canFrame.extendedFrame()) is more idiomatic than assertEquals(true, canFrame.extendedFrame()). This pattern appears in multiple tests in this file.

✨ Example fix
-    assertEquals(true, canFrame.extendedFrame());
+    assertTrue(canFrame.extendedFrame());

Similarly for assertEquals(false, ...):

-    assertEquals(false, canFrame.extendedFrame());
+    assertFalse(canFrame.extendedFrame());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/test/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodecTest.java`
around lines 45 - 46, Replace boolean equality assertions using assertEquals
with the idiomatic assertTrue/assertFalse in the tests; specifically, change
assertions like assertEquals(true, canFrame.extendedFrame()) to
assertTrue(canFrame.extendedFrame()) and assertEquals(false, someBoolean) to
assertFalse(someBoolean). Update all occurrences in SlcanCanFrameStreamCodecTest
that call canFrame.extendedFrame() (and any other boolean-returning methods) so
they use junit.framework.Assert/Assertions' assertTrue/assertFalse API instead
of assertEquals.
canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/CanFrameStreamCodec.java (1)

48-50: Clarify that close() does not close the streams.

The close() method only flushes the output stream but doesn't actually close the input/output streams. Consider adding a Javadoc comment to clarify this design decision, or renaming to cleanup() to avoid confusion with typical close() semantics.

📝 Add clarifying documentation
+  /**
+   * Performs codec cleanup before stream closure.
+   * Note: This method flushes but does not close the streams;
+   * stream lifecycle is managed by the owning device.
+   */
   default void close(InputStream inputStream, OutputStream outputStream) throws IOException {
     flush(outputStream);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/CanFrameStreamCodec.java`
around lines 48 - 50, The close(InputStream inputStream, OutputStream
outputStream) method currently only calls flush(outputStream) and does not close
the streams, which is misleading; update the CanFrameStreamCodec API by either
renaming close to a clearer name like cleanup or flushStreams, or add a Javadoc
on the close(...) method that explicitly states it only flushes the OutputStream
and does not close either stream; ensure the change references the existing
close(InputStream, OutputStream) and flush(OutputStream) methods so callers know
to close streams themselves or to use the new method name.
canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/BaseTest.java (1)

211-293: Consider extracting shared logic between the two computeAllowedRawRange overloads.

The computeAllowedRawRange(N2kFieldDefinition) method (lines 228-293) shares significant logic with computeAllowedRawRange(N2kCompiledField) (lines 98-167). Consider extracting the common range computation into a private helper that accepts primitive parameters (bitLength, isSigned, rangeMin, rangeMax, resolution, offset) to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/BaseTest.java`
around lines 211 - 293, The two methods
computeAllowedRawRange(N2kFieldDefinition) and
computeAllowedRawRange(N2kCompiledField) duplicate the same bit/scale clamping
logic; extract that shared logic into a private helper (e.g.,
computeAllowedRawRangeForPrimitives or computeRawRange) that takes primitive
parameters: int bitLength, boolean isSigned, Double rangeMin, Double rangeMax,
double resolution, double offset and returns RawRange; then have both
computeAllowedRawRange(...) overloads call that helper after converting
field/compiledField properties (getBitLength(), isSigned(),
getRangeMin()/getRangeMax(), getResolution(), getOffset()) and keep their
existing early invalid/edge checks only if they differ in semantics.
canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kJsonSchemaValidationTest.java (1)

145-159: Update stale comments referring to "decoded" instead of "packet".

The comments on lines 148, 150, and 156 still reference "decoded" but the code now uses "packet" as the property name. Consider updating for consistency.

📝 Update comments
-    assertTrue(envelope.has("packet"), "Missing decoded for PGN=" + pgn);
+    assertTrue(envelope.has("packet"), "Missing packet for PGN=" + pgn);
     JsonObject decoded = envelope.getAsJsonObject("packet");
-    assertNotNull(decoded, "decoded is not an object for PGN=" + pgn);
+    assertNotNull(decoded, "packet is not an object for PGN=" + pgn);

     JsonObject schemaProperties = schema.getAsJsonObject("properties");
     assertNotNull(schemaProperties, "Schema missing properties for PGN=" + pgn);

     JsonObject decodedSchema = schemaProperties.getAsJsonObject("packet");
-    assertNotNull(decodedSchema, "Schema missing decoded for PGN=" + pgn);
+    assertNotNull(decodedSchema, "Schema missing packet for PGN=" + pgn);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kJsonSchemaValidationTest.java`
around lines 145 - 159, Update the assertion error messages that still mention
"decoded" to use "packet" to match the property name used in the code: change
messages in the assertions around envelope.has/getAsJsonObject("packet") and the
schema checks (references involving envelope, decoded, schemaProperties,
decodedSchema, decodedSchemaProperties) so they say "Missing packet" / "packet
is not an object" / "Schema missing packet" / "Schema packet missing properties"
(preserve PGN context and existing variables names like envelope, decoded,
schemaProperties, decodedSchema, decodedSchemaProperties).
canbus-core/src/test/java/io/mapsmessaging/canbus/app/Vcan0ReadWriteDemo.java (1)

71-87: Consider logging or using the received frame for demo purposes.

The readFrame() result is discarded (line 73). For a demo utility, it might be helpful to log a sample of received frames periodically to show the system is working correctly.

📝 Example enhancement
       try {
-        reader.readFrame();
+        CanFrame frame = reader.readFrame();
         received++;
         Instant now = Instant.now();
         if (Duration.between(lastPrint, now).toSeconds() >= 5) {
-          System.out.println("RX stats: received=" + received);
+          System.out.println("RX stats: received=" + received + ", last frame id=0x" + 
+              Integer.toHexString(frame.canIdentifier()));
           lastPrint = now;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/test/java/io/mapsmessaging/canbus/app/Vcan0ReadWriteDemo.java`
around lines 71 - 87, The demo discards the result of reader.readFrame(), so
capture its return value (e.g., assign to a variable like frame =
reader.readFrame()) and use it for periodic logging; update the loop in
Vcan0ReadWriteDemo to increment received as before but also store the most
recent frame and include a brief summary (timestamp, id, payload length or hex
snippet) when printing RX stats (the same block that uses received and
lastPrint), and ensure you still handle IOException and stop running via
running.set(false).
canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/N2kCompiler.java (1)

147-159: Avoid calling builder.build() twice.

builder.build() is called on line 147-148 to access getBitLength(), and again on line 159 to add the field to the list. This creates an unnecessary intermediate object.

♻️ Store bitLength in a local variable instead
     if (compileTimeFixed
         && fieldDefinition.getBitOffset() != null
         && supportsBitStorage(fieldType)) {

       int bitOffset = fieldDefinition.getBitOffset();
       int startByte = bitOffset >>> 3;
       int startBit = bitOffset & 7;
+      Integer bitLength = resolveBitLength(fieldDefinition);

-      if ( builder.build().getBitLength() > 0) {
-        int totalBits = startBit + builder.build().getBitLength();
+      if (bitLength != null && bitLength > 0) {
+        int totalBits = startBit + bitLength;
         int bytesToRead = (totalBits + 7) >>> 3;

         builder
             .bitOffset(bitOffset)
             .startByte(startByte)
             .startBit(startBit)
             .bytesToRead(bytesToRead);
       }
     }

     compiledFields.add(builder.build());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/N2kCompiler.java`
around lines 147 - 159, The code currently calls builder.build() twice; instead
call builder.build().getBitLength() once and store it in a local int (e.g., int
bitLength = builder.build().getBitLength()), use that bitLength to compute
totalBits and bytesToRead, set
builder.bitOffset(...).startByte(...).startBit(...).bytesToRead(bytesToRead) and
then call builder.build() a single time when adding to compiledFields
(compiledFields.add(builder.build())); this avoids the duplicate build()
invocation while keeping references to bitOffset, startByte, startBit, and
bytesToRead intact.
canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/Processor.java (1)

40-42: Default computePayloadLength(FieldValueSource) returns 0 - ensure processors override when needed.

The default implementation returns 0, which is a no-op. Processors handling variable-length fields (like StringLauProcessor) must override this to compute the correct payload length from the source. The StringLauProcessor correctly overrides this (lines 26-34 in that file), but this could be a pitfall for future implementers.

Consider adding a Javadoc comment to document that processors with variable-length fields must override this method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/Processor.java`
around lines 40 - 42, Add a Javadoc to the
Processor.computePayloadLength(N2kCompiledField, FieldValueSource) method
explaining that the default implementation returns 0 and is a no-op, and that
any Processor which handles variable-length fields (e.g., StringLauProcessor)
must override computePayloadLength to compute the actual payload length from the
FieldValueSource; reference the method name computePayloadLength, the Processor
interface/class, FieldValueSource, and mention StringLauProcessor as an example
so future implementers know to override it.
canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodec.java (1)

44-47: Recursive call for blank lines may cause stack overflow.

Deeply nested recursion when processing many consecutive blank lines could exhaust the stack. Consider using a loop instead.

♻️ Suggested fix using a loop
-    String trimmed = line.trim();
-    if (trimmed.isEmpty()) {
-      return read(inputStream);
-    }
+    String trimmed = line.trim();
+    while (trimmed.isEmpty()) {
+      line = readLine(inputStream);
+      if (line == null) {
+        return null;
+      }
+      trimmed = line.trim();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodec.java`
around lines 44 - 47, The read(inputStream) method in SlcanCanFrameStreamCodec
currently recurses when encountering blank lines (String trimmed = line.trim();
if (trimmed.isEmpty()) { return read(inputStream); }), risking stack overflow;
replace the recursive retry with an iterative loop that reads lines from the
same inputStream until a non-empty trimmed line is found (or EOF), then proceed
with the existing parsing logic so no additional recursion occurs and blank-line
handling is done in-place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c7c0dfee-5bb8-4117-aec5-c3c09a6b702b

📥 Commits

Reviewing files that changed from the base of the PR and between 5556073 and 73b212e.

📒 Files selected for processing (86)
  • .buildkite/pipeline.yml
  • README.md
  • canbus-J1939/src/main/java/io/mapsmessaging/canbus/j1939/CanId.java
  • canbus-J1939/src/main/java/io/mapsmessaging/canbus/j1939/CanIdBuilder.java
  • canbus-J1939/src/test/java/io/mapsmessaging/canbus/j1939/CanIdBuilderTest.java
  • canbus-core/pi_setup.md
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/CanCapabilities.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/CanDevice.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/JnaInterfaceIndexResolver.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/JnaLibCFacade.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/LibCFacade.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/SerialCanDevice.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/SocketCanDevice.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/CanFrameStreamCodec.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/SlcanBitRate.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodec.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/impl/WaveshareUsbCanAStreamCodec.java
  • canbus-core/src/main/java/io/mapsmessaging/canbus/device/frames/CanFrame.java
  • canbus-core/src/main/script/can0.service
  • canbus-core/src/main/script/can1.service
  • canbus-core/src/test/java/io/mapsmessaging/canbus/app/CanLoad.java
  • canbus-core/src/test/java/io/mapsmessaging/canbus/app/Vcan0ReadWriteDemo.java
  • canbus-core/src/test/java/io/mapsmessaging/canbus/device/JnaInterfaceIndexResolverTest.java
  • canbus-core/src/test/java/io/mapsmessaging/canbus/device/SocketCanDeviceTest.java
  • canbus-core/src/test/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodecTest.java
  • canbus-core/src/test/java/io/mapsmessaging/canbus/device/codec/WaveshareUsbCanAStreamCodecTest.java
  • canbus-core/src/test/java/io/mapsmessaging/canbus/device/frames/CanFrameTest.java
  • canbus-events/pom.xml
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/CanaerospaceFrameParser.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/DataTypeCodec.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/ParsedCanaerospaceMessage.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/ResolvedIdentifier.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/CanaerospaceSchema.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/CanaerospaceSchemaRegistry.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/DataTypeEntry.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/DataTypesDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/FrameDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/FrameDlcDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/HexRange.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/IdRange.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/IdentifierDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/IdentifierRangeDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/MessageTypeDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NativeFormatChannel.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeIdExample.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServiceChannelDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServiceCodeDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServiceFormatDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServiceMessageFormat.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NodeServicesDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NumericRange.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/NumericRangeInteger.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/PayloadFieldDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/ResolvedCanDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/SchemaMeta.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/ServiceCodeDefinition.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/TimeSlotAllocationExample.java
  • canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/schema/ValidationDefinition.java
  • canbus-events/src/main/resources/canaerospace_schema.yaml
  • canbus-events/src/test/java/io/mapsmessaging/canbus/canaerospace/parser/CanaerospaceFrameParserTest.java
  • canbus-events/src/test/java/io/mapsmessaging/canbus/canaerospace/parser/CanaerospaceSchemaRoundTripParserTest.java
  • canbus-events/src/test/java/io/mapsmessaging/canbus/canaerospace/schema/CanaerospaceSchemaRegistryTest.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/FieldValueSource.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/LookupProcessor.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/N2kMessageParser.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/NumericProcessor.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/Processor.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/ReservedProcessor.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/StringLauProcessor.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/StringProcessor.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/N2kCompiledField.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/N2kCompiler.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/framing/FramePacker.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/framing/message/KnownMessage.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/schema/N2kJsonSchemaGenerator.java
  • canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/schema/N2kSchemaRegistry.java
  • canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/BaseTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kCodecRoundTripTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kJsonSchemaValidationTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripCanboatLogFramingTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripCanboatLogTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/codec/N2kMessageParserFieldValueSourceTest.java
  • canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/schema/N2kJsonSchemaGeneratorTest.java
  • pom.xml
💤 Files with no reviewable changes (1)
  • .buildkite/pipeline.yml

Comment thread canbus-core/pi_setup.md

Example using **500 kbit/s**:

```bash
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resolve markdownlint warnings (MD003/MD040) before release docs freeze.

Static analysis flags heading style inconsistency at Line 76 and fenced blocks without language identifiers in this section.

Suggested cleanup
  • Convert the non-ATX heading at Line 76 to ##/### style.
  • Add explicit languages (for example bash/ini) to the flagged fenced code blocks.

Also applies to: 95-135

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 76-76: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@canbus-core/pi_setup.md` at line 76, Convert the non-ATX heading in the
affected markdown section to an ATX-style heading (use ## or ###) and add
explicit language identifiers to all fenced code blocks (e.g., ```bash, ```ini)
in the same section; apply the same fixes to the subsequent fenced blocks in
that section so MD003/MD040 warnings are resolved.

Comment thread canbus-core/pi_setup.md
Comment on lines +110 to +115
ExecStart=/sbin/ip link set can0 up type can bitrate 500000
ExecStart=/sbin/ip link set can1 up type can bitrate 500000

ExecStop=/sbin/ip link set can0 down
ExecStop=/sbin/ip link set can1 down

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align systemd example runtime options with the shipped services.

Lines 110–115 omit restart-ms and txqueuelen used in can0.service/can1.service, so behavior differs between “doc path” and “artifact path.”

💡 Suggested edit
-ExecStart=/sbin/ip link set can0 up type can bitrate 500000
-ExecStart=/sbin/ip link set can1 up type can bitrate 500000
+ExecStart=/sbin/ip link set can0 down
+ExecStart=/sbin/ip link set can0 up type can bitrate 500000 restart-ms 100
+ExecStart=/sbin/ip link set can0 txqueuelen 5000
+ExecStart=/sbin/ip link set can1 down
+ExecStart=/sbin/ip link set can1 up type can bitrate 500000 restart-ms 100
+ExecStart=/sbin/ip link set can1 txqueuelen 5000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ExecStart=/sbin/ip link set can0 up type can bitrate 500000
ExecStart=/sbin/ip link set can1 up type can bitrate 500000
ExecStop=/sbin/ip link set can0 down
ExecStop=/sbin/ip link set can1 down
ExecStart=/sbin/ip link set can0 down
ExecStart=/sbin/ip link set can0 up type can bitrate 500000 restart-ms 100
ExecStart=/sbin/ip link set can0 txqueuelen 5000
ExecStart=/sbin/ip link set can1 down
ExecStart=/sbin/ip link set can1 up type can bitrate 500000 restart-ms 100
ExecStart=/sbin/ip link set can1 txqueuelen 5000
ExecStop=/sbin/ip link set can0 down
ExecStop=/sbin/ip link set can1 down
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@canbus-core/pi_setup.md` around lines 110 - 115, The example systemd
ExecStart lines in pi_setup.md (the ExecStart entries that bring up can0 and
can1) are missing the same runtime options used in the shipped
can0.service/can1.service (restart-ms and txqueuelen); update the ExecStart
commands in pi_setup.md to include the same restart-ms=<value> and
txqueuelen=<value> options used by can0.service/can1.service so the documented
behavior matches the shipped services.

Comment on lines +70 to +86
int canIdentifier = Integer.parseUnsignedInt(trimmed.substring(1, 1 + identifierLength), 16);
int dataLengthCode = Character.digit(trimmed.charAt(1 + identifierLength), 16);
if (dataLengthCode < 0 || dataLengthCode > 8) {
throw new IOException("Invalid SLCAN DLC: " + trimmed);
}

int expectedLength = 1 + identifierLength + 1 + (dataLengthCode * 2);
if (trimmed.length() != expectedLength) {
throw new IOException("Invalid SLCAN frame length: " + trimmed);
}

byte[] data = new byte[dataLengthCode];
int offset = 1 + identifierLength + 1;
for (int index = 0; index < dataLengthCode; index++) {
int start = offset + (index * 2);
data[index] = (byte) Integer.parseInt(trimmed.substring(start, start + 2), 16);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Uncaught NumberFormatException for malformed hex input.

Integer.parseUnsignedInt and Integer.parseInt throw NumberFormatException for invalid hex strings, which would propagate as an unchecked exception rather than the expected IOException.

🐛 Suggested fix to wrap hex parsing exceptions
-    int canIdentifier = Integer.parseUnsignedInt(trimmed.substring(1, 1 + identifierLength), 16);
+    int canIdentifier;
+    try {
+      canIdentifier = Integer.parseUnsignedInt(trimmed.substring(1, 1 + identifierLength), 16);
+    } catch (NumberFormatException e) {
+      throw new IOException("Invalid SLCAN identifier: " + trimmed, e);
+    }
     int dataLengthCode = Character.digit(trimmed.charAt(1 + identifierLength), 16);
     if (dataLengthCode < 0 || dataLengthCode > 8) {
       throw new IOException("Invalid SLCAN DLC: " + trimmed);
     }
 
     int expectedLength = 1 + identifierLength + 1 + (dataLengthCode * 2);
     if (trimmed.length() != expectedLength) {
       throw new IOException("Invalid SLCAN frame length: " + trimmed);
     }
 
     byte[] data = new byte[dataLengthCode];
     int offset = 1 + identifierLength + 1;
     for (int index = 0; index < dataLengthCode; index++) {
       int start = offset + (index * 2);
-      data[index] = (byte) Integer.parseInt(trimmed.substring(start, start + 2), 16);
+      try {
+        data[index] = (byte) Integer.parseInt(trimmed.substring(start, start + 2), 16);
+      } catch (NumberFormatException e) {
+        throw new IOException("Invalid SLCAN data byte at index " + index + ": " + trimmed, e);
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
int canIdentifier = Integer.parseUnsignedInt(trimmed.substring(1, 1 + identifierLength), 16);
int dataLengthCode = Character.digit(trimmed.charAt(1 + identifierLength), 16);
if (dataLengthCode < 0 || dataLengthCode > 8) {
throw new IOException("Invalid SLCAN DLC: " + trimmed);
}
int expectedLength = 1 + identifierLength + 1 + (dataLengthCode * 2);
if (trimmed.length() != expectedLength) {
throw new IOException("Invalid SLCAN frame length: " + trimmed);
}
byte[] data = new byte[dataLengthCode];
int offset = 1 + identifierLength + 1;
for (int index = 0; index < dataLengthCode; index++) {
int start = offset + (index * 2);
data[index] = (byte) Integer.parseInt(trimmed.substring(start, start + 2), 16);
}
int canIdentifier;
try {
canIdentifier = Integer.parseUnsignedInt(trimmed.substring(1, 1 + identifierLength), 16);
} catch (NumberFormatException e) {
throw new IOException("Invalid SLCAN identifier: " + trimmed, e);
}
int dataLengthCode = Character.digit(trimmed.charAt(1 + identifierLength), 16);
if (dataLengthCode < 0 || dataLengthCode > 8) {
throw new IOException("Invalid SLCAN DLC: " + trimmed);
}
int expectedLength = 1 + identifierLength + 1 + (dataLengthCode * 2);
if (trimmed.length() != expectedLength) {
throw new IOException("Invalid SLCAN frame length: " + trimmed);
}
byte[] data = new byte[dataLengthCode];
int offset = 1 + identifierLength + 1;
for (int index = 0; index < dataLengthCode; index++) {
int start = offset + (index * 2);
try {
data[index] = (byte) Integer.parseInt(trimmed.substring(start, start + 2), 16);
} catch (NumberFormatException e) {
throw new IOException("Invalid SLCAN data byte at index " + index + ": " + trimmed, e);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/main/java/io/mapsmessaging/canbus/device/codec/SlcanCanFrameStreamCodec.java`
around lines 70 - 86, The hex parsing calls in SlcanCanFrameStreamCodec (the
Integer.parseUnsignedInt for the CAN identifier and the Integer.parseInt inside
the data byte loop) can throw NumberFormatException and should be caught and
rethrown as IOException; wrap the identifier parse and the per-byte parse in
try/catch blocks that catch NumberFormatException and throw new IOException with
a clear message including the offending trimmed string (and index for the byte
loop) so callers receive a checked IOException instead of an unchecked
NumberFormatException.

Comment on lines +163 to +169
@Test
void constructor_rejectsInvalidDataLengthCode() {
Assertions.assertThrows(
IllegalArgumentException.class,
() -> new CanFrame(0x123, false, 9, new byte[8])
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test name is misleading — this fails on data length, not DLC validation.

The test name suggests it validates DLC bounds, but with dataLengthCode=9 and data.length=8, the exception is thrown by validateData (because data.length < dataLengthCode) rather than validateDataLengthCode (which allows 0-64). The test passes, but the reason differs from what the name implies.

Consider either:

  1. Renaming to constructor_rejectsDataShorterThanDlc_classic (but that duplicates line 179-185), or
  2. Testing actual DLC bounds: new CanFrame(0x123, false, 65, new byte[65]) to exceed the 64-byte limit.
Suggested fix to test actual DLC validation
   `@Test`
-  void constructor_rejectsInvalidDataLengthCode() {
+  void constructor_rejectsDataLengthCodeAbove64() {
     Assertions.assertThrows(
         IllegalArgumentException.class,
-        () -> new CanFrame(0x123, false, 9, new byte[8])
+        () -> new CanFrame(0x123, false, 65, new byte[65])
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-core/src/test/java/io/mapsmessaging/canbus/device/frames/CanFrameTest.java`
around lines 163 - 169, The test name is misleading because the thrown exception
comes from validateData (data.length < dataLengthCode) not
validateDataLengthCode; update the test in CanFrameTest so it actually validates
DLC bounds by calling new CanFrame(0x123, false, 65, new byte[65]) to trigger
the 0-64 check in validateDataLengthCode, or alternatively rename the test to
reflect the real failure (e.g., constructor_rejectsDataShorterThanDlc) and keep
the existing call that exercises validateData; reference CanFrame, validateData,
and validateDataLengthCode when making the change.

Comment on lines +47 to +67
if (resolvedIdentifier != null && resolvedIdentifier.getDataType() != null) {
expectedDataTypeName = resolvedIdentifier.getDataType();

raw = DataTypeCodec.decode(expectedDataTypeName, dataBytes);

Double resolution = resolvedIdentifier.getResolution();
if (resolution != null && raw instanceof Number number) {
engineeringValue = number.doubleValue() * resolution;
} else if (raw instanceof Float floatValue) {
engineeringValue = floatValue.doubleValue();
} else if (raw instanceof Double doubleValue) {
engineeringValue =doubleValue;
}
}

Optional<String> payloadDataTypeNameOptional = registry.findDataTypeNameByNumber(payloadDataTypeNumber);
String payloadDataTypeName = payloadDataTypeNameOptional.orElse(null);

boolean dataTypeMismatch = false;
if (expectedDataTypeName != null && payloadDataTypeName != null) {
dataTypeMismatch = !expectedDataTypeName.equals(payloadDataTypeName);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Decode using the payload’s declared datatype before checking for mismatches.

payload[1] is the wire datatype, but this method decodes rawValue with resolvedIdentifier.getDataType() and only afterwards sets dataTypeMismatch. When the payload type differs from the schema type, callers get a decoded value produced by the wrong codec.

Suggested direction
-    Object raw = null;
-    Double engineeringValue = null;
-    String expectedDataTypeName = null;
+    Object raw = null;
+    Double engineeringValue = null;
+    String expectedDataTypeName = null;
+    String payloadDataTypeName = registry.findDataTypeNameByNumber(payloadDataTypeNumber).orElse(null);

     if (resolvedIdentifier != null && resolvedIdentifier.getDataType() != null) {
       expectedDataTypeName = resolvedIdentifier.getDataType();
-
-      raw = DataTypeCodec.decode(expectedDataTypeName, dataBytes);
+      String decodeTypeName = payloadDataTypeName != null ? payloadDataTypeName : expectedDataTypeName;
+      raw = decodeTypeName == null ? null : DataTypeCodec.decode(decodeTypeName, dataBytes);
       ...
     }
-
-    Optional<String> payloadDataTypeNameOptional = registry.findDataTypeNameByNumber(payloadDataTypeNumber);
-    String payloadDataTypeName = payloadDataTypeNameOptional.orElse(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-events/src/main/java/io/mapsmessaging/canbus/canaerospace/parser/CanaerospaceFrameParser.java`
around lines 47 - 67, The code decodes using resolvedIdentifier.getDataType()
before checking the payload's declared datatype, so when payload[1] differs from
the schema you end up decoding with the wrong codec; change the flow to first
resolve payloadDataTypeName via
registry.findDataTypeNameByNumber(payloadDataTypeNumber) and use that (if
present) as the datatype passed to DataTypeCodec.decode, falling back to
resolvedIdentifier.getDataType() only when the payload type is null, then set
dataTypeMismatch by comparing expectedDataTypeName (from resolvedIdentifier) to
payloadDataTypeName, and finally apply resolution from resolvedIdentifier (if
any) to numeric raw to compute engineeringValue (so keep handling of resolution
and numeric conversions using raw, engineeringValue,
resolvedIdentifier.getResolution(), and DataTypeCodec.decode).

Comment on lines +9 to +31
public int pack(N2kCompiledField field, byte[] payload, int cursor, JsonObject decoded) {
if (!decoded.has(field.getId()) || decoded.get(field.getId()).isJsonNull()) {
return cursor;
}

double numericValue = decoded.get(field.getId()).getAsDouble();
return writeNumericValue(field, payload, cursor, numericValue);
}

@Override
public int pack(N2kCompiledField field, byte[] payload, int cursor, FieldValueSource source) {
Double numericValue = source.getDouble(field.getId());
if (numericValue != null) {
return writeNumericValue(field, payload, cursor, numericValue);
}

Long longValue = source.getLong(field.getId());
if (longValue != null) {
return writeNumericValue(field, payload, cursor, longValue.doubleValue());
}

return cursor;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Advance the cursor when a cursor-relative numeric field is missing.

For non-fixed fields, the no-value path currently returns the incoming cursor. computePayloadLength(...) still allocates this field’s bytes, so every following cursor-based field gets packed into the wrong offset when the numeric value is omitted.

Suggested fix
   public int pack(N2kCompiledField field, byte[] payload, int cursor, JsonObject decoded) {
     if (!decoded.has(field.getId()) || decoded.get(field.getId()).isJsonNull()) {
-      return cursor;
+      return field.isCompileTimeFixed() ? cursor : cursor + computeRelativeBytes(field);
     }

     double numericValue = decoded.get(field.getId()).getAsDouble();
     return writeNumericValue(field, payload, cursor, numericValue);
   }
@@
   public int pack(N2kCompiledField field, byte[] payload, int cursor, FieldValueSource source) {
     Double numericValue = source.getDouble(field.getId());
     if (numericValue != null) {
       return writeNumericValue(field, payload, cursor, numericValue);
     }

     Long longValue = source.getLong(field.getId());
     if (longValue != null) {
       return writeNumericValue(field, payload, cursor, longValue.doubleValue());
     }

-    return cursor;
+    return field.isCompileTimeFixed() ? cursor : cursor + computeRelativeBytes(field);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/codec/NumericProcessor.java`
around lines 9 - 31, When a numeric field is missing the pack methods currently
return the incoming cursor, but computePayloadLength(...) still reserves bytes
for non-fixed (cursor-relative) fields; update both pack(N2kCompiledField,
byte[], int, JsonObject) and pack(N2kCompiledField, byte[], int,
FieldValueSource) so that when a value is absent you advance the cursor by the
allocated size for that field (e.g., add cursor += computePayloadLength(field)
or cursor += field.getAllocatedByteLength()) for non-fixed/cursor-relative
fields instead of returning the original cursor. Ensure you only advance for the
no-value path and keep existing behavior when a value is written by
writeNumericValue(...).

Comment on lines +103 to +105
if (!reserved && id == null || id.isBlank() ||!seenIds.add(id)) {
return isPositionFixed;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix operator precedence in the id validation condition.

The condition !reserved && id == null || id.isBlank() ||!seenIds.add(id) has unclear precedence. Due to && binding tighter than ||, this evaluates as ((!reserved && id == null) || id.isBlank() || !seenIds.add(id)).

If reserved is true, the first clause is false, but id.isBlank() or the duplicate check can still trigger a return. This might cause issues if id is null (NPE on id.isBlank()).

🔧 Proposed fix with explicit grouping and null-safety
-    if (!reserved && id == null || id.isBlank() ||!seenIds.add(id)) {
+    if (!reserved && (id == null || id.isBlank() || !seenIds.add(id))) {
       return isPositionFixed;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/N2kCompiler.java`
around lines 103 - 105, The current compound condition in the N2kCompiler method
mixes && and || and can cause a NullPointerException when id is null; update the
guard to first check reserved and then group the id checks with null-safety so
id.isBlank() is only called when id != null. Concretely, change the condition
involving reserved, id, and seenIds (the current expression using reserved, id,
id.isBlank(), and seenIds.add(id)) to use explicit grouping and short-circuit
null checks (e.g., ensure id == null is evaluated before id.isBlank() and wrap
the three id-related checks together) so the return uses isPositionFixed only
when !reserved AND one of the grouped id conditions is true.

Comment on lines +61 to +70
<Field>
<Order>2</Order>
<Id>reserved</Id>
<Name>Reserved</Name>
<BitLength>56</BitLength>
<BitOffset>8</BitOffset>
<BitStart>0</BitStart>
<FieldType>BINARY</FieldType>
<TypeInPdf>reserved</TypeInPdf>
</Field>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Find FieldType enum/definitions"
rg -n --type=java 'enum\s+FieldType|class\s+.*FieldType'

echo
echo "2) Find parser/encoder handling for RESERVED/BINARY/NUMBER"
rg -n --type=java 'case\s+(RESERVED|BINARY|NUMBER)|FieldType\.(RESERVED|BINARY|NUMBER)'

echo
echo "3) Spot reserved-bit handling logic"
rg -n --type=java 'reserved|padding|skip|ignore'

Repository: Maps-Messaging/canbus_interface

Length of output: 110


🏁 Script executed:

find . -name "NMEA_database_1_300.xml" -type f

Repository: Maps-Messaging/canbus_interface

Length of output: 140


🏁 Script executed:

ls -la canbus-nmea2000/ 2>/dev/null | head -20 || find . -type f -name "*.xml" | head -20

Repository: Maps-Messaging/canbus_interface

Length of output: 356


🏁 Script executed:

cat -n ./canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml | sed -n '55,75p'

Repository: Maps-Messaging/canbus_interface

Length of output: 792


🏁 Script executed:

cat -n ./canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml | sed -n '148,170p'

Repository: Maps-Messaging/canbus_interface

Length of output: 947


🏁 Script executed:

# Check all unique FieldType values in the file
rg -o '<FieldType>[^<]+</FieldType>' ./canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml | sort | uniq -c | sort -rn

Repository: Maps-Messaging/canbus_interface

Length of output: 350


🏁 Script executed:

# Search for all reserved field definitions
rg -B2 -A10 '<Id>reserved</Id>' ./canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml

Repository: Maps-Messaging/canbus_interface

Length of output: 45545


Standardize reserved field types to RESERVED.

Lines 68 and 164 use inconsistent FieldType values for reserved bits: BINARY and NUMBER respectively. The schema defines 131 reserved fields as RESERVED type; only these 2 instances deviate. The NUMBER field at line 164 also carries extraneous properties (Resolution, Signed, RangeMin, RangeMax) inappropriate for reserved bits.

🔧 Proposed XML fixes

Line 68: Change <FieldType>BINARY</FieldType> to <FieldType>RESERVED</FieldType>

Lines 160–164: Replace with <FieldType>RESERVED</FieldType> (remove Resolution, Signed, RangeMin, RangeMax)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml` around lines
61 - 70, Two <Field> entries defined for reserved bits are using incorrect types
and extra attributes; locate the <Field> elements with Id "reserved" (one with
BitOffset 8/BitStart 0 and the other around the later reserved block) and change
their <FieldType> values to RESERVED and remove any non-applicable attributes
(Resolution, Signed, RangeMin, RangeMax) from the reserved entry so both
reserved fields match the schema's RESERVED type and contain no extraneous
properties.

Comment on lines +101 to +102
envelope.addProperty("name", msg.getId());
envelope.add("packet", decoded);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "N2kRoundTripAllPgnsTest.java" -type f

Repository: Maps-Messaging/canbus_interface

Length of output: 170


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | sed -n '90,130p'

Repository: Maps-Messaging/canbus_interface

Length of output: 1876


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | sed -n '1,50p'

Repository: Maps-Messaging/canbus_interface

Length of output: 2341


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | sed -n '43,75p'

Repository: Maps-Messaging/canbus_interface

Length of output: 1474


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | wc -l

Repository: Maps-Messaging/canbus_interface

Length of output: 79


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | sed -n '100,115p'

Repository: Maps-Messaging/canbus_interface

Length of output: 793


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | sed -n '48,56p'

Repository: Maps-Messaging/canbus_interface

Length of output: 519


🏁 Script executed:

# Check if there are any other usages of msg.getId() to understand the contract
rg "getId()" ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java -A 2 -B 2

Repository: Maps-Messaging/canbus_interface

Length of output: 759


🏁 Script executed:

# Let's check the N2kCompiledMessage class to understand the getId() contract
find . -name "N2kCompiledMessage.java" -type f | head -1

Repository: Maps-Messaging/canbus_interface

Length of output: 173


🏁 Script executed:

cat -n ./canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/N2kCompiledMessage.java | head -100

Repository: Maps-Messaging/canbus_interface

Length of output: 1698


🏁 Script executed:

cat -n ./canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java | sed -n '59,75p'

Repository: Maps-Messaging/canbus_interface

Length of output: 756


🏁 Script executed:

# Let's check if there are any messages in the registry with null IDs
# by looking at how messages are built
rg "getId()" ./canbus-nmea2000/src/main/java/io/mapsmessaging/canbus/j1939/n2k/compile/ -A 2 -B 2 | head -50

Repository: Maps-Messaging/canbus_interface

Length of output: 2103


🏁 Script executed:

# Check if there are any null IDs in the actual message definitions
# by looking for PGNs with no ID
rg "messageDefinition\\.getId()" ./canbus-nmea2000/src/main/java -A 2 -B 2

Repository: Maps-Messaging/canbus_interface

Length of output: 1453


🏁 Script executed:

# Let's look for the actual message definition files or data
find . -name "*.json" -o -name "*.xml" | grep -i "n2k\|pgn" | head -20

Repository: Maps-Messaging/canbus_interface

Length of output: 140


🏁 Script executed:

# Check if there are any messages without an ID in the NMEA database
grep -o '<Message[^>]*>' ./canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml | head -20

Repository: Maps-Messaging/canbus_interface

Length of output: 57


🏁 Script executed:

# Let's examine the structure of the XML file
head -100 ./canbus-nmea2000/src/main/resources/n2k/NMEA_database_1_300.xml

Repository: Maps-Messaging/canbus_interface

Length of output: 2895


Add null-safe handling for message ID in round-trip assertion.

At line 110, get("name").getAsString() throws IllegalStateException when msg.getId() is null (which can occur per line 53's null check). The test fails with a misleading exception instead of a proper assertion error. Guard the assertion to safely handle null IDs from the envelope round-trip.

Suggested fix
-    envelope.addProperty("name", msg.getId());
+    if (msg.getId() == null || msg.getId().isBlank()) {
+      envelope.add("name", null);
+    } else {
+      envelope.addProperty("name", msg.getId());
+    }

-    assertEquals(msg.getId(), decodedBackEnvelope.get("name").getAsString());
+    JsonElement nameElement = decodedBackEnvelope.get("name");
+    if (msg.getId() == null || msg.getId().isBlank()) {
+      assertTrue(nameElement == null || nameElement.isJsonNull());
+    } else {
+      assertNotNull(nameElement);
+      assertEquals(msg.getId(), nameElement.getAsString());
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@canbus-nmea2000/src/test/java/io/mapsmessaging/canbus/j1939/n2k/N2kRoundTripAllPgnsTest.java`
around lines 101 - 102, The test reads envelope.get("name").getAsString() which
throws when msg.getId() was null; update the round-trip assertion in
N2kRoundTripAllPgnsTest to be null-safe: after performing the round-trip, check
envelope.has("name") and !envelope.get("name").isJsonNull() before calling
getAsString() and assert equality with msg.getId(), otherwise
assertNull(msg.getId()) (or assertTrue/envelope check) so the assertion covers
both non-null IDs and null IDs produced by envelope.addProperty("name",
msg.getId()); reference the envelope object and msg.getId() usage and replace
the direct get("name").getAsString() call with this guarded check.

Comment thread pom.xml
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>21</maven.compiler.release>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read-only scan for Java version pins in common build/CI/container files.
fd -HI -t f '(pom\.xml|Dockerfile|.*\.ya?ml|\.tool-versions|\.java-version)$' \
| xargs -r rg -n --no-heading 'maven.compiler.release|java-version|setup-java|temurin|openjdk|jdk|FROM .*java|FROM .*jdk'

Repository: Maps-Messaging/canbus_interface

Length of output: 227


🏁 Script executed:

# Look for CI configuration files
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".jdk-version" -o -name ".java-version" \) | head -20

Repository: Maps-Messaging/canbus_interface

Length of output: 195


🏁 Script executed:

# Check for GitHub Actions workflows
fd -t f 'workflows' --type d

Repository: Maps-Messaging/canbus_interface

Length of output: 57


🏁 Script executed:

# Look for Dockerfile or container files
fd -t f '(Dockerfile|docker-compose.*)'

Repository: Maps-Messaging/canbus_interface

Length of output: 57


🏁 Script executed:

# Check for README or documentation
fd -t f '(README|CONTRIBUTING)' | head -10

Repository: Maps-Messaging/canbus_interface

Length of output: 156


🏁 Script executed:

cat -n .buildkite/pipeline.yml

Repository: Maps-Messaging/canbus_interface

Length of output: 588


🏁 Script executed:

cat -n .buildkite/pipeline_release.yml

Repository: Maps-Messaging/canbus_interface

Length of output: 262


🏁 Script executed:

head -50 README.md

Repository: Maps-Messaging/canbus_interface

Length of output: 1678


🏁 Script executed:

grep -i "java\|jdk\|version" README.md | head -20

Repository: Maps-Messaging/canbus_interface

Length of output: 304


Add explicit Java 21 version enforcement to Buildkite CI pipelines.

Line 83 sets maven.compiler.release to Java 21, but the Buildkite pipeline files (.buildkite/pipeline.yml and .buildkite/pipeline_release.yml) reference only a generic java_build_queue agent without specifying Java version. This creates a mismatch risk: if agents default to Java 11 or 17, the build compiles with target=21 on a lower JVM, potentially causing runtime failures. Add explicit Java 21 version enforcement in both pipeline files (e.g., via Buildkite agent environment setup or a versioned container image), and document the Java 21 requirement in README.md.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` at line 83, pom.xml sets maven.compiler.release to 21 but Buildkite
pipelines don't enforce Java 21; update .buildkite/pipeline.yml and
.buildkite/pipeline_release.yml to ensure agents run Java 21 (for example select
a Java 21 container image, set an agent environment variable like JAVA_HOME
pointing to Java 21, or use a versioned build queue that guarantees Java 21) so
builds use the same JVM target as the maven.compiler.release setting, and add a
short note to README.md documenting the Java 21 requirement and any agent
selection instructions; reference maven.compiler.release in the README to
explain why Java 21 is required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants