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});