1use crate::llm::actions::{
33 protocol_trait::{ActionResult, Protocol, Server},
34 ActionDefinition, Parameter, ParameterDefinition,
35};
36use crate::protocol::log_template::LogTemplate;
37use crate::protocol::EventType;
38use crate::server::can::frame::CanFrame;
39use anyhow::{anyhow, Result};
40use serde_json::json;
41use std::sync::LazyLock;
42
43pub const NO_RESPONSE_ACTION: &str = "no_response";
45pub const SEND_CAN_FRAME_ACTION: &str = "send_can_frame";
47
48pub const DEFAULT_INTERFACE: &str = "can0";
51
52pub static CAN_FRAME_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
58 EventType::new(
59 "can_frame_received",
60 "A CAN frame arrived on the bus. There is no addressing: this frame reached every node, \
61 and every node decides for itself whether the identifier is one it answers. Reply with \
62 send_can_frame if this identifier is one the ECU you are simulating owns, or with \
63 no_response if it is not - saying nothing is the normal case on a CAN bus, not a \
64 failure.",
65 json!({
66 "type": "send_can_frame",
67 "id": "0x7E8",
68 "extended": false,
69 "data": "0341050f",
70 "encoding": "hex"
71 }),
72 )
73 .with_actions(can_actions())
74 .with_alternative_example(json!({"type": "no_response"}))
75 .with_parameters(vec![
76 Parameter {
77 name: "id".to_string(),
78 type_hint: "string".to_string(),
79 description: "Identifier in hex, e.g. '0x7DF' (11-bit) or '0x18DAF110' (29-bit)"
80 .to_string(),
81 required: true,
82 },
83 Parameter {
84 name: "extended".to_string(),
85 type_hint: "boolean".to_string(),
86 description: "True for a 29-bit identifier, false for 11-bit. Never inferred from \
87 the identifier's magnitude - 0x123 is legal in both formats and they \
88 are different frames."
89 .to_string(),
90 required: true,
91 },
92 Parameter {
93 name: "rtr".to_string(),
94 type_hint: "boolean".to_string(),
95 description: "Remote transmission request: another node is asking for this \
96 identifier's data and sent none of its own. The dlc says how many \
97 bytes it wants."
98 .to_string(),
99 required: true,
100 },
101 Parameter {
102 name: "dlc".to_string(),
103 type_hint: "number".to_string(),
104 description: "Data length code. Equal to the byte count for classic CAN; for CAN FD \
105 it is a length CODE, where 9..15 mean 12, 16, 20, 24, 32, 48 and 64 \
106 bytes."
107 .to_string(),
108 required: true,
109 },
110 Parameter {
111 name: "data".to_string(),
112 type_hint: "string".to_string(),
113 description: "The payload, hex-encoded (data_encoding is always 'hex' on receive). \
114 CAN payloads are opaque: their meaning lives in a DBC file."
115 .to_string(),
116 required: true,
117 },
118 Parameter {
119 name: "fd".to_string(),
120 type_hint: "boolean".to_string(),
121 description: "True for a CAN FD frame (up to 64 bytes), false for classic CAN 2.0"
122 .to_string(),
123 required: true,
124 },
125 Parameter {
126 name: "brs".to_string(),
127 type_hint: "boolean".to_string(),
128 description: "CAN FD bit-rate switch: the data phase ran at the faster bit rate"
129 .to_string(),
130 required: true,
131 },
132 Parameter {
133 name: "interface".to_string(),
134 type_hint: "string".to_string(),
135 description: "CAN interface the frame arrived on, e.g. 'can0' or 'vcan0'".to_string(),
136 required: true,
137 },
138 ])
139 .with_log_template(
140 LogTemplate::new()
141 .with_info("CAN {interface} rx {id} dlc={dlc}")
142 .with_debug("CAN rx: id={id} ext={extended} rtr={rtr} fd={fd} dlc={dlc} data={data}")
143 .with_trace("CAN rx: {json_pretty(.)}"),
144 )
145});
146
147pub static CAN_ERROR_FRAME_EVENT: LazyLock<EventType> = LazyLock::new(|| {
149 EventType::new(
150 "can_error_frame",
151 "The CAN controller delivered an error frame. This is a report from the local controller \
152 about the state of the bus - a lost arbitration, a missing acknowledgement, a protocol \
153 violation - and NOT a message any node sent you. You cannot reply to it with an error \
154 frame: NetGet has no action that emits one, because an error frame corrupts whatever is \
155 in flight and repeated ones drive nodes off the bus. Answer with no_response unless the \
156 condition means the ECU you simulate should transmit something.",
157 json!({"type": "no_response"}),
158 )
159 .with_actions(can_actions())
160 .with_alternative_example(json!({
161 "type": "send_can_frame",
162 "id": "0x7E8",
163 "data": "03410c1aF8",
164 "encoding": "hex"
165 }))
166 .with_parameters(vec![
167 Parameter {
168 name: "error_classes".to_string(),
169 type_hint: "array".to_string(),
170 description: "Decoded error classes, e.g. ['no_acknowledgement'], \
171 ['arbitration_lost'], ['controller_problem'], ['bus_off']"
172 .to_string(),
173 required: true,
174 },
175 Parameter {
176 name: "id_decimal".to_string(),
177 type_hint: "number".to_string(),
178 description: "The raw error class bitmask, for anyone who wants the bits".to_string(),
179 required: true,
180 },
181 Parameter {
182 name: "data".to_string(),
183 type_hint: "string".to_string(),
184 description: "The eight controller detail octets, hex-encoded".to_string(),
185 required: true,
186 },
187 Parameter {
188 name: "interface".to_string(),
189 type_hint: "string".to_string(),
190 description: "CAN interface reporting the error".to_string(),
191 required: true,
192 },
193 ])
194 .with_log_template(
195 LogTemplate::new()
196 .with_info("CAN {interface} error frame {error_classes}")
197 .with_debug("CAN error frame: classes={error_classes} detail={data}")
198 .with_trace("CAN error frame: {json_pretty(.)}"),
199 )
200});
201
202pub static CAN_BUS_STATE_CHANGED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
204 EventType::new(
205 "can_bus_state_changed",
206 "The local CAN controller changed error-confinement state. The ladder is error_active -> \
207 error_warning -> error_passive -> bus_off, driven by the controller's own error \
208 counters. At bus_off the controller has disconnected itself: nothing you transmit will \
209 reach the bus until it is restarted. Usually the right answer is no_response; \
210 transmitting into a degrading bus makes it worse.",
211 json!({"type": "no_response"}),
212 )
213 .with_actions(can_actions())
214 .with_parameters(vec![
215 Parameter {
216 name: "bus_state".to_string(),
217 type_hint: "string".to_string(),
218 description: "One of 'error_active', 'error_warning', 'error_passive', 'bus_off'"
219 .to_string(),
220 required: true,
221 },
222 Parameter {
223 name: "previous_state".to_string(),
224 type_hint: "string".to_string(),
225 description: "The state being left".to_string(),
226 required: true,
227 },
228 Parameter {
229 name: "error_classes".to_string(),
230 type_hint: "array".to_string(),
231 description: "Error classes of the frame that reported the change".to_string(),
232 required: true,
233 },
234 Parameter {
235 name: "interface".to_string(),
236 type_hint: "string".to_string(),
237 description: "CAN interface whose state changed".to_string(),
238 required: true,
239 },
240 ])
241 .with_log_template(
242 LogTemplate::new()
243 .with_info("CAN {interface} bus state {previous_state} -> {bus_state}")
244 .with_debug("CAN bus state change: {previous_state} -> {bus_state} ({error_classes})")
245 .with_trace("CAN bus state: {json_pretty(.)}"),
246 )
247});
248
249pub fn can_actions() -> Vec<ActionDefinition> {
259 vec![send_can_frame_action(), no_response_action()]
260}
261
262fn send_can_frame_action() -> ActionDefinition {
263 ActionDefinition {
264 name: SEND_CAN_FRAME_ACTION.to_string(),
265 description:
266 "Transmit one CAN frame. It reaches every node on the bus - there is no addressing \
267 and no authentication, so the identifier alone decides who treats it as theirs. \
268 Diagnostic convention: a tester requests on 0x7DF (broadcast) or 0x7E0+n, and ECU n \
269 answers on 0x7E8+n."
270 .to_string(),
271 parameters: vec![
272 Parameter {
273 name: "id".to_string(),
274 type_hint: "string".to_string(),
275 description:
276 "Identifier, hex with or without the 0x prefix ('0x7E8', '7E8') or a decimal \
277 number. 11 bits (max 0x7FF) unless 'extended' is true, then 29 (max \
278 0x1FFFFFFF)."
279 .to_string(),
280 required: true,
281 },
282 Parameter {
283 name: "extended".to_string(),
284 type_hint: "boolean".to_string(),
285 description:
286 "True for a 29-bit identifier. Default false. State it explicitly: it is NOT \
287 inferred from the identifier's size, because 0x123 is a legal identifier in \
288 both formats and they are different frames that different nodes answer."
289 .to_string(),
290 required: false,
291 },
292 Parameter {
293 name: "data".to_string(),
294 type_hint: "string".to_string(),
295 description:
296 "The payload (see 'encoding'). 0-8 bytes for classic CAN; 0-8, 12, 16, 20, \
297 24, 32, 48 or 64 for CAN FD - no other FD length exists, and one that is not \
298 encodable is refused rather than padded. Omit for a zero-length frame."
299 .to_string(),
300 required: false,
301 },
302 Parameter {
303 name: "encoding".to_string(),
304 type_hint: "string".to_string(),
305 description:
306 "How 'data' is written: 'hex' (the default - CAN payloads are opaque bytes \
307 whose meaning lives in a DBC file) or 'text' for ASCII. This is read, not \
308 guessed: '48656c6c6f' is valid hex AND valid text and only you know which \
309 you meant."
310 .to_string(),
311 required: false,
312 },
313 Parameter {
314 name: "rtr".to_string(),
315 type_hint: "boolean".to_string(),
316 description:
317 "Send a remote transmission request instead of data: ask whichever node owns \
318 this identifier to transmit it. Carries no payload; 'dlc' says how many \
319 bytes are being requested. Not available on CAN FD."
320 .to_string(),
321 required: false,
322 },
323 Parameter {
324 name: "dlc".to_string(),
325 type_hint: "number".to_string(),
326 description: "Bytes requested by a remote frame, 0-8. Ignored on a data frame, \
327 whose length comes from 'data'."
328 .to_string(),
329 required: false,
330 },
331 Parameter {
332 name: "fd".to_string(),
333 type_hint: "boolean".to_string(),
334 description:
335 "Send a CAN FD frame (payloads up to 64 bytes). Default false. The bus and \
336 every listening node must support FD; a classic-only node sees an error."
337 .to_string(),
338 required: false,
339 },
340 Parameter {
341 name: "brs".to_string(),
342 type_hint: "boolean".to_string(),
343 description: "CAN FD bit-rate switch: run the data phase at the faster bit rate. \
344 Requires 'fd'."
345 .to_string(),
346 required: false,
347 },
348 ],
349 example: json!({
350 "type": "send_can_frame",
351 "id": "0x7E8",
352 "extended": false,
353 "data": "0341050f",
354 "encoding": "hex"
355 }),
356 log_template: Some(
357 LogTemplate::new()
358 .with_info("-> CAN {id} {data}")
359 .with_debug("CAN tx: id={id} ext={extended} rtr={rtr} fd={fd} data={data}"),
360 ),
361 }
362}
363
364fn no_response_action() -> ActionDefinition {
365 ActionDefinition {
366 name: NO_RESPONSE_ACTION.to_string(),
367 description:
368 "Transmit nothing, deliberately. On a CAN bus this is the normal outcome: every frame \
369 reaches every node and almost every node ignores almost every frame. Use it whenever \
370 the identifier is not one the ECU you are simulating owns. It is a real decision and \
371 is logged as one (decision=model_reject), distinct from failing to answer."
372 .to_string(),
373 parameters: vec![],
374 example: json!({"type": "no_response"}),
375 log_template: Some(LogTemplate::new().with_info("-> CAN silent (no_response)")),
376 }
377}
378
379pub struct CanProtocol;
385
386impl CanProtocol {
387 pub fn new() -> Self {
388 Self
389 }
390}
391
392impl Default for CanProtocol {
393 fn default() -> Self {
394 Self::new()
395 }
396}
397
398impl Protocol for CanProtocol {
399 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
400 vec![
401 ParameterDefinition {
402 name: "interface".to_string(),
403 type_hint: "string".to_string(),
404 description: format!(
405 "CAN interface to bind, e.g. 'can0' or 'vcan0' (default: {DEFAULT_INTERFACE}). \
406 It must already exist and be up. A virtual bus needs no hardware: `sudo \
407 modprobe vcan && sudo ip link add dev vcan0 type vcan && sudo ip link set up \
408 vcan0`."
409 ),
410 required: false,
411 example: json!("vcan0"),
412 default: None,
413 },
414 ParameterDefinition {
415 name: "transport".to_string(),
416 type_hint: "string".to_string(),
417 description:
418 "'socketcan' (default) for a real AF_CAN socket, which exists only on Linux; \
419 'udp' for the unprivileged TEST transport, which carries SocketCAN frame \
420 structs as UDP datagram payloads and which no real CAN node speaks."
421 .to_string(),
422 required: false,
423 example: json!("socketcan"),
424 default: None,
425 },
426 ParameterDefinition {
427 name: "udp_peer".to_string(),
428 type_hint: "string".to_string(),
429 description:
430 "HOST:PORT to transmit to on the 'udp' test transport when nothing has been \
431 received yet. Rejected with transport 'socketcan', where it would do nothing."
432 .to_string(),
433 required: false,
434 example: json!("127.0.0.1:34567"),
435 default: None,
436 },
437 ]
438 }
439
440 fn get_async_actions(
445 &self,
446 _state: &crate::state::app_state::AppState,
447 ) -> Vec<ActionDefinition> {
448 can_actions()
449 }
450
451 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
453 can_actions()
454 }
455
456 fn get_event_types(&self) -> Vec<EventType> {
457 vec![
458 CAN_FRAME_RECEIVED_EVENT.clone(),
459 CAN_ERROR_FRAME_EVENT.clone(),
460 CAN_BUS_STATE_CHANGED_EVENT.clone(),
461 ]
462 }
463
464 fn protocol_name(&self) -> &'static str {
465 "CAN"
466 }
467
468 fn stack_name(&self) -> &'static str {
469 "CAN"
470 }
471
472 fn keywords(&self) -> Vec<&'static str> {
473 vec![
474 "can",
475 "canbus",
476 "socketcan",
477 "automotive",
478 "ecu",
479 "obd",
480 "obd2",
481 "j1939",
482 "vcan",
483 "canfd",
484 ]
485 }
486
487 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
488 use crate::protocol::metadata::{
489 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
490 };
491
492 ProtocolMetadataV2::builder()
493 .state(DevelopmentState::Experimental)
494 .privilege_requirement(PrivilegeRequirement::None)
500 .connectionless()
504 .deliberately_silent()
508 .implementation(
509 "Frame representation, validation and the SocketCAN struct can_frame / \
510 canfd_frame wire encoding are a pure module (src/server/can/frame.rs) with no \
511 socket, no cfg and no state. Two transports sit on it: a real AF_CAN socket via \
512 the socketcan crate on Linux, and a UDP test transport carrying the same frame \
513 structs in datagrams. On macOS and Windows the socketcan transport returns an \
514 error naming the reason instead of starting.",
515 )
516 .llm_control(
517 "The model is the ECU. It sees can_frame_received (id, extended, rtr, error, \
518 dlc, hex data, fd, brs, interface), can_error_frame (decoded error classes) and \
519 can_bus_state_changed (the error-confinement ladder), and answers with \
520 send_can_frame or no_response. There is no action that emits a CAN error frame, \
521 deliberately: an error frame corrupts traffic in flight and repeated ones drive \
522 nodes bus-off.",
523 )
524 .e2e_testing(
525 "The codec is asserted against literal values - both DLC tables in both \
526 directions, all sixteen CAN FD length codes, standard vs extended identifiers, \
527 RTR, error-class decoding, the 16- and 72-octet struct layouts, and an \
528 over-length payload being refused rather than truncated. The whole event -> \
529 handler/LLM -> action -> frame path runs in-process over the UDP test transport \
530 (tests/server/can/e2e_test.rs), including the assertion that an LLM failure puts \
531 nothing on the bus. The AF_CAN transport is covered by none of it.",
532 )
533 .notes(
534 "VERIFIED: frame construction, validation and the SocketCAN wire layout, against \
535 literal values; that the CAN FD DLC is treated as a length CODE and not a byte \
536 count; that an unencodable FD length is refused; that macOS/Windows refuse to \
537 start with an explicit reason rather than reporting Running; and the full \
538 event/action path over the UDP test transport. NOT VERIFIED: the SocketCAN \
539 transport has never been compiled or run. AF_CAN exists only in the Linux \
540 kernel, the machine this was written on is macOS, and no frame this code \
541 produced has reached a real CAN bus or a real CAN peer. Treat first use on Linux \
542 as bring-up. Experimental, not Beta, for exactly that reason - the path to Beta \
543 is cheap and is written down in src/server/can/CLAUDE.md: Linux vcan needs no \
544 hardware and can-utils' cansend/candump are real third-party peers.",
545 )
546 .build()
547 }
548
549 fn description(&self) -> &'static str {
550 "CAN bus (SocketCAN) ECU simulator - Linux only, or a UDP test transport"
551 }
552
553 fn example_prompt(&self) -> &'static str {
554 "Act as an engine ECU on the CAN bus: answer OBD-II requests on 0x7DF from 0x7E8, \
555 reporting 2400 rpm and 88 degrees coolant temperature"
556 }
557
558 fn group_name(&self) -> &'static str {
559 "Core"
560 }
561
562 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
563 use crate::llm::actions::StartupExamples;
564
565 StartupExamples::new(
566 json!({
568 "type": "open_server",
569 "port": 0,
570 "base_stack": "can",
571 "instruction": "You are an engine control unit on a vehicle CAN bus. Answer \
572 OBD-II mode 01 requests addressed to 0x7DF or 0x7E0 by \
573 transmitting from 0x7E8. Ignore every other identifier.",
574 "startup_params": {"interface": "vcan0"}
575 }),
576 json!({
578 "type": "open_server",
579 "port": 0,
580 "base_stack": "can",
581 "startup_params": {"interface": "vcan0"},
582 "event_handlers": [
583 {
584 "event_pattern": "can_frame_received",
585 "handler": {
586 "type": "script",
587 "language": "python",
588 "code": "actions = [{'type': 'send_can_frame', 'id': '0x7E8', 'data': '0341051e', 'encoding': 'hex'}] if event['data'].startswith('024105') else [{'type': 'no_response'}]"
589 }
590 }
591 ]
592 }),
593 json!({
595 "type": "open_server",
596 "port": 0,
597 "base_stack": "can",
598 "startup_params": {"interface": "vcan0"},
599 "event_handlers": [
600 {
601 "event_pattern": "can_*",
602 "handler": {
603 "type": "static",
604 "actions": [
605 {
606 "type": "send_can_frame",
607 "id": "0x7E8",
608 "data": "0341050f",
609 "encoding": "hex"
610 }
611 ]
612 }
613 }
614 ]
615 }),
616 )
617 }
618}
619
620impl Server for CanProtocol {
621 fn spawn(
622 &self,
623 ctx: crate::protocol::SpawnContext,
624 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
625 {
626 Box::pin(async move {
627 let listen_addr = ctx
628 .socket_addr()
629 .unwrap_or_else(|| ctx.legacy_listen_addr());
630 crate::server::can::CanServer::spawn_with_llm_actions(
631 listen_addr,
632 ctx.interface.clone(),
633 ctx.llm_client,
634 ctx.state,
635 ctx.status_tx,
636 ctx.server_id,
637 ctx.startup_params,
638 )
639 .await
640 })
641 }
642
643 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
651 let action_type = action
652 .get("type")
653 .and_then(|v| v.as_str())
654 .ok_or_else(|| anyhow!("action must have a 'type' field"))?;
655
656 match action_type {
657 SEND_CAN_FRAME_ACTION => {
658 let frame = CanFrame::from_action(&action)?;
659 Ok(ActionResult::Output(frame.to_wire_bytes()?))
660 }
661 NO_RESPONSE_ACTION => Ok(ActionResult::NoAction),
662 other => Err(anyhow!(
663 "unknown CAN action {other:?}; expected one of {SEND_CAN_FRAME_ACTION}, \
664 {NO_RESPONSE_ACTION}"
665 )),
666 }
667 }
668}