1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition, Parameter, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::lwm2m::actions::{action, parameter, VALUES_HELP};
8use crate::server::lwm2m::content;
9use anyhow::{bail, ensure, Context, Result};
10use serde_json::{json, Value};
11use std::sync::LazyLock;
12
13#[derive(Default)]
14pub struct Lwm2mClientProtocol;
15impl Lwm2mClientProtocol {
16 pub fn new() -> Self {
17 Self
18 }
19}
20
21fn content_action() -> ActionDefinition {
22 action(
23 "lwm2m_content",
24 "Answer a read with the values at the requested path",
25 vec![parameter("values", "array", VALUES_HELP, true)],
26 json!({"type": "lwm2m_content", "values": [{"path": "/3/0/0", "value": "NetGet"}]}),
27 )
28}
29fn ok() -> ActionDefinition {
30 action(
31 "lwm2m_ok",
32 "Accept a write, execute, create or delete request",
33 vec![],
34 json!({"type": "lwm2m_ok"}),
35 )
36}
37fn error() -> ActionDefinition {
38 action(
39 "lwm2m_error",
40 "Refuse a request with an LwM2M error",
41 vec![parameter("code", "string", "not_found (4.04), method_not_allowed (4.05), bad_request (4.00) or unauthorized (4.01)", true)],
42 json!({"type": "lwm2m_error", "code": "not_found"}),
43 )
44}
45fn notify() -> ActionDefinition {
46 action(
47 "lwm2m_notify",
48 "Send the server a notification for a path it observes (the observed path or a resource beneath it)",
49 vec![parameter("path", "string", "The observed path", true), parameter("values", "array", VALUES_HELP, true)],
50 json!({"type": "lwm2m_notify", "path": "/3303/0/5700", "values": [{"path": "/3303/0/5700", "value": 22.5}]}),
51 )
52}
53fn update() -> ActionDefinition {
54 action(
55 "lwm2m_update",
56 "Send a registration update now",
57 vec![],
58 json!({"type": "lwm2m_update"}),
59 )
60}
61fn disconnect() -> ActionDefinition {
62 action(
63 "disconnect",
64 "Deregister from the server and stop",
65 vec![],
66 json!({"type": "disconnect"}),
67 )
68}
69
70fn answers() -> Vec<ActionDefinition> {
71 vec![content_action(), ok(), error(), notify()]
72}
73fn path_param() -> Parameter {
74 parameter("path", "string", "The LwM2M path the server named", true)
75}
76fn request_event(
77 id: &str,
78 description: &str,
79 mut params: Vec<Parameter>,
80 example: Value,
81) -> EventType {
82 params.insert(0, path_param());
83 EventType::new(id, description, example)
84 .with_parameters(params)
85 .with_actions(answers())
86}
87
88pub static REGISTERED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
89 EventType::new(
90 "lwm2m_registered",
91 "The server accepted the registration",
92 json!({"type": "lwm2m_update"}),
93 )
94 .with_parameters(vec![
95 parameter("server", "string", "The server's address", true),
96 parameter(
97 "location",
98 "string",
99 "The registration's location, e.g. /rd/5",
100 true,
101 ),
102 parameter(
103 "lifetime",
104 "number",
105 "The registration lifetime in seconds",
106 true,
107 ),
108 ])
109 .with_actions(vec![notify(), update(), disconnect()])
110});
111pub static READ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
112 request_event(
113 "lwm2m_read_request",
114 "The server reads (or starts observing) a path; answer with its values",
115 vec![parameter(
116 "observe",
117 "boolean",
118 "Whether the server also starts observing it",
119 true,
120 )],
121 content_action().example.clone(),
122 )
123});
124pub static WRITE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
125 request_event(
126 "lwm2m_write_request",
127 "The server writes values",
128 vec![
129 parameter("values", "array", "[{path, value}]", true),
130 parameter("mode", "string", "replace (PUT) or update (POST)", true),
131 ],
132 ok().example.clone(),
133 )
134});
135pub static EXECUTE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
136 request_event(
137 "lwm2m_execute_request",
138 "The server executes a resource",
139 vec![parameter(
140 "arguments",
141 "string",
142 "The execute arguments, if any",
143 false,
144 )],
145 ok().example.clone(),
146 )
147});
148pub static CREATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
149 request_event(
150 "lwm2m_create_request",
151 "The server creates an object instance",
152 vec![parameter("values", "array", "[{path, value}]", true)],
153 ok().example.clone(),
154 )
155});
156pub static DELETE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
157 request_event(
158 "lwm2m_delete_request",
159 "The server deletes an object instance",
160 vec![],
161 ok().example.clone(),
162 )
163});
164
165pub fn error_code(name: &str) -> Option<u8> {
167 use crate::server::coap::codec::code;
168 Some(match name {
169 "not_found" => code(4, 4),
170 "method_not_allowed" => code(4, 5),
171 "bad_request" => code(4, 0),
172 "unauthorized" => code(4, 1),
173 _ => return None,
174 })
175}
176
177impl Protocol for Lwm2mClientProtocol {
178 fn protocol_name(&self) -> &'static str {
179 "LwM2M"
180 }
181 fn stack_name(&self) -> &'static str {
182 "ETH>IP>UDP>CoAP>LwM2M"
183 }
184 fn keywords(&self) -> Vec<&'static str> {
185 vec![
186 "lwm2m",
187 "lwm2m client",
188 "lwm2m device",
189 "device management client",
190 ]
191 }
192 fn description(&self) -> &'static str {
193 "LwM2M 1.1 device over CoAP/UDP: registers its objects with a server and answers its reads, writes, executes and observations"
194 }
195 fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
196 vec![notify(), update(), disconnect()]
197 }
198 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
199 answers()
200 }
201 fn get_event_types(&self) -> Vec<EventType> {
202 vec![
203 REGISTERED_EVENT.clone(),
204 READ_EVENT.clone(),
205 WRITE_EVENT.clone(),
206 EXECUTE_EVENT.clone(),
207 CREATE_EVENT.clone(),
208 DELETE_EVENT.clone(),
209 ]
210 }
211 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
212 vec![
213 ParameterDefinition {
214 name: "endpoint".into(),
215 type_hint: "string".into(),
216 description: "Endpoint name; netget-<id> when omitted".into(),
217 required: false,
218 example: json!("sensor-7"),
219 default: None,
220 },
221 ParameterDefinition {
222 name: "lifetime".into(),
223 type_hint: "number".into(),
224 description: "Registration lifetime in seconds; updates are sent at half of it"
225 .into(),
226 required: false,
227 example: json!(60),
228 default: Some(json!(super::DEFAULT_LIFETIME)),
229 },
230 ParameterDefinition {
231 name: "objects".into(),
232 type_hint: "array".into(),
233 description:
234 "The object instances to register, e.g. [\"/1/0\", \"/3/0\", \"/3303/0\"]"
235 .into(),
236 required: false,
237 example: json!(["/1/0", "/3/0", "/3303/0"]),
238 default: Some(json!(super::DEFAULT_OBJECTS)),
239 },
240 ]
241 }
242 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
243 use crate::protocol::metadata::*;
244 ProtocolMetadataV2::builder()
245 .state(DevelopmentState::Experimental)
246 .privilege_requirement(PrivilegeRequirement::None)
247 .implementation("The server's CoAP exchange and LwM2M content code as a device: registration with updates, discovery from the registered objects, and the handler's answers to every request")
248 .llm_control("The values the device reports, which writes, executes, creates and deletes it accepts, and when it notifies")
249 .e2e_testing("tests/client/lwm2m: the Eclipse Leshan 2.0.0-M15 server demo (independent, Java/Californium) reads, writes, executes and observes the device through its REST API")
250 .notes("No DTLS, OSCORE or bootstrap; SenML JSON, text and link format only; no block-wise transfer.")
251 .build()
252 }
253 fn example_prompt(&self) -> &'static str {
254 "Register with the LwM2M server at 127.0.0.1:5683 as a temperature sensor reading 21.5"
255 }
256 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
257 let llm = json!({"type":"open_client","protocol":"lwm2m","remote_addr":"127.0.0.1:5683","instruction":"Be a temperature sensor at 21.5 C","startup_params":{"objects":["/1/0","/3/0","/3303/0"]}});
258 let mut static_example = llm.clone();
259 static_example["event_handlers"] = json!([{"event_pattern":"lwm2m_read_request","handler":{"type":"static","actions":[{"type":"lwm2m_content","values":[{"path":"/3303/0/5700","value":21.5}]}]}}]);
260 let mut scripted = llm.clone();
261 scripted["event_handlers"] = json!([{"event_pattern":"lwm2m_read_request","handler":{"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'lwm2m_content','values':[{'path':e['path'],'value':21.5}]}]}))"}}]);
262 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
263 }
264 fn group_name(&self) -> &'static str {
265 "IoT"
266 }
267}
268
269impl Client for Lwm2mClientProtocol {
270 fn connect(
271 &self,
272 ctx: ConnectContext,
273 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
274 {
275 Box::pin(super::connect(ctx))
276 }
277 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
278 match v["type"].as_str() {
279 Some("disconnect") => return Ok(ClientActionResult::Disconnect),
280 Some("lwm2m_update" | "lwm2m_ok") => {}
281 Some("lwm2m_content") => {
282 content::senml_encode(v["values"].as_array().context("values is an array")?)?;
283 }
284 Some("lwm2m_notify") => {
285 ensure!(
286 v["path"].as_str().is_some_and(content::valid_path),
287 "path is an LwM2M path"
288 );
289 content::senml_encode(v["values"].as_array().context("values is an array")?)?;
290 }
291 Some("lwm2m_error") => ensure!(
292 v["code"].as_str().and_then(error_code).is_some(),
293 "code is not_found, method_not_allowed, bad_request or unauthorized"
294 ),
295 _ => bail!("Unknown LwM2M client action"),
296 }
297 Ok(ClientActionResult::Custom {
298 name: v["type"].as_str().unwrap_or_default().into(),
299 data: v,
300 })
301 }
302}