1use crate::llm::actions::protocol_trait::{ActionResult, Server};
2use crate::llm::actions::{protocol_trait::Protocol, ActionDefinition};
3use crate::protocol::{EventType, SpawnContext};
4use crate::server::ics_support::{action, parameter};
5use crate::state::AppState;
6use anyhow::{ensure, Result};
7use serde_json::{json, Value};
8use std::{future::Future, net::SocketAddr, pin::Pin, sync::LazyLock};
9#[derive(Default)]
10pub struct BacnetProtocol;
11impl BacnetProtocol {
12 pub fn new() -> Self {
13 Self
14 }
15}
16pub fn replies() -> Vec<ActionDefinition> {
17 vec![action(
18 "bacnet_reply",
19 "Typed read value, explicit write approval or BACnet error",
20 vec![
21 parameter(
22 "value_type",
23 "string",
24 "null,boolean,unsigned,signed,real,string,enumerated,object_identifier",
25 false,
26 ),
27 parameter("value", "any", "Typed property value", false),
28 parameter(
29 "accepted",
30 "boolean",
31 "Explicit approval of the property write",
32 false,
33 ),
34 parameter("error_class", "number", "BACnet error class 0..7", false),
35 parameter("error_code", "number", "BACnet error code", false),
36 ],
37 json!({"type":"bacnet_reply","value_type":"real","value":12.5}),
38 )]
39}
40pub static EVENTS: LazyLock<Vec<EventType>> = LazyLock::new(|| {
41 vec![EventType::new(
42 "bacnet_request",
43 "BACnet/IP discovery and typed property reads/writes",
44 replies()[0].example.clone(),
45 )
46 .with_parameters(vec![
47 parameter(
48 "operation",
49 "string",
50 "Device data read or write operation",
51 true,
52 ),
53 parameter(
54 "object_type",
55 "number",
56 "BACnet object type number, 0..1023",
57 true,
58 ),
59 parameter("instance", "number", "Object instance", true),
60 parameter(
61 "property",
62 "number",
63 "BACnet property identifier number",
64 true,
65 ),
66 parameter(
67 "invoke_id",
68 "number",
69 "BACnet transaction invoke identifier",
70 true,
71 ),
72 parameter("service", "number", "Confirmed service", true),
73 parameter(
74 "array_index",
75 "number",
76 "Optional property array element index",
77 false,
78 ),
79 parameter(
80 "priority",
81 "number",
82 "BACnet write priority number, 1..16",
83 false,
84 ),
85 parameter(
86 "value_type",
87 "string",
88 "Type of the decoded write request value",
89 false,
90 ),
91 parameter(
92 "value",
93 "any",
94 "Decoded typed value of the write request",
95 false,
96 ),
97 ])
98 .with_actions(replies())]
99});
100impl Protocol for BacnetProtocol {
101 fn protocol_name(&self) -> &'static str {
102 "BACnet/IP"
103 }
104 fn stack_name(&self) -> &'static str {
105 "ETH>IP>UDP>BACnet"
106 }
107 fn keywords(&self) -> Vec<&'static str> {
108 vec!["bacnet"]
109 }
110 fn description(&self) -> &'static str {
111 "BACnet/IP discovery and typed property reads/writes"
112 }
113 fn example_prompt(&self) -> &'static str {
114 "Simulate a BACnet/IP device on localhost"
115 }
116 fn group_name(&self) -> &'static str {
117 "Industrial"
118 }
119 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
120 vec![action(
121 "disconnect",
122 "Close this peer",
123 vec![],
124 json!({"type":"disconnect"}),
125 )]
126 }
127 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
128 replies()
129 }
130 fn get_event_types(&self) -> Vec<EventType> {
131 EVENTS.clone()
132 }
133 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
134 vec![crate::llm::actions::ParameterDefinition {
135 name: "device_id".into(),
136 type_hint: "number".into(),
137 description: "BACnet Device instance 0..4194303".into(),
138 required: false,
139 example: json!(1234),
140 default: Some(json!(super::codec::DEFAULT_DEVICE_ID)),
141 }]
142 }
143 fn metadata(&self) -> crate::protocol::ProtocolMetadataV2 {
144 use crate::protocol::metadata::*;
145 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).privilege_requirement(PrivilegeRequirement::None).implementation("BVLC/NPDU/APDU with unsegmented WhoIs/IAm and ReadProperty/WriteProperty, explicit Error/Reject/Abort responses").llm_control("Handler supplies device values, operation approvals and refusals; client handler selects structured operations").e2e_testing("Independent peer integration plus negative and lifecycle tests in tests/server/bacnet").notes("Local IPv4 and unicast client discovery. Broadcast WhoIs accepted. No BBMD, routing, segmentation, ReadPropertyMultiple or COV. Segmented requests abort; unsupported services reject. Typed primitives only; no stored object data. 480-byte datagrams; 256 concurrent handler turns; 10s response timeout.").max_inbound_bytes(crate::server::bacnet::codec::MAX_FRAME).well_known_udp_port(47808).answers_on_failure().connectionless().build()
146 }
147 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
148 let base = json!({"type":"open_server","base_stack":"bacnet","port":47808,"instruction":"Supply device decisions with structured actions"});
149 let a = replies()[0].example.clone();
150 let mut fixed = base.clone();
151 fixed["event_handlers"] = json!([{"event_pattern":"bacnet_request","handler":{"type":"static","actions":[a.clone()]}}]);
152 let code = format!("print({})", json!(json!({"actions":[a]}).to_string()));
153 let mut script = base.clone();
154 script["event_handlers"] = json!([{"event_pattern":"bacnet_request","handler":{"type":"script","language":"python","code":code}}]);
155 crate::llm::actions::StartupExamples::new(base, script, fixed)
156 }
157}
158impl Server for BacnetProtocol {
159 fn spawn(&self, ctx: SpawnContext) -> Pin<Box<dyn Future<Output = Result<SocketAddr>> + Send>> {
160 Box::pin(super::spawn(ctx))
161 }
162 fn execute_action(&self, v: Value) -> Result<ActionResult> {
163 if v["type"] == "disconnect" {
164 return Ok(ActionResult::CloseConnection);
165 }
166 ensure!(
167 replies().iter().any(|a| v["type"] == a.name),
168 "unknown device reply action"
169 );
170 ensure!(
171 crate::utils::json_budget::within_budget(&v, 4096, 1024, 16),
172 "reply exceeds JSON budget"
173 );
174 Ok(ActionResult::Custom {
175 name: v["type"].as_str().expect("validated").into(),
176 data: v,
177 })
178 }
179}