Skip to main content

netget/server/bacnet/
actions.rs

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}