Skip to main content

netget/client/lwm2m/
actions.rs

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
165/// The error code an `lwm2m_error` names.
166pub 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}