voltage_j1939

Crates.iovoltage_j1939
lib.rsvoltage_j1939
version0.1.0
created_at2025-12-19 03:05:35.518944+00
updated_at2025-12-19 03:05:35.518944+00
descriptionSAE J1939 protocol decoder - PGN/SPN database and CAN frame parsing
homepage
repositoryhttps://github.com/EvanL1/voltage-j1939
max_upload_size
id1994098
size66,735
(EvanL1)

documentation

README

voltage_j1939

Crates.io Documentation License

SAE J1939 protocol decoder for Rust. Provides PGN/SPN database and CAN frame parsing for heavy-duty vehicles and industrial equipment.

Features

  • Zero dependencies - Pure Rust, no external crates required
  • Built-in SPN database - 60+ SPNs across 12+ PGNs for engine/generator monitoring
  • CAN ID parsing - Parse and build 29-bit extended J1939 CAN IDs
  • Bit-level decoding - Extract values with scale, offset, and bit field support
  • "Not available" detection - Automatic handling of J1939 special values (0xFF, 0xFFFF, etc.)

Installation

[dependencies]
voltage_j1939 = "0.1"

Quick Start

use voltage_j1939::{decode_frame, parse_can_id};

// Parse a J1939 CAN frame (EEC1 from SA=0x00)
let can_id = 0x0CF00400u32;
let data = [0x00, 0x00, 0x00, 0x20, 0x4E, 0x00, 0x00, 0x00];

// Decode all SPNs in the frame
let decoded = decode_frame(can_id, &data);
for spn in decoded {
    println!("{}: {} {}", spn.name, spn.value, spn.unit);
}

// Parse CAN ID components
let id = parse_can_id(can_id);
println!("PGN: {}, SA: 0x{:02X}", id.pgn, id.source_address);

Decoding Individual SPNs

use voltage_j1939::{decode_spn, get_spn_def};

// Get SPN definition for Engine Coolant Temperature
let spn_def = get_spn_def(110).unwrap();

// Decode from raw CAN data
let data = [130u8, 0, 0, 0, 0, 0, 0, 0];  // Raw value 130
if let Some(value) = decode_spn(&data, spn_def) {
    // value = 130 * 1.0 + (-40) = 90°C
    println!("Coolant temp: {}°C", value);
}

Supported PGNs

PGN Name Description
61444 EEC1 Electronic Engine Controller 1
61443 EEC2 Electronic Engine Controller 2
65270 EEC3 Electronic Engine Controller 3
65262 ET1 Engine Temperature 1
65263 EFL/P1 Engine Fluid Level/Pressure 1
65270 IC1 Inlet/Exhaust Conditions 1
65271 VEP1 Vehicle Electrical Power 1
65269 AMB Ambient Conditions
65266 LFE Fuel Economy
65253 HOURS Engine Hours/Revolutions
65257 FC Fuel Consumption
65259 VH Vehicle Hours
65276 DD Dash Display
65265 CCVS Cruise Control/Vehicle Speed

J1939 CAN ID Format

J1939 uses 29-bit extended CAN IDs:

| Priority | R | DP | PF | PS/DA | SA |
|   3 bit  |1b | 1b | 8b |  8b   | 8b |
  • Priority: Message priority (0-7, lower is higher)
  • DP: Data Page (0 or 1)
  • PF: PDU Format (determines PDU1 vs PDU2)
  • PS/DA: PDU Specific or Destination Address
  • SA: Source Address

PDU Format

  • PDU1 (PF < 240): Peer-to-peer messages, PS is destination address
  • PDU2 (PF >= 240): Broadcast messages, PS is part of PGN

Building Request PGN Frames

use voltage_j1939::build_request_pgn;

// Request Engine Hours (PGN 65253) from ECU at address 0x00
let (can_id, data) = build_request_pgn(0xFE, 0x00, 65253);
// can_id = 0x18EA00FE
// data = [0xE5, 0xFE, 0x00] (PGN in little-endian)

Database Statistics

use voltage_j1939::{database_stats, list_supported_pgns};

let (spn_count, pgn_count) = database_stats();
println!("Database: {} SPNs across {} PGNs", spn_count, pgn_count);

for pgn in list_supported_pgns() {
    println!("PGN {}", pgn);
}

Integration with socketcan

This crate handles J1939 protocol decoding only. For CAN bus communication, use socketcan or similar:

use socketcan::CanSocket;
use voltage_j1939::{decode_frame, parse_can_id};

let socket = CanSocket::open("can0")?;
loop {
    let frame = socket.read_frame()?;
    if frame.is_extended() {
        let id = parse_can_id(frame.id());
        let decoded = decode_frame(frame.id(), frame.data());
        for spn in decoded {
            println!("[SA=0x{:02X}] {}: {} {}",
                id.source_address, spn.name, spn.value, spn.unit);
        }
    }
}

License

Licensed under either of:

at your option.

Commit count: 0

cargo fmt