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}