Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
163c05a
cleanup
mbuckton Jan 24, 2026
e10eca7
add tests and doc
mbuckton Jan 31, 2026
01175af
fix the tests
mbuckton Jan 31, 2026
8efb54e
spam the link
mbuckton Jan 31, 2026
6700fbc
Merge branch 'development' of https://github.com/Maps-Messaging/canbu…
mbuckton Feb 1, 2026
eef23a0
feat(canbus): Merge to 1 canbus project
mbuckton Feb 1, 2026
f556154
docs(canbus): Add Raspberry Pi CAN Bus setup guide for dual MCP2515 H…
mbuckton Feb 3, 2026
0e7a101
refactor(canbus): Simplify and enhance `CanId` and `CanIdBuilder` by …
mbuckton Feb 6, 2026
528d581
test(canbus): Add tests for `CanIdBuilder` validation and priority ha…
mbuckton Feb 6, 2026
43b9c18
refactor(canbus): Replace `CanFrame` class with `record`, update refe…
mbuckton Feb 8, 2026
eee2be6
feat(canbus): Add `name` property to N2kMessageParser envelope
mbuckton Feb 9, 2026
06e0cf2
Merge remote-tracking branch 'origin/development' into development
mbuckton Feb 9, 2026
05a4450
feat(canbus): Add `name` property to N2kMessageParser envelope
mbuckton Feb 9, 2026
8a69a5c
refactor(n2k): Rename `decoded` to `packet` and update schema, tests,…
mbuckton Feb 9, 2026
b36187b
added deepwiki badge
krital Feb 5, 2026
85f2aaf
fix(api) reduce any issues with accessing the device
mbuckton Mar 2, 2026
8953d5e
refactor(canbus): Move `Vcan0ReadWriteDemo` to `app` package, update …
mbuckton Mar 2, 2026
c1d117b
test(canbus): Add tests for `CanLoad` and `Vcan0ReadWriteDemo`, adjus…
mbuckton Mar 2, 2026
b95c9d2
test(canbus): Add unit tests for `JnaInterfaceIndexResolver` to cover…
mbuckton Mar 2, 2026
cbd3d6a
refactor(canbus): Extract `validateWriteArgs` method for argument val…
mbuckton Mar 2, 2026
f9252f5
Merge pull request #2 from Maps-Messaging/canbus-api-fix
mbuckton Mar 3, 2026
0aa3c63
feat(canbus-events): Add CANaerospace schema, parser implementation, …
mbuckton Mar 4, 2026
7bbb321
feat(canbus): Add systemd service files to enable and configure can0 …
mbuckton Mar 4, 2026
3dbd385
Merge pull request #3 from Maps-Messaging/canbus-api-fix
mbuckton Mar 4, 2026
623903e
feat(canaerospace): Refactor schema registry to improve identifier lo…
mbuckton Mar 4, 2026
ebac563
test(canbus-events): Add round-trip parser unit tests for CANaerospac…
mbuckton Mar 4, 2026
22ac4cf
test(canbus-events): Add round-trip parser unit tests for CANaerospac…
mbuckton Mar 19, 2026
64231c9
feat(canaerospace): Implement `DataTypeCodec.encode` for schema-based…
mbuckton Mar 20, 2026
d9b01db
refactor(canbus): Optimize frame reading by introducing reusable buff…
mbuckton Mar 28, 2026
449f4e1
feat(nmea2000): Add `FieldValueSource` interface and support for enco…
mbuckton Apr 15, 2026
2d22507
Merge remote-tracking branch 'origin/development' into development
mbuckton Apr 15, 2026
7261956
fix(api) expose the pack for a byte[]
mbuckton Apr 16, 2026
ffba607
Merge remote-tracking branch 'origin/development' into development
mbuckton Apr 16, 2026
fead556
feat(canbus): Add `SerialCanDevice` and SLCAN codec implementations w…
mbuckton Apr 19, 2026
35592c0
feat(canbus): Add support for CAN FD frames and extend validation logic
mbuckton Apr 19, 2026
351fcbc
feat(nmea2000): Extend NMEA database with definitions for Heartbeat a…
mbuckton Apr 20, 2026
887bcd1
feat(nmea2000): Add ISO Request PGN (`59904`) with detailed field def…
mbuckton Apr 20, 2026
912fc4e
feat(nmea2000): Add support for `STRING_LAU` field type and enhance f…
mbuckton Apr 20, 2026
4e7f3dc
feat(canbus): Add `WaveshareUsbCanAStreamCodec` for encoding and deco…
mbuckton Apr 25, 2026
a507f99
Merge remote-tracking branch 'origin/development' into development
mbuckton Apr 25, 2026
63c522c
test(canbus): Add unit tests for `WaveshareUsbCanAStreamCodec`
mbuckton Apr 25, 2026
6eb1740
feat(canbus): Introduce `CanDevice` interface and update `SocketCanDe…
mbuckton Apr 26, 2026
73b212e
feat(canbus): Add `getCanCapabilities` method to `CanDevice` and impl…
mbuckton Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ steps:
- export SONAR_TOKEN=$(buildkite-agent secret get SONAR_TOKEN)
- export NVD_API_KEY=$(buildkite-agent secret get nvd_api_key)
- mvn -Dgpg.skip=true clean deploy -Psnapshot
- mvn sonar:sonar -Dsonar.login=${SONAR_TOKEN}
plugins:
- test-collector#v1.0.0:
files: "target/surefire-reports/*.xml"
format: "junit"
env:
BUILDKITE_ANALYTICS_TOKEN: "$(buildkite-agent secret get SchemasBuildkiteAnalytics)"
SONAR_TOKEN: "${SONAR_TOKEN}"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Dependencies:

NMEA 2000 is treated as a **specialised J1939 profile**, preserving correct layering and reuse.

Want to dive deeper?
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Maps-Messaging/canbus_interface)
---

## Current Capabilities
Expand Down
92 changes: 56 additions & 36 deletions canbus-J1939/src/main/java/io/mapsmessaging/canbus/j1939/CanId.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,80 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.mapsmessaging.canbus.j1939;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class CanId {

private static final int EXTENDED_ID_MASK = 0x1FFFFFFF;

private static final int PRIORITY_SHIFT = 26;
private static final int PRIORITY_MASK = 0x7;

private static final int DATA_PAGE_SHIFT = 24;
private static final int DATA_PAGE_MASK = 0x1;

private static final int PDU_FORMAT_SHIFT = 16;
private static final int BYTE_MASK = 0xFF;

private static final int PDU1_MAX_PF = 239;
private static final int BROADCAST_ADDRESS = 255;

private final int priority;
private final int pgn;
private final int sourceAddress;
private final int destinationAddress;

private CanId(int priority, int pgn, int sourceAddress, int destinationAddress) {
this.priority = priority;
this.pgn = pgn;
this.sourceAddress = sourceAddress;
this.destinationAddress = destinationAddress;
}

/**
* Parse a 29-bit extended CAN identifier used by NMEA 2000.
*
* Layout (J1939 style):
* - Priority: bits 26..28 (3 bits)
* - PGN: derived from PF/PS/DP
* - Source: bits 0..7
*
* PGN rules:
* - PF < 240 (PDU1): PGN uses PF and DP, PS is destination and excluded from PGN (low byte becomes 0)
* - PF >= 240 (PDU2): PGN includes PF and PS, destination is "global" (255)
*/
public static CanId parse(int canIdentifier) {
int identifier = canIdentifier & 0x1FFFFFFF;
int identifier = canIdentifier & EXTENDED_ID_MASK;

int priority = (identifier >> 26) & 0x07;
int pf = (identifier >> 16) & 0xFF;
int ps = (identifier >> 8) & 0xFF;
int source = identifier & 0xFF;
int dataPage = (identifier >> 24) & 0x01;
int priority = (identifier >> PRIORITY_SHIFT) & PRIORITY_MASK;
int pduFormat = (identifier >> PDU_FORMAT_SHIFT) & BYTE_MASK;
int pduSpecific = (identifier >> 8) & BYTE_MASK;
int sourceAddress = identifier & BYTE_MASK;
int dataPage = (identifier >> DATA_PAGE_SHIFT) & DATA_PAGE_MASK;

int pgn;
int destination;
int destinationAddress;

if (pf < 240) {
// PDU1
destination = ps;
pgn = (dataPage << 16) | (pf << 8);
if (pduFormat <= PDU1_MAX_PF) {
destinationAddress = pduSpecific;
pgn = (dataPage << 16) | (pduFormat << 8);
} else {
// PDU2
destination = 255;
pgn = (dataPage << 16) | (pf << 8) | ps;
destinationAddress = BROADCAST_ADDRESS;
pgn = (dataPage << 16) | (pduFormat << 8) | pduSpecific;
}

return new CanId(priority, pgn, sourceAddress, destinationAddress);
}

public boolean isPdu1() {
return (pgn & BYTE_MASK) == 0;
}

public boolean isPdu2() {
return !isPdu1();
}

public boolean isBroadcast() {
return isPdu2();
}

public Integer getDestinationAddressOrNull() {
if (isPdu1()) {
return destinationAddress;
}
return null;
}

return new CanId(priority, pgn, source, destination);
public Integer getGroupExtensionOrNull() {
if (isPdu2()) {
return pgn & BYTE_MASK;
}
return null;
Comment on lines +70 to +93
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

PDU type detection is incorrect for valid PDU2 PGNs with group-extension 0x00.

isPdu1() currently infers type from PGN low byte, but PDU type is defined by PF (<=239 vs >=240). For PF >=240 and low byte 0x00, this misclassifies PDU2 as PDU1 and breaks isBroadcast(), getDestinationAddressOrNull(), and getGroupExtensionOrNull().

🐛 Suggested fix (classify by PF, not PGN low byte)
   public boolean isPdu1() {
-    return (pgn & BYTE_MASK) == 0;
+    int pduFormat = (pgn >> PDU_FORMAT_SHIFT) & BYTE_MASK;
+    return pduFormat <= PDU1_MAX_PF;
   }
@@
   public boolean isBroadcast() {
-    return isPdu2();
+    return isPdu2() || destinationAddress == BROADCAST_ADDRESS;
   }
🤖 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/CanId.java` around
lines 70 - 93, isPdu1()/isPdu2() logic is wrong because it checks low PGN byte
instead of the PF field; change isPdu1() to compute PF from pgn (e.g., (pgn >>
8) & BYTE_MASK or equivalent) and return true only when PF <= 239, make isPdu2()
return !isPdu1(), keep isBroadcast() delegating to isPdu2(), and ensure
getDestinationAddressOrNull() and getGroupExtensionOrNull() use the corrected
isPdu1()/isPdu2() behavior (use pgn & BYTE_MASK for group extension when
isPdu2(), and return destinationAddress only when isPdu1()) so PGNs with PF >=
240 and low byte 0x00 are classified as PDU2.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.mapsmessaging.canbus.j1939;

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 PDU1_MAX_PF = 239;

private CanIdBuilder() {
}

Expand All @@ -35,22 +39,29 @@ private CanIdBuilder() {
* - SA: bits 0..7
*
* PGN rules:
* - PF < 240 => PDU1 => PS is destination
* - PF < 240 => PDU1 => PS is destination, PGN low byte must be 0x00
* - PF >= 240 => PDU2 => PS comes from PGN low byte, destination implied global
*/
public static int build(int pgn, int priority, int sourceAddress, int destinationAddress) {
int prio = priority & 0x07;
if (priority < 0 || priority > PRIORITY_MASK) {
throw new IllegalArgumentException("priority must be in range 0..7, got " + priority);
}

int prio = priority & PRIORITY_MASK;
int dp = (pgn >> 16) & 0x01;
int pf = (pgn >> 8) & 0xFF;
int pf = (pgn >> 8) & BYTE_MASK;

int ps;
if (pf < 240) {
ps = destinationAddress & 0xFF;
if (pf <= PDU1_MAX_PF) {
if ((pgn & BYTE_MASK) != 0) {
throw new IllegalArgumentException("PDU1 PGN must have low byte 0x00, got 0x" + Integer.toHexString(pgn));
}
ps = destinationAddress & BYTE_MASK;
} else {
ps = pgn & 0xFF;
ps = pgn & BYTE_MASK;
}

int sa = sourceAddress & 0xFF;
int sa = sourceAddress & BYTE_MASK;

int identifier = 0;
identifier |= (prio << 26);
Expand All @@ -59,6 +70,7 @@ public static int build(int pgn, int priority, int sourceAddress, int destinatio
identifier |= (ps << 8);
identifier |= sa;

return identifier & 0x1FFFFFFF;
return identifier & EXTENDED_ID_MASK;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

class CanIdBuilderTest {

Expand Down Expand Up @@ -61,9 +60,21 @@ void build_pdu2_pfGreaterOrEqual240_usesPgnLowByteInPs() {
}

@Test
void build_priorityIsMaskedTo3Bits() {
void build_priorityOutsideRangeThrows() {
int pgn = 0x00EA00; // PF < 240 so destination is used
int priority = 0xFF; // should become 7
int priority = 0xFF; // invalid
int sourceAddress = 0x01;
int destinationAddress = 0x02;

assertThrows(IllegalArgumentException.class,
() -> CanIdBuilder.build(pgn, priority, sourceAddress, destinationAddress));
}


@Test
void build_priorityIsPreservedWhenValid() {
int pgn = 0x00EA00;
int priority = 7;
int sourceAddress = 0x01;
int destinationAddress = 0x02;

Expand All @@ -72,6 +83,7 @@ void build_priorityIsMaskedTo3Bits() {
assertEquals(7, extractPriority(id));
}


@Test
void build_sourceAndDestinationAreMaskedTo8Bits() {
int pgn = 0x00EA00; // PF < 240 => destination used
Expand Down
Loading
Loading