oms_modbus/server.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Modbus server (slave) service trait.
3
4use async_trait::async_trait;
5
6use crate::frame::{Exception, Request, Response};
7
8/// A Modbus server service — handles incoming requests and produces responses.
9///
10/// Implement this trait to create a custom Modbus server. The transport loops
11/// (TCP, RTU, ASCII) call [`call`](Service::call) for each decoded request.
12///
13/// For hooking into the request/response lifecycle without reimplementing the
14/// entire trait, see [`ServerHook`] and [`HookedService`].
15///
16/// # Example
17///
18/// ```no_run
19/// use async_trait::async_trait;
20/// use oms_modbus::frame::{Request, Response, Exception};
21/// use oms_modbus::server::Service;
22///
23/// /// A fixed-value service — always returns the same register value.
24/// struct FixedService { value: u16 }
25///
26/// #[async_trait]
27/// impl Service for FixedService {
28/// async fn call(&self, request: Request<'_>) -> Result<Response, Exception> {
29/// match request {
30/// Request::ReadHoldingRegisters(_addr, qty) =>
31/// Ok(Response::ReadHoldingRegisters(vec![self.value; qty as usize])),
32/// _ => Err(Exception::IllegalFunction),
33/// }
34/// }
35/// }
36/// ```
37#[async_trait]
38pub trait Service: Send + Sync + 'static {
39 async fn call(&self, request: Request<'_>) -> Result<Response, Exception>;
40}
41
42// ── Server hook support ─────────────────────────────────────────────────────
43
44/// Thread-local request context — set by `process_server_request` before
45/// calling [`Service::call`]. Hooks use this to see which slave the request
46/// is addressed to without changing the `Service` trait signature.
47#[doc(hidden)]
48pub mod context {
49 tokio::task_local! {
50 pub static SLAVE_ID: u8;
51 }
52}
53
54/// A hook that intercepts server requests — compose with [`HookedService`].
55///
56/// All methods have default no-op implementations. Implement only the hooks
57/// you need.
58///
59/// # Examples
60///
61/// ```no_run
62/// use oms_modbus::*;
63/// use async_trait::async_trait;
64///
65/// // Log every request/response pair
66/// struct LogHook;
67/// #[async_trait]
68/// impl ServerHook for LogHook {
69/// async fn after_call(
70/// &self, slave: u8,
71/// result: Result<Response, Exception>,
72/// ) -> Result<Response, Exception> {
73/// println!("[slave={slave}] {:?}", result);
74/// result
75/// }
76/// }
77/// ```
78#[async_trait]
79pub trait ServerHook: Send + Sync + 'static {
80 /// Called before the inner service. Return `Some(response)` to skip the
81 /// inner service entirely (short-circuit). Return `None` to proceed.
82 async fn before_call(&self, _slave: u8, _request: &Request<'_>) -> Option<Response> {
83 None
84 }
85
86 /// Called after the inner service produces a result. Transform or replace
87 /// the response before it is sent to the client.
88 async fn after_call(
89 &self,
90 _slave: u8,
91 result: Result<Response, Exception>,
92 ) -> Result<Response, Exception> {
93 result
94 }
95}
96
97/// A [`Service`] wrapper that applies a [`ServerHook`] around another service.
98///
99/// # Examples
100///
101/// ```no_run
102/// use oms_modbus::*;
103/// use std::sync::Arc;
104///
105/// # async fn example() {
106/// let store = Arc::new(SlaveStore::new());
107/// # struct DummyHook; #[async_trait::async_trait] impl ServerHook for DummyHook {}
108/// let hook = DummyHook;
109/// let hooked = HookedService::new(store, hook);
110/// // Pass `hooked` to any serve_forever() — it implements Service.
111/// # }
112/// ```
113#[derive(Clone)]
114pub struct HookedService<S, H> {
115 pub inner: S,
116 pub hook: H,
117}
118
119impl<S, H> HookedService<S, H> {
120 /// Wrap a service with a hook.
121 pub fn new(inner: S, hook: H) -> Self {
122 Self { inner, hook }
123 }
124}
125
126#[async_trait]
127impl<S, H> Service for HookedService<S, H>
128where
129 S: Service,
130 H: ServerHook,
131{
132 async fn call(&self, request: Request<'_>) -> Result<Response, Exception> {
133 // Fallback to 0xFF (outside valid 0–247 range) when SLAVE_ID
134 // task-local isn't set — lets hooks distinguish "not set" from
135 // the broadcast address (0) or a real slave (1–247).
136 let slave = context::SLAVE_ID.try_with(|&id| id).unwrap_or(0xFF);
137
138 // 1. before_call — may short-circuit the inner service
139 if let Some(rsp) = self.hook.before_call(slave, &request).await {
140 return Ok(rsp);
141 }
142
143 // 2. Call the inner service
144 let result = self.inner.call(request).await;
145
146 // 3. after_call — transform or pass through
147 self.hook.after_call(slave, result).await
148 }
149}