1use crate::llm::actions::client_trait::{Client, ClientActionResult};
2use crate::llm::actions::{protocol_trait::Protocol, ActionDefinition};
3use crate::protocol::{ConnectContext, EventType};
4use crate::server::ics_support::{action, parameter};
5use crate::state::AppState;
6use anyhow::Result;
7use serde_json::{json, Value};
8use std::{future::Future, net::SocketAddr, pin::Pin, sync::LazyLock};
9#[derive(Default)]
10pub struct BacnetClientProtocol;
11impl BacnetClientProtocol {
12 pub fn new() -> Self {
13 Self
14 }
15}
16pub fn requests() -> Vec<ActionDefinition> {
17 let f = || {
18 vec![
19 parameter("object_type", "number", "Object type 0..1023", true),
20 parameter("instance", "number", "Object instance 0..4194303", true),
21 parameter("property", "number", "Property identifier", true),
22 parameter("array_index", "number", "Optional array index", false),
23 ]
24 };
25 let mut w = f();
26 w.extend([
27 parameter("value_type", "string", "Application value type", true),
28 parameter("value", "any", "Typed property value", true),
29 parameter("priority", "number", "Write priority 1..16", false),
30 ]);
31 vec![
32 action(
33 "bacnet_discover",
34 "Unicast WhoIs/IAm discovery",
35 vec![],
36 json!({"type":"bacnet_discover"}),
37 ),
38 action(
39 "bacnet_read",
40 "Read a typed BACnet object property",
41 f(),
42 json!({"type":"bacnet_read","object_type":2,"instance":1,"property":85}),
43 ),
44 action(
45 "bacnet_write",
46 "Write a typed BACnet object property",
47 w,
48 json!({"type":"bacnet_write","object_type":2,"instance":1,"property":85,"value_type":"real","value":12.5}),
49 ),
50 action(
51 "disconnect",
52 "Close UDP client",
53 vec![],
54 json!({"type":"disconnect"}),
55 ),
56 ]
57}
58pub static EVENTS: LazyLock<Vec<EventType>> = LazyLock::new(|| {
59 vec![
60 EventType::new(
61 "bacnet_connected",
62 "Transport ready for device operations",
63 requests()[0].example.clone(),
64 )
65 .with_parameters(vec![parameter(
66 "remote_addr",
67 "string",
68 "Address of the connected remote device",
69 true,
70 )])
71 .with_actions(requests()),
72 EventType::new(
73 "bacnet_response",
74 "Device operation result or protocol status",
75 requests()[0].example.clone(),
76 )
77 .with_parameters(vec![
78 parameter(
79 "success",
80 "boolean",
81 "Whether the device accepted the operation",
82 false,
83 ),
84 parameter("status", "number", "Protocol error status, if any", false),
85 parameter("value", "object", "Decoded response fields", false),
86 ])
87 .with_actions(requests()),
88 ]
89});
90impl Protocol for BacnetClientProtocol {
91 fn protocol_name(&self) -> &'static str {
92 "BACnet/IP"
93 }
94 fn stack_name(&self) -> &'static str {
95 "ETH>IP>UDP>BACnet"
96 }
97 fn keywords(&self) -> Vec<&'static str> {
98 vec!["bacnet"]
99 }
100 fn description(&self) -> &'static str {
101 "BACnet/IP discovery and typed property reads/writes"
102 }
103 fn example_prompt(&self) -> &'static str {
104 "Connect to a BACnet/IP device on localhost"
105 }
106 fn group_name(&self) -> &'static str {
107 "Industrial"
108 }
109 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
110 requests()
111 }
112 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
113 vec![]
114 }
115 fn get_event_types(&self) -> Vec<EventType> {
116 EVENTS.clone()
117 }
118
119 fn metadata(&self) -> crate::protocol::ProtocolMetadataV2 {
120 use crate::protocol::metadata::*;
121 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/client/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).build()
122 }
123 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
124 let base = json!({"type":"open_client","protocol":"bacnet","remote_addr":"127.0.0.1:47808","instruction":"Supply device decisions with structured actions"});
125 let a = requests()[0].example.clone();
126 let mut fixed = base.clone();
127 fixed["event_handlers"] = json!([{"event_pattern":"bacnet_connected","handler":{"type":"static","actions":[a.clone()]}}]);
128 let code = format!("print({})", json!(json!({"actions":[a]}).to_string()));
129 let mut script = base.clone();
130 script["event_handlers"] = json!([{"event_pattern":"bacnet_connected","handler":{"type":"script","language":"python","code":code}}]);
131 crate::llm::actions::StartupExamples::new(base, script, fixed)
132 }
133}
134impl Client for BacnetClientProtocol {
135 fn connect(
136 &self,
137 ctx: ConnectContext,
138 ) -> Pin<Box<dyn Future<Output = Result<SocketAddr>> + Send>> {
139 Box::pin(super::connect(ctx))
140 }
141 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
142 if v["type"] == "disconnect" {
143 return Ok(ClientActionResult::Disconnect);
144 }
145 crate::server::bacnet::codec::validate(&v)?;
146 Ok(ClientActionResult::Custom {
147 name: v["type"]
148 .as_str()
149 .ok_or_else(|| anyhow::anyhow!("action type"))?
150 .into(),
151 data: v,
152 })
153 }
154}