Skip to main content

netget/client/s7comm/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::ics_support::{action, parameter};
8use crate::state::AppState;
9use anyhow::Result;
10use serde_json::{json, Value};
11use std::{future::Future, net::SocketAddr, pin::Pin, sync::Arc};
12#[derive(Default)]
13pub struct S7commClientProtocol;
14impl S7commClientProtocol {
15    pub fn new() -> Self {
16        Self
17    }
18}
19pub fn requests() -> Vec<ActionDefinition> {
20    let fields = || {
21        vec![
22            parameter("area", "string", "db, inputs, outputs or markers", true),
23            parameter("db", "number", "DB number (0 for non-DB areas)", true),
24            parameter("start", "number", "Byte offset (0..2097151)", true),
25        ]
26    };
27    let mut r = fields();
28    r.push(parameter("count", "number", "Byte count 1..200", true));
29    let mut w = fields();
30    w.push(parameter(
31        "values",
32        "array",
33        "Byte elements 0..255, 1..200 elements",
34        true,
35    ));
36    vec![
37        action(
38            "s7comm_read",
39            "Read a legacy data area",
40            r,
41            json!({"type":"s7comm_read","area":"db","db":1,"start":0,"count":1}),
42        ),
43        action(
44            "s7comm_write",
45            "Write a legacy data area",
46            w,
47            json!({"type":"s7comm_write","area":"db","db":1,"start":0,"values":[42]}),
48        ),
49        action(
50            "disconnect",
51            "Close the PLC connection",
52            vec![],
53            json!({"type":"disconnect"}),
54        ),
55    ]
56}
57impl Protocol for S7commClientProtocol {
58    fn protocol_name(&self) -> &'static str {
59        "S7comm"
60    }
61    fn stack_name(&self) -> &'static str {
62        "ETH>IP>TCP>TPKT>COTP>S7comm"
63    }
64    fn keywords(&self) -> Vec<&'static str> {
65        vec!["s7comm"]
66    }
67    fn description(&self) -> &'static str {
68        "Selected legacy S7comm byte-area scanner"
69    }
70    fn example_prompt(&self) -> &'static str {
71        "Read byte 0 of DB1 from the S7comm PLC at 127.0.0.1:102"
72    }
73    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
74        let base = json!({"type":"open_client","protocol":"s7comm","remote_addr":"127.0.0.1:102","instruction":"Supply data-area decisions with structured actions"});
75        let mut fixed = base.clone();
76        fixed["event_handlers"] = json!([{"event_pattern":"s7comm_connected","handler":{"type":"static","actions":[requests()[0].example]}}]);
77        let mut script = base.clone();
78        script["event_handlers"] = json!([{"event_pattern":"s7comm_connected","handler":{"type":"script","language":"python","code":"import json\nprint(json.dumps({'actions':[{'type':'s7comm_read','area':'db','db':1,'start':0,'count':1}]}))"}}]);
79        crate::llm::actions::StartupExamples::new(base, script, fixed)
80    }
81    fn group_name(&self) -> &'static str {
82        "Industrial"
83    }
84    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
85        requests()
86    }
87    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
88        vec![]
89    }
90    fn get_event_types(&self) -> Vec<EventType> {
91        EVENTS.clone()
92    }
93    fn metadata(&self) -> crate::protocol::ProtocolMetadataV2 {
94        use crate::protocol::metadata::*;
95        ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).privilege_requirement(PrivilegeRequirement::None).implementation("Hand-written TPKT/COTP/S7comm, setup negotiation and selected S7ANY byte items").llm_control("Selected data-area read values, write acceptance and address/access errors; client read/write operations").e2e_testing("Independent python-snap7 peer, malformed framing, error replies and stop/rebind lifecycle").notes("Legacy byte reads/writes only: DB, inputs, outputs, markers. No S7plus, block upload/download, PLC control or persistence. 480-byte PDU, 16 items per request, 200 bytes per item. 30 s first frame, 600 s idle, 256 connections, 10 s client exchange.").max_inbound_bytes(crate::server::s7comm::codec::MAX_FRAME).build()
96    }
97}
98impl Client for S7commClientProtocol {
99    fn connect(
100        &self,
101        ctx: ConnectContext,
102    ) -> Pin<Box<dyn Future<Output = Result<SocketAddr>> + Send>> {
103        Box::pin(super::connect(ctx))
104    }
105    fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
106        if v["type"] == "disconnect" {
107            return Ok(ClientActionResult::Disconnect);
108        }
109        crate::server::s7comm::codec::validate(&v)?;
110        Ok(ClientActionResult::Custom {
111            name: v["type"].as_str().expect("validated").into(),
112            data: v,
113        })
114    }
115}
116pub static EVENTS: std::sync::LazyLock<Vec<EventType>> = std::sync::LazyLock::new(|| {
117    vec![
118        EventType::new(
119            "s7comm_connected",
120            "COTP and S7 setup complete",
121            requests()[0].example.clone(),
122        )
123        .with_actions(requests()),
124        EventType::new(
125            "s7comm_response",
126            "PLC response, including error status",
127            requests()[0].example.clone(),
128        )
129        .with_actions(requests()),
130    ]
131});