j1939_macros/
lib.rs

1//! # j1939-macros
2//!
3//! Procedural macros for the j1939-rs library.
4//!
5//! ## Note on `std` Usage
6//!
7//! This crate uses the standard library (`std`) and does **not** have `#![no_std]`.
8//! This is intentional and correct:
9//!
10//! - **Proc macros run at compile time** on the developer's machine, not on the embedded target
11//! - The Rust compiler **requires** proc macro crates to use `std`
12//! - Dynamic allocations (Vec, String, HashMap) are used during code generation
13//! - This code **never runs** on the embedded system
14//!
15//! The **generated code** from these macros is fully `no_std` compatible and runs on embedded
16//! targets without heap allocation. See the `j1939-rs` crate documentation for details on
17//! embedded system support.
18
19use proc_macro::TokenStream;
20use quote::quote;
21use syn::Expr::Lit;
22use syn::Meta::NameValue;
23use syn::{Data, DeriveInput, ExprLit, Fields, parse_macro_input};
24
25// Internal modules for macro implementation
26mod codegen;
27mod documentation_generation;
28mod field_info;
29mod parse;
30
31use codegen::*;
32use field_info::*;
33use parse::*;
34
35/// Attribute macro for defining J1939 message structures with automatic marshalling/unmarshalling.
36///
37/// This macro transforms a struct definition into a complete J1939 message with serialization
38/// and deserialization capabilities. It generates implementations of the `Marshall` and `Unmarshall`
39/// traits, as well as associated constants for message metadata.
40///
41/// # Parameters
42///
43/// - `pgn`: (required) Parameter Group Number - A unique identifier for the message (0-262143)
44/// - `priority`: (optional) Message priority (0-7, where 0 is highest). Default: 6
45/// - `length`: (optional) Message length in bits. Default: 64 bits (8 bytes)
46///
47/// # Field Attributes
48///
49/// Each field must have a `#[j1939(...)]` attribute with the following parameters:
50///
51/// - `bits = start..end`: (required) Bit range for this field within the message
52/// - `scale = f32`: (optional) Scaling factor for float conversion. Use with `f32` fields
53/// - `offset = f32`: (optional) Offset applied to scaled values. Default: 0.0
54///   - Formula: `raw = (value - offset) / scale`
55/// - `encoding = "type"`: (optional) Special encoding type (e.g., "q9" for fixed-point)
56/// - `unit = "string"`: (optional) Physical unit for documentation (e.g., "km/h", "°C")
57/// - `reserved`: (optional flag) Marks bits as reserved/unused
58///
59/// ## Encoding Strategies
60///
61/// The encoding strategy is automatically determined based on field type and attributes:
62///
63/// | Field Type | Attributes | Encoding | Description |
64/// |------------|-----------|----------|-------------|
65/// | `u8`, `u16`, `u32` | - | UInt | Direct unsigned integer |
66/// | `i8`, `i16`, `i32` | - | SInt | Signed integer with sign extension |
67/// | `f32` | `scale`, `offset?` | Scaled | Linear transformation: `(raw * scale) + offset` |
68/// | `f32` | `encoding = "q9"` | Q9 | Fixed-point format (10 bits: 1 sign + 9 fractional) |
69/// | Custom enum | - | Enum | Type-safe enum (requires `#[repr(u8)]` and `#[j1939_enum]`) |
70/// | `()` | `reserved` | Reserved | Unused bits set to zero |
71///
72/// ### Offset Behavior
73///
74/// When using `scale` with `offset`:
75/// - `offset != 0.0`: Raw values treated as **unsigned** (common for temperatures, percentages)
76/// - `offset == 0.0` (default): Sign extension applied for **signed** values
77///
78/// # Generated Items
79///
80/// The macro generates:
81/// - Constants: `PGN`, `PRIORITY`, `LENGTH`
82/// - `Marshall` trait implementation for encoding
83/// - `Unmarshall` trait implementation for decoding
84/// - Comprehensive documentation table with field layout
85///
86/// # Examples
87///
88/// ## Basic Message with Scaled Values
89///
90/// ```ignore
91/// use j1939_rs::prelude::*;
92///
93/// #[j1939_message(pgn = 61444, priority = 3, length = 64)]
94/// pub struct EngineSpeed {
95///     /// Engine RPM
96///     #[j1939(bits = 0..16, scale = 0.125, unit = "rpm")]
97///     pub engine_speed: f32,
98///
99///     /// Coolant temperature with offset
100///     #[j1939(bits = 16..24, scale = 1.0, offset = -40.0, unit = "°C")]
101///     pub coolant_temp: f32,
102///
103///     /// Reserved bits
104///     #[j1939(bits = 24..64, reserved)]
105///     pub reserved: (),
106/// }
107///
108/// // Usage
109/// let msg = EngineSpeed {
110///     engine_speed: 1850.0,
111///     coolant_temp: 85.0,
112///     reserved: (),
113/// };
114///
115/// let mut j1939_msg = J1939Message::default();
116/// msg.marshall(&mut j1939_msg).unwrap();
117///
118/// let decoded = EngineSpeed::unmarshall(&j1939_msg).unwrap();
119/// assert_eq!(decoded.engine_speed, 1850.0);
120/// ```
121///
122/// ## Message with Enums
123///
124/// ```ignore
125/// use j1939_rs::prelude::*;
126///
127/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
128/// #[repr(u8)]
129/// #[j1939_enum]
130/// pub enum GearPosition {
131///     Park = 0,
132///     Reverse = 1,
133///     Neutral = 2,
134///     Drive = 3,
135/// }
136///
137/// #[j1939_message(pgn = 12345, priority = 6)]
138/// pub struct TransmissionStatus {
139///     #[j1939(bits = 0..2)]
140///     pub gear: GearPosition,
141///
142///     #[j1939(bits = 2..18, scale = 0.1, unit = "km/h")]
143///     pub speed: f32,
144///
145///     #[j1939(bits = 18..64, reserved)]
146///     pub reserved: (),
147/// }
148/// ```
149///
150/// ## Message with Q9 Fixed-Point Encoding
151///
152/// ```ignore
153/// use j1939_rs::prelude::*;
154///
155/// #[j1939_message(pgn = 65400)]
156/// pub struct ControlData {
157///     /// High-precision control value using Q9 fixed-point
158///     #[j1939(bits = 0..10, encoding = "q9")]
159///     pub control_value: f32,
160///
161///     #[j1939(bits = 10..64, reserved)]
162///     pub reserved: (),
163/// }
164/// ```
165///
166/// # Notes
167///
168/// - All bit ranges must be non-overlapping and within the message length
169/// - Float fields require either `scale` or `encoding` attribute
170/// - When `offset` is non-zero, raw values are treated as unsigned
171/// - When `offset` is zero (default), sign extension is applied for negative values
172/// - Reserved bits are automatically set to zero during marshalling
173/// - Encoding types are inferred automatically from field types and attributes
174///
175/// # See Also
176///
177/// - [`j1939_enum`] - Attribute macro for registering enums used in messages
178#[proc_macro_attribute]
179pub fn j1939_message(attr: TokenStream, item: TokenStream) -> TokenStream {
180    let input = parse_macro_input!(item as DeriveInput);
181    let attr_args = parse_macro_input!(attr as MessageAttributes);
182
183    // Parse the struct
184    let struct_data = match &input.data {
185        Data::Struct(data) => data,
186        _ => {
187            return syn::Error::new_spanned(&input, "j1939_message can only be used on structs")
188                .to_compile_error()
189                .into();
190        }
191    };
192
193    let fields = match &struct_data.fields {
194        Fields::Named(fields) => fields,
195        _ => {
196            return syn::Error::new_spanned(&input, "j1939_message requires named fields")
197                .to_compile_error()
198                .into();
199        }
200    };
201
202    // Parse field attributes
203    let field_infos: Vec<FieldInfo> = match parse_fields(fields) {
204        Ok(infos) => infos,
205        Err(e) => return e.to_compile_error().into(),
206    };
207
208    // Validate fields and auto-calculate length if needed
209    let mut attr_args = attr_args;
210    if let Err(e) = validate_fields(&field_infos, &mut attr_args) {
211        return e.to_compile_error().into();
212    }
213
214    // Generate code
215    let expanded = generate_message_impl(&input, &attr_args, &field_infos);
216
217    TokenStream::from(expanded)
218}
219
220/// Attribute macro for registering enums used in J1939 messages.
221///
222/// This macro registers enum information for automatic documentation generation. When an enum
223/// marked with `#[j1939_enum]` is used as a field type in a message, the generated documentation
224/// will include a comprehensive table showing all enum variants, their values, and descriptions.
225///
226/// # Requirements
227///
228/// - The enum must have `#[repr(u8)]` to ensure proper memory layout
229/// - Variants can have explicit discriminant values or use implicit incrementing values
230/// - Each variant can have doc comments which will appear in the generated documentation
231///
232/// # Benefits
233///
234/// - Automatic documentation table generation for enum fields in J1939 messages
235/// - Variant values shown in both decimal and binary format
236/// - Integration with the message field layout documentation
237/// - Type-safe enum encoding/decoding
238///
239/// # Examples
240///
241/// ## Basic Enum Registration
242///
243/// ```ignore
244/// use j1939_rs::prelude::*;
245///
246/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
247/// #[repr(u8)]
248/// #[j1939_enum]
249/// /// Engine operating mode
250/// pub enum EngineMode {
251///     /// Engine is stopped
252///     Stopped = 0,
253///     /// Engine is starting
254///     Starting = 1,
255///     /// Engine running normally
256///     Running = 2,
257///     /// Engine has critical fault
258///     Fault = 7,
259/// }
260/// ```
261///
262/// ## Enum with Implicit Values
263///
264/// ```ignore
265/// use j1939_rs::prelude::*;
266///
267/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
268/// #[repr(u8)]
269/// #[j1939_enum]
270/// /// Gear selection
271/// pub enum GearPosition {
272///     /// Park - vehicle locked
273///     Park,      // = 0
274///     /// Reverse gear
275///     Reverse,   // = 1
276///     /// Neutral - no gear engaged
277///     Neutral,   // = 2
278///     /// Drive mode
279///     Drive,     // = 3
280///     /// Low gear for climbing
281///     Low,       // = 4
282/// }
283/// ```
284///
285/// ## Using Enums in Messages
286///
287/// ```ignore
288/// use j1939_rs::prelude::*;
289///
290/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
291/// #[repr(u8)]
292/// #[j1939_enum]
293/// pub enum TorqueMode {
294///     LowIdle = 0,
295///     AcceleratorControl = 1,
296///     CruiseControl = 2,
297/// }
298///
299/// #[j1939_message(pgn = 61444, priority = 3)]
300/// pub struct EngineController {
301///     /// Current torque control mode
302///     #[j1939(bits = 0..4)]
303///     pub torque_mode: TorqueMode,
304///
305///     #[j1939(bits = 4..20, scale = 0.125, unit = "rpm")]
306///     pub engine_speed: f32,
307///
308///     #[j1939(bits = 20..64, reserved)]
309///     pub reserved: (),
310/// }
311/// ```
312///
313/// # Notes
314///
315/// - The macro preserves the original enum definition unchanged
316/// - Enum information is stored in a compile-time registry for documentation generation
317/// - Enums are transmitted as their underlying `u8` representation
318/// - Use `#[repr(u8)]` to ensure consistent memory layout across platforms
319#[proc_macro_attribute]
320pub fn j1939_enum(_attr: TokenStream, item: TokenStream) -> TokenStream {
321    let input = parse_macro_input!(item as DeriveInput);
322
323    // Parse and register enum information
324    if let Data::Enum(enum_data) = &input.data {
325        let enum_name = input.ident.to_string();
326
327        // Extract enum-level documentation
328        let enum_docs = extract_doc_comments(&input.attrs);
329
330        // Extract variant information
331        let mut variants = Vec::new();
332        let mut current_value = 0u64;
333
334        for variant in &enum_data.variants {
335            let variant_name = variant.ident.to_string();
336            let variant_docs = extract_doc_comments(&variant.attrs);
337
338            // Handle explicit discriminant values
339            let value = if let Some((_, expr)) = &variant.discriminant
340                && let Lit(ExprLit {
341                    lit: syn::Lit::Int(lit_int),
342                    ..
343                }) = expr
344            {
345                current_value = lit_int.base10_parse().unwrap_or(current_value);
346                current_value
347            } else {
348                current_value
349            };
350
351            variants.push(EnumVariant {
352                name: variant_name,
353                value,
354                doc_comments: variant_docs,
355            });
356
357            current_value += 1;
358        }
359
360        // Register the enum
361        let enum_info = EnumInfo {
362            name: enum_name,
363            doc_comments: enum_docs,
364            variants,
365        };
366
367        register_enum(enum_info);
368    }
369
370    // Return the original enum unchanged
371    let enum_name = &input.ident;
372    let vis = &input.vis;
373    let attrs = &input.attrs;
374
375    if let Data::Enum(enum_data) = &input.data {
376        let variants = &enum_data.variants;
377
378        quote! {
379            #(#attrs)*
380            #vis enum #enum_name {
381                #variants
382            }
383        }
384        .into()
385    } else {
386        syn::Error::new_spanned(&input, "j1939_enum can only be used on enums")
387            .to_compile_error()
388            .into()
389    }
390}
391
392fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec<String> {
393    attrs
394        .iter()
395        .filter_map(|attr| {
396            if attr.path().is_ident("doc")
397                && let NameValue(nv) = &attr.meta
398                && let Lit(ExprLit {
399                    lit: syn::Lit::Str(s),
400                    ..
401                }) = &nv.value
402            {
403                return Some(s.value().trim().to_string());
404            }
405            None
406        })
407        .collect()
408}