1use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct ServerAppConfig {
13 pub schema_version: u32,
14 pub device: DeviceConfig,
15 #[serde(default)]
16 pub hooks: HooksConfig,
17 pub memory_map: MemoryMap,
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize)]
21pub struct DeviceConfig {
22 pub name: String,
23 pub unit_id: u8,
24 pub response_timeout_ms: Option<u32>,
25}
26
27#[derive(Debug, Clone, Default, Deserialize, Serialize)]
30pub struct HooksConfig {
31 pub on_write_coil: Option<String>,
33 pub on_write_holding: Option<String>,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize, Default)]
38pub struct MemoryMap {
39 #[serde(default)]
40 pub coils: Vec<MapEntry>,
41 #[serde(default)]
42 pub discrete_inputs: Vec<MapEntry>,
43 #[serde(default)]
44 pub holding_registers: Vec<MapEntry>,
45 #[serde(default)]
46 pub input_registers: Vec<MapEntry>,
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
50pub struct MapEntry {
51 pub name: String,
52 pub address: u16,
53 pub access: Access,
54 #[serde(default)]
58 pub on_write: Option<String>,
59}
60
61#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
62#[serde(rename_all = "lowercase")]
63pub enum Access {
64 Ro,
65 Wo,
66 Rw,
67}
68
69impl Access {
70 pub fn is_readable(self) -> bool {
71 matches!(self, Access::Ro | Access::Rw)
72 }
73
74 pub fn is_writable(self) -> bool {
75 matches!(self, Access::Wo | Access::Rw)
76 }
77}
78
79pub fn parse_yaml(text: &str) -> Result<ServerAppConfig, String> {
83 serde_yaml::from_str(text).map_err(|e| format!("invalid YAML: {e}"))
84}
85
86pub fn validate_config(config: &ServerAppConfig) -> Result<(), String> {
88 if config.schema_version != 1 {
89 return Err(format!(
90 "unsupported schema_version {} (expected 1)",
91 config.schema_version
92 ));
93 }
94
95 validate_entries_unique("coils", &config.memory_map.coils)?;
96 validate_entries_unique("discrete_inputs", &config.memory_map.discrete_inputs)?;
97 validate_entries_unique("holding_registers", &config.memory_map.holding_registers)?;
98 validate_entries_unique("input_registers", &config.memory_map.input_registers)?;
99
100 for e in &config.memory_map.discrete_inputs {
101 if e.access.is_writable() {
102 return Err(format!(
103 "discrete_input '{}' at address {} cannot be writable (Modbus read-only)",
104 e.name, e.address
105 ));
106 }
107 }
108 for e in &config.memory_map.input_registers {
109 if e.access.is_writable() {
110 return Err(format!(
111 "input_register '{}' at address {} cannot be writable (Modbus read-only)",
112 e.name, e.address
113 ));
114 }
115 }
116
117 Ok(())
118}
119
120pub fn render_rust_dispatcher(config: &ServerAppConfig) -> String {
122 let coil_read_entries: Vec<&MapEntry> = config
123 .memory_map
124 .coils
125 .iter()
126 .filter(|e| e.access.is_readable())
127 .collect();
128 let coil_write_entries: Vec<&MapEntry> = config
129 .memory_map
130 .coils
131 .iter()
132 .filter(|e| e.access.is_writable())
133 .collect();
134 let discrete_read_entries: Vec<&MapEntry> = config
135 .memory_map
136 .discrete_inputs
137 .iter()
138 .filter(|e| e.access.is_readable())
139 .collect();
140 let holding_read_entries: Vec<&MapEntry> = config
141 .memory_map
142 .holding_registers
143 .iter()
144 .filter(|e| e.access.is_readable())
145 .collect();
146 let holding_write_entries: Vec<&MapEntry> = config
147 .memory_map
148 .holding_registers
149 .iter()
150 .filter(|e| e.access.is_writable())
151 .collect();
152 let input_read_entries: Vec<&MapEntry> = config
153 .memory_map
154 .input_registers
155 .iter()
156 .filter(|e| e.access.is_readable())
157 .collect();
158
159 let has_coils = !config.memory_map.coils.is_empty();
160 let has_discrete = !config.memory_map.discrete_inputs.is_empty();
161 let has_holding = !config.memory_map.holding_registers.is_empty();
162 let has_input = !config.memory_map.input_registers.is_empty();
163
164 let mut extern_lines: BTreeSet<String> = BTreeSet::new();
166 for entry in &coil_write_entries {
167 if let Some(sym) = resolve_on_write(entry, &config.hooks.on_write_coil) {
168 extern_lines.insert(format!(
169 " fn {sym}(ctx: *mut c_void, address: u16, value: u8) -> MbusHookStatus;"
170 ));
171 }
172 }
173 for entry in &holding_write_entries {
174 if let Some(sym) = resolve_on_write(entry, &config.hooks.on_write_holding) {
175 extern_lines.insert(format!(
176 " fn {sym}(ctx: *mut c_void, address: u16, value: u16) -> MbusHookStatus;"
177 ));
178 }
179 }
180
181 let mut out = String::new();
182
183 out.push_str("// @generated by mbus-codegen via build.rs. Do not edit manually.\n");
185 out.push_str(
186 "// Rust owns all register/coil state. C receives write-notification callbacks.\n",
187 );
188 out.push_str(
189 "// Uses the hand-written server FFI infrastructure (pool, transport, config).\n\n",
190 );
191
192 out.push_str("use core::ffi::c_void;\n");
194 out.push_str("use core::ptr;\n");
195 out.push_str("use crate::c::server::callbacks::*;\n");
196 let mut imports: Vec<&str> = vec!["modbus_app"];
197 if has_coils {
198 imports.push("CoilMap");
199 imports.push("CoilsModel");
200 }
201 if has_discrete {
202 imports.push("DiscreteInputMap");
203 imports.push("DiscreteInputsModel");
204 }
205 if has_holding {
206 imports.push("HoldingRegisterMap");
207 imports.push("HoldingRegistersModel");
208 }
209 if has_input {
210 imports.push("InputRegisterMap");
211 imports.push("InputRegistersModel");
212 }
213 imports.sort_unstable();
214 imports.dedup();
215 out.push_str(&format!("use mbus_server::{{{}}};\n\n", imports.join(", ")));
216
217 out.push_str("/// Status returned by write-notification hooks and FFI accessors.\n");
219 out.push_str("#[repr(C)]\n");
220 out.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n");
221 out.push_str("pub enum MbusHookStatus {\n");
222 out.push_str(" MbusHookOk = 0,\n");
223 out.push_str(" MbusHookIllegalDataAddress = 1,\n");
224 out.push_str(" MbusHookIllegalDataValue = 2,\n");
225 out.push_str(" MbusHookDeviceFailure = 3,\n");
226 out.push_str("}\n\n");
227
228 out.push_str("unsafe extern \"C\" {\n");
230 for line in &extern_lines {
231 out.push_str(line);
232 out.push('\n');
233 }
234 out.push_str(
235 " /// Acquire your RTOS mutex / critical section before Rust state is mutated.\n",
236 );
237 out.push_str(" /// Single-threaded bare-metal: implement as an empty function.\n");
238 out.push_str(" fn mbus_app_lock();\n");
239 out.push_str(" /// Release the lock acquired by mbus_app_lock().\n");
240 out.push_str(" fn mbus_app_unlock();\n");
241 out.push_str("}\n\n");
242
243 if has_coils {
245 out.push_str(&render_coils_model(&config.memory_map.coils));
246 out.push('\n');
247 }
248 if has_discrete {
249 out.push_str(&render_discrete_inputs_model(
250 &config.memory_map.discrete_inputs,
251 ));
252 out.push('\n');
253 }
254 if has_holding {
255 out.push_str(&render_holding_registers_model(
256 &config.memory_map.holding_registers,
257 ));
258 out.push('\n');
259 }
260 if has_input {
261 out.push_str(&render_input_registers_model(
262 &config.memory_map.input_registers,
263 ));
264 out.push('\n');
265 }
266
267 out.push_str(&render_app_struct(config));
269 out.push_str("\n\n");
270
271 out.push_str("static mut APP_MODEL: Option<AppModel> = None;\n\n");
273
274 if !coil_write_entries.is_empty() {
276 out.push_str(&render_write_dispatch_u8(
277 "dispatch_write_coil",
278 &coil_write_entries,
279 &config.hooks.on_write_coil,
280 ));
281 out.push('\n');
282 }
283 if !holding_write_entries.is_empty() {
284 out.push_str(&render_write_dispatch_u16(
285 "dispatch_write_holding",
286 &holding_write_entries,
287 &config.hooks.on_write_holding,
288 ));
289 out.push('\n');
290 }
291
292 out.push_str(&render_get_coil_ffi(&coil_read_entries, has_coils));
294 out.push('\n');
295 out.push_str(&render_set_coil_ffi(&coil_write_entries, has_coils));
296 out.push('\n');
297 out.push_str(&render_get_discrete_input_ffi(
298 &discrete_read_entries,
299 has_discrete,
300 ));
301 out.push('\n');
302 out.push_str(&render_get_holding_ffi(&holding_read_entries, has_holding));
303 out.push('\n');
304 out.push_str(&render_set_holding_ffi(&holding_write_entries, has_holding));
305 out.push('\n');
306 out.push_str(&render_get_input_ffi(&input_read_entries, has_input));
307 out.push('\n');
308
309 out.push_str(&render_hook_to_exception_code());
311 out.push('\n');
312
313 out.push_str(&render_server_handler_callbacks(config));
315 out.push('\n');
316
317 out.push_str(&render_model_init_and_default_handlers(config));
319 out.push('\n');
320
321 out.push_str(&render_named_ffi(config));
323
324 out
325}
326
327pub fn render_c_header(config: &ServerAppConfig) -> String {
329 let coil_write_entries: Vec<&MapEntry> = config
330 .memory_map
331 .coils
332 .iter()
333 .filter(|e| e.access.is_writable())
334 .collect();
335 let holding_write_entries: Vec<&MapEntry> = config
336 .memory_map
337 .holding_registers
338 .iter()
339 .filter(|e| e.access.is_writable())
340 .collect();
341 let has_coils = !config.memory_map.coils.is_empty();
342 let has_discrete = !config.memory_map.discrete_inputs.is_empty();
343 let has_holding = !config.memory_map.holding_registers.is_empty();
344 let has_input = !config.memory_map.input_registers.is_empty();
345
346 let mut write_hook_decls: BTreeSet<String> = BTreeSet::new();
347 for entry in &coil_write_entries {
348 if let Some(sym) = resolve_on_write(entry, &config.hooks.on_write_coil) {
349 write_hook_decls.insert(format!(
350 "MbusHookStatus {sym}(void* ctx, uint16_t address, uint8_t value);"
351 ));
352 }
353 }
354 for entry in &holding_write_entries {
355 if let Some(sym) = resolve_on_write(entry, &config.hooks.on_write_holding) {
356 write_hook_decls.insert(format!(
357 "MbusHookStatus {sym}(void* ctx, uint16_t address, uint16_t value);"
358 ));
359 }
360 }
361
362 let mut out = String::new();
363 out.push_str(
364 "/* @generated by mbus-codegen via xtask gen-server-app. Do not edit manually. */\n",
365 );
366 out.push_str("#ifndef MBUS_SERVER_APP_H\n");
367 out.push_str("#define MBUS_SERVER_APP_H\n\n");
368 out.push_str("#include <stdbool.h>\n");
369 out.push_str("#include <stdint.h>\n");
370 out.push_str("#include \"modbus_rs_server.h\"\n\n");
371 out.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n");
372
373 out.push_str("typedef enum MbusHookStatus {\n");
374 out.push_str(" MBUS_HOOK_OK = 0,\n");
375 out.push_str(" MBUS_HOOK_ILLEGAL_DATA_ADDRESS = 1,\n");
376 out.push_str(" MBUS_HOOK_ILLEGAL_DATA_VALUE = 2,\n");
377 out.push_str(" MBUS_HOOK_DEVICE_FAILURE = 3\n");
378 out.push_str("} MbusHookStatus;\n\n");
379
380 out.push_str("/*\n");
381 out.push_str(" * mbus_app_lock() / mbus_app_unlock()\n");
382 out.push_str(" *\n");
383 out.push_str(" * Protect concurrent access to the Modbus register state owned by Rust.\n");
384 out.push_str(" * Called around every state mutation (Modbus writes and named push APIs).\n");
385 out.push_str(" *\n");
386 out.push_str(" * Implementation guidance:\n");
387 out.push_str(" * RTOS / multi-threaded : acquire/release a mutex or binary semaphore.\n");
388 out.push_str(" * Single-threaded bare-metal : leave both functions empty.\n");
389 out.push_str(
390 " * Bare-metal with interrupts : enter/exit a critical section (disable/enable IRQ).\n",
391 );
392 out.push_str(" *\n");
393 out.push_str(" * Constraints:\n");
394 out.push_str(
395 " * - NOT called before write-notification hooks (hooks run outside the lock).\n",
396 );
397 out.push_str(" * Do NOT call mbus_server_get_* or mbus_server_set_* from inside a hook.\n");
398 out.push_str(" * - Must not be called recursively (not reentrant).\n");
399 out.push_str(" */\n");
400 out.push_str("void mbus_app_lock(void);\n");
401 out.push_str("void mbus_app_unlock(void);\n\n");
402
403 if !write_hook_decls.is_empty() {
404 out.push_str("/*\n");
405 out.push_str(" * Write-notification hooks\n");
406 out.push_str(" *\n");
407 out.push_str(" * Called BEFORE the value is stored in Rust state.\n");
408 out.push_str(" * Return MBUS_HOOK_OK to allow the write; any other value rejects it\n");
409 out.push_str(" * and leaves the Rust state unchanged.\n");
410 out.push_str(
411 " * Called WITHOUT the lock — do not call mbus_server_get_* / set_* from here.\n",
412 );
413 out.push_str(" */\n");
414 for decl in &write_hook_decls {
415 out.push_str(decl);
416 out.push('\n');
417 }
418 out.push('\n');
419 }
420
421 out.push_str("/*\n");
422 out.push_str(" * Address-based register/coil access.\n");
423 out.push_str(" * Useful for direct reads/writes outside the normal server flow.\n");
424 out.push_str(" * Set functions call the write-notification hook before storing.\n");
425 out.push_str(" */\n");
426 out.push_str(
427 "MbusHookStatus mbus_server_get_coil(void* ctx, uint16_t address, uint8_t* out_value);\n",
428 );
429 out.push_str(
430 "MbusHookStatus mbus_server_set_coil(void* ctx, uint16_t address, uint8_t value);\n",
431 );
432 out.push_str("MbusHookStatus mbus_server_get_discrete_input(void* ctx, uint16_t address, uint8_t* out_value);\n");
433 out.push_str("MbusHookStatus mbus_server_get_holding_register(void* ctx, uint16_t address, uint16_t* out_value);\n");
434 out.push_str("MbusHookStatus mbus_server_set_holding_register(void* ctx, uint16_t address, uint16_t value);\n");
435 out.push_str("MbusHookStatus mbus_server_get_input_register(void* ctx, uint16_t address, uint16_t* out_value);\n\n");
436
437 out.push_str("/*\n");
439 out.push_str(" * Server lifecycle — uses the standard mbus-ffi server infrastructure\n");
440 out.push_str(" * (MbusTransportCallbacks, MbusServerHandlers, MbusServerConfig).\n");
441 out.push_str(" *\n");
442 out.push_str(" * 1. mbus_server_model_init() — init Rust-owned register model\n");
443 out.push_str(
444 " * 2. mbus_server_default_handlers(ud) — get populated MbusServerHandlers struct\n",
445 );
446 out.push_str(
447 " * 3. mbus_tcp_server_new(&t, &h, &cfg) — create server via standard pool API\n",
448 );
449 out.push_str(" * 4. mbus_tcp_server_connect(id) — open transport\n");
450 out.push_str(" * 5. mbus_tcp_server_poll(id) — drive server state machine\n");
451 out.push_str(" * 6. mbus_tcp_server_disconnect(id) — close transport\n");
452 out.push_str(" * 7. mbus_tcp_server_free(id) — release pool slot\n");
453 out.push_str(" */\n");
454 out.push_str("void mbus_server_model_init(void);\n");
455 out.push_str("struct MbusServerHandlers mbus_server_default_handlers(void *userdata);\n\n");
456
457 out.push_str("/*\n");
459 out.push_str(" * Generated handler callbacks.\n");
460 out.push_str(" * These are automatically wired by mbus_server_default_handlers().\n");
461 out.push_str(" * Declared here for users who want to build MbusServerHandlers manually.\n");
462 out.push_str(" */\n");
463 if has_coils {
464 out.push_str("enum MbusServerExceptionCode mbus_gen_on_read_coils(\n");
465 out.push_str(" struct MbusServerReadCoilsReq *req, void *userdata);\n");
466 }
467 if !coil_write_entries.is_empty() {
468 out.push_str("enum MbusServerExceptionCode mbus_gen_on_write_single_coil(\n");
469 out.push_str(" const struct MbusServerWriteSingleCoilReq *req, void *userdata);\n");
470 out.push_str("enum MbusServerExceptionCode mbus_gen_on_write_multiple_coils(\n");
471 out.push_str(" const struct MbusServerWriteMultipleCoilsReq *req, void *userdata);\n");
472 }
473 if has_discrete {
474 out.push_str("enum MbusServerExceptionCode mbus_gen_on_read_discrete_inputs(\n");
475 out.push_str(" struct MbusServerReadDiscreteInputsReq *req, void *userdata);\n");
476 }
477 if has_holding {
478 out.push_str("enum MbusServerExceptionCode mbus_gen_on_read_holding_registers(\n");
479 out.push_str(" struct MbusServerReadHoldingRegistersReq *req, void *userdata);\n");
480 }
481 if !holding_write_entries.is_empty() {
482 out.push_str("enum MbusServerExceptionCode mbus_gen_on_write_single_register(\n");
483 out.push_str(" const struct MbusServerWriteSingleRegisterReq *req, void *userdata);\n");
484 out.push_str("enum MbusServerExceptionCode mbus_gen_on_write_multiple_registers(\n");
485 out.push_str(
486 " const struct MbusServerWriteMultipleRegistersReq *req, void *userdata);\n",
487 );
488 }
489 if has_input {
490 out.push_str("enum MbusServerExceptionCode mbus_gen_on_read_input_registers(\n");
491 out.push_str(" struct MbusServerReadInputRegistersReq *req, void *userdata);\n");
492 }
493 out.push('\n');
494
495 out.push_str("/*\n");
496 out.push_str(" * Named field accessors — push/pull values from your application code.\n");
497 out.push_str(" * Bypass write-notification hooks (app-side push, not a Modbus write).\n");
498 out.push_str(
499 " * Use to seed initial sensor values or read coil state from application code.\n",
500 );
501 out.push_str(" */\n");
502 for entry in &config.memory_map.coils {
503 let name = &entry.name;
504 out.push_str(&format!("void mbus_server_set_{name}(uint8_t value);\n"));
505 out.push_str(&format!(
506 "MbusHookStatus mbus_server_get_{name}(uint8_t* out_value);\n"
507 ));
508 }
509 for entry in &config.memory_map.discrete_inputs {
510 let name = &entry.name;
511 out.push_str(&format!("void mbus_server_set_{name}(uint8_t value);\n"));
512 out.push_str(&format!(
513 "MbusHookStatus mbus_server_get_{name}(uint8_t* out_value);\n"
514 ));
515 }
516 for entry in &config.memory_map.holding_registers {
517 let name = &entry.name;
518 out.push_str(&format!("void mbus_server_set_{name}(uint16_t value);\n"));
519 out.push_str(&format!(
520 "MbusHookStatus mbus_server_get_{name}(uint16_t* out_value);\n"
521 ));
522 }
523 for entry in &config.memory_map.input_registers {
524 let name = &entry.name;
525 out.push_str(&format!("void mbus_server_set_{name}(uint16_t value);\n"));
526 out.push_str(&format!(
527 "MbusHookStatus mbus_server_get_{name}(uint16_t* out_value);\n"
528 ));
529 }
530
531 out.push('\n');
532 out.push_str(&format!(
533 "/* Device: {} | Unit ID: {} */\n\n",
534 config.device.name, config.device.unit_id
535 ));
536 out.push_str("#ifdef __cplusplus\n}\n#endif\n\n");
537 out.push_str("#endif /* MBUS_SERVER_APP_H */\n");
538 out
539}
540
541fn validate_entries_unique(section: &str, entries: &[MapEntry]) -> Result<(), String> {
544 let mut seen = std::collections::BTreeSet::new();
545 for entry in entries {
546 if !seen.insert(entry.address) {
547 return Err(format!(
548 "duplicate address {} in section {}",
549 entry.address, section
550 ));
551 }
552 }
553 Ok(())
554}
555
556fn resolve_on_write<'a>(entry: &'a MapEntry, global: &'a Option<String>) -> Option<&'a str> {
557 entry.on_write.as_deref().or(global.as_deref())
558}
559
560fn render_coils_model(entries: &[MapEntry]) -> String {
561 let mut out = String::new();
562 out.push_str("#[derive(Debug, Default, CoilsModel)]\n");
563 out.push_str("pub struct AppCoils {\n");
564 for e in entries {
565 out.push_str(&format!(" #[coil(addr = {})]\n", e.address));
566 out.push_str(&format!(" pub {}: bool,\n", e.name));
567 }
568 out.push_str("}\n");
569 out
570}
571
572fn render_discrete_inputs_model(entries: &[MapEntry]) -> String {
573 let mut out = String::new();
574 out.push_str("#[derive(Debug, Default, DiscreteInputsModel)]\n");
575 out.push_str("pub struct AppDiscreteInputs {\n");
576 for e in entries {
577 out.push_str(&format!(" #[discrete_input(addr = {})]\n", e.address));
578 out.push_str(&format!(" pub {}: bool,\n", e.name));
579 }
580 out.push_str("}\n");
581 out
582}
583
584fn render_holding_registers_model(entries: &[MapEntry]) -> String {
585 let mut out = String::new();
586 out.push_str("#[derive(Debug, Default, HoldingRegistersModel)]\n");
587 out.push_str("pub struct AppHolding {\n");
588 for e in entries {
589 out.push_str(&format!(" #[reg(addr = {})]\n", e.address));
590 out.push_str(&format!(" pub {}: u16,\n", e.name));
591 }
592 out.push_str("}\n");
593 out
594}
595
596fn render_input_registers_model(entries: &[MapEntry]) -> String {
597 let mut out = String::new();
598 out.push_str("#[derive(Debug, Default, InputRegistersModel)]\n");
599 out.push_str("pub struct AppInput {\n");
600 for e in entries {
601 out.push_str(&format!(" #[reg(addr = {})]\n", e.address));
602 out.push_str(&format!(" pub {}: u16,\n", e.name));
603 }
604 out.push_str("}\n");
605 out
606}
607
608fn render_app_struct(config: &ServerAppConfig) -> String {
609 let has_coils = !config.memory_map.coils.is_empty();
610 let has_discrete = !config.memory_map.discrete_inputs.is_empty();
611 let has_holding = !config.memory_map.holding_registers.is_empty();
612 let has_input = !config.memory_map.input_registers.is_empty();
613
614 let mut macro_args: Vec<String> = vec![];
615 if has_coils {
616 macro_args.push("coils(coils)".to_string());
617 }
618 if has_discrete {
619 macro_args.push("discrete_inputs(discrete_inputs)".to_string());
620 }
621 if has_holding {
622 macro_args.push("holding_registers(holding)".to_string());
623 }
624 if has_input {
625 macro_args.push("input_registers(input)".to_string());
626 }
627
628 let mut out = String::new();
629 out.push_str(&format!(
630 "/// Generated server app for: {}\n",
631 config.device.name
632 ));
633 out.push_str("#[derive(Debug, Default)]\n");
634 out.push_str(&format!("#[modbus_app({})]\n", macro_args.join(", ")));
635 out.push_str("pub struct AppModel {\n");
636 if has_coils {
637 out.push_str(" pub coils: AppCoils,\n");
638 }
639 if has_discrete {
640 out.push_str(" pub discrete_inputs: AppDiscreteInputs,\n");
641 }
642 if has_holding {
643 out.push_str(" pub holding: AppHolding,\n");
644 }
645 if has_input {
646 out.push_str(" pub input: AppInput,\n");
647 }
648 out.push('}');
649 out
650}
651
652fn render_write_dispatch_u8(name: &str, entries: &[&MapEntry], global: &Option<String>) -> String {
653 let mut out = String::new();
654 out.push_str(&format!(
655 "#[inline(always)]\nfn {name}(ctx: *mut c_void, address: u16, value: u8) -> MbusHookStatus {{\n"
656 ));
657 out.push_str(" match address {\n");
658 for entry in entries {
659 match resolve_on_write(entry, global) {
660 Some(sym) => {
661 out.push_str(&format!(
662 " {} => unsafe {{ {sym}(ctx, address, value) }},\n",
663 entry.address
664 ));
665 }
666 None => {
667 out.push_str(&format!(
668 " {} => MbusHookStatus::MbusHookOk,\n",
669 entry.address
670 ));
671 }
672 }
673 }
674 out.push_str(" _ => MbusHookStatus::MbusHookIllegalDataAddress,\n");
675 out.push_str(" }\n}\n");
676 out
677}
678
679fn render_write_dispatch_u16(name: &str, entries: &[&MapEntry], global: &Option<String>) -> String {
680 let mut out = String::new();
681 out.push_str(&format!(
682 "#[inline(always)]\nfn {name}(ctx: *mut c_void, address: u16, value: u16) -> MbusHookStatus {{\n"
683 ));
684 out.push_str(" match address {\n");
685 for entry in entries {
686 match resolve_on_write(entry, global) {
687 Some(sym) => {
688 out.push_str(&format!(
689 " {} => unsafe {{ {sym}(ctx, address, value) }},\n",
690 entry.address
691 ));
692 }
693 None => {
694 out.push_str(&format!(
695 " {} => MbusHookStatus::MbusHookOk,\n",
696 entry.address
697 ));
698 }
699 }
700 }
701 out.push_str(" _ => MbusHookStatus::MbusHookIllegalDataAddress,\n");
702 out.push_str(" }\n}\n");
703 out
704}
705
706fn render_get_coil_ffi(entries: &[&MapEntry], has_coils: bool) -> String {
707 let mut out = String::new();
708 out.push_str("#[unsafe(no_mangle)]\n");
709 out.push_str("pub extern \"C\" fn mbus_server_get_coil(\n");
710 out.push_str(" _ctx: *mut c_void,\n");
711 out.push_str(" address: u16,\n");
712 out.push_str(" out_value: *mut u8,\n");
713 out.push_str(") -> MbusHookStatus {\n");
714 if !has_coils || entries.is_empty() {
715 out.push_str(" let _ = (address, out_value);\n");
716 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress\n");
717 } else {
718 out.push_str(
719 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
720 );
721 out.push_str(" unsafe {\n");
722 out.push_str(" mbus_app_lock();\n");
723 out.push_str(" let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().and_then(|app| match address {\n");
724 for entry in entries {
725 out.push_str(&format!(
726 " {} => Some(app.coils.{} as u8),\n",
727 entry.address, entry.name
728 ));
729 }
730 out.push_str(" _ => None,\n");
731 out.push_str(" });\n");
732 out.push_str(" mbus_app_unlock();\n");
733 out.push_str(" match val {\n");
734 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
735 out.push_str(" None => MbusHookStatus::MbusHookIllegalDataAddress,\n");
736 out.push_str(" }\n");
737 out.push_str(" }\n");
738 }
739 out.push_str("}\n");
740 out
741}
742
743fn render_set_coil_ffi(entries: &[&MapEntry], has_coils: bool) -> String {
744 let mut out = String::new();
745 out.push_str("#[unsafe(no_mangle)]\n");
746 out.push_str("pub extern \"C\" fn mbus_server_set_coil(\n");
747 out.push_str(" ctx: *mut c_void,\n");
748 out.push_str(" address: u16,\n");
749 out.push_str(" value: u8,\n");
750 out.push_str(") -> MbusHookStatus {\n");
751 if !has_coils || entries.is_empty() {
752 out.push_str(" let _ = (ctx, address, value);\n");
753 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress\n");
754 } else {
755 out.push_str(" let st = dispatch_write_coil(ctx, address, value);\n");
756 out.push_str(" if st != MbusHookStatus::MbusHookOk { return st; }\n");
757 out.push_str(" unsafe {\n");
758 out.push_str(" mbus_app_lock();\n");
759 out.push_str(
760 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {\n",
761 );
762 out.push_str(" match address {\n");
763 for entry in entries {
764 out.push_str(&format!(
765 " {} => app.coils.{} = value != 0,\n",
766 entry.address, entry.name
767 ));
768 }
769 out.push_str(" _ => {}\n");
770 out.push_str(" }\n");
771 out.push_str(" }\n");
772 out.push_str(" mbus_app_unlock();\n");
773 out.push_str(" }\n");
774 out.push_str(" MbusHookStatus::MbusHookOk\n");
775 }
776 out.push_str("}\n");
777 out
778}
779
780fn render_get_discrete_input_ffi(entries: &[&MapEntry], has_discrete: bool) -> String {
781 let mut out = String::new();
782 out.push_str("#[unsafe(no_mangle)]\n");
783 out.push_str("pub extern \"C\" fn mbus_server_get_discrete_input(\n");
784 out.push_str(" _ctx: *mut c_void,\n");
785 out.push_str(" address: u16,\n");
786 out.push_str(" out_value: *mut u8,\n");
787 out.push_str(") -> MbusHookStatus {\n");
788 if !has_discrete || entries.is_empty() {
789 out.push_str(" let _ = (address, out_value);\n");
790 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress\n");
791 } else {
792 out.push_str(
793 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
794 );
795 out.push_str(" unsafe {\n");
796 out.push_str(" mbus_app_lock();\n");
797 out.push_str(" let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().and_then(|app| match address {\n");
798 for entry in entries {
799 out.push_str(&format!(
800 " {} => Some(app.discrete_inputs.{} as u8),\n",
801 entry.address, entry.name
802 ));
803 }
804 out.push_str(" _ => None,\n");
805 out.push_str(" });\n");
806 out.push_str(" mbus_app_unlock();\n");
807 out.push_str(" match val {\n");
808 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
809 out.push_str(" None => MbusHookStatus::MbusHookIllegalDataAddress,\n");
810 out.push_str(" }\n");
811 out.push_str(" }\n");
812 }
813 out.push_str("}\n");
814 out
815}
816
817fn render_get_holding_ffi(entries: &[&MapEntry], has_holding: bool) -> String {
818 let mut out = String::new();
819 out.push_str("#[unsafe(no_mangle)]\n");
820 out.push_str("pub extern \"C\" fn mbus_server_get_holding_register(\n");
821 out.push_str(" _ctx: *mut c_void,\n");
822 out.push_str(" address: u16,\n");
823 out.push_str(" out_value: *mut u16,\n");
824 out.push_str(") -> MbusHookStatus {\n");
825 if !has_holding || entries.is_empty() {
826 out.push_str(" let _ = (address, out_value);\n");
827 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress\n");
828 } else {
829 out.push_str(
830 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
831 );
832 out.push_str(" unsafe {\n");
833 out.push_str(" mbus_app_lock();\n");
834 out.push_str(" let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().and_then(|app| match address {\n");
835 for entry in entries {
836 out.push_str(&format!(
837 " {} => Some(app.holding.{}()),\n",
838 entry.address, entry.name
839 ));
840 }
841 out.push_str(" _ => None,\n");
842 out.push_str(" });\n");
843 out.push_str(" mbus_app_unlock();\n");
844 out.push_str(" match val {\n");
845 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
846 out.push_str(" None => MbusHookStatus::MbusHookIllegalDataAddress,\n");
847 out.push_str(" }\n");
848 out.push_str(" }\n");
849 }
850 out.push_str("}\n");
851 out
852}
853
854fn render_set_holding_ffi(entries: &[&MapEntry], has_holding: bool) -> String {
855 let mut out = String::new();
856 out.push_str("#[unsafe(no_mangle)]\n");
857 out.push_str("pub extern \"C\" fn mbus_server_set_holding_register(\n");
858 out.push_str(" ctx: *mut c_void,\n");
859 out.push_str(" address: u16,\n");
860 out.push_str(" value: u16,\n");
861 out.push_str(") -> MbusHookStatus {\n");
862 if !has_holding || entries.is_empty() {
863 out.push_str(" let _ = (ctx, address, value);\n");
864 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress\n");
865 } else {
866 out.push_str(" let st = dispatch_write_holding(ctx, address, value);\n");
867 out.push_str(" if st != MbusHookStatus::MbusHookOk { return st; }\n");
868 out.push_str(" unsafe {\n");
869 out.push_str(" mbus_app_lock();\n");
870 out.push_str(
871 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {\n",
872 );
873 out.push_str(" match address {\n");
874 for entry in entries {
875 out.push_str(&format!(
876 " {} => app.holding.set_{}(value),\n",
877 entry.address, entry.name
878 ));
879 }
880 out.push_str(" _ => {}\n");
881 out.push_str(" }\n");
882 out.push_str(" }\n");
883 out.push_str(" mbus_app_unlock();\n");
884 out.push_str(" }\n");
885 out.push_str(" MbusHookStatus::MbusHookOk\n");
886 }
887 out.push_str("}\n");
888 out
889}
890
891fn render_get_input_ffi(entries: &[&MapEntry], has_input: bool) -> String {
892 let mut out = String::new();
893 out.push_str("#[unsafe(no_mangle)]\n");
894 out.push_str("pub extern \"C\" fn mbus_server_get_input_register(\n");
895 out.push_str(" _ctx: *mut c_void,\n");
896 out.push_str(" address: u16,\n");
897 out.push_str(" out_value: *mut u16,\n");
898 out.push_str(") -> MbusHookStatus {\n");
899 if !has_input || entries.is_empty() {
900 out.push_str(" let _ = (address, out_value);\n");
901 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress\n");
902 } else {
903 out.push_str(
904 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
905 );
906 out.push_str(" unsafe {\n");
907 out.push_str(" mbus_app_lock();\n");
908 out.push_str(" let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().and_then(|app| match address {\n");
909 for entry in entries {
910 out.push_str(&format!(
911 " {} => Some(app.input.{}()),\n",
912 entry.address, entry.name
913 ));
914 }
915 out.push_str(" _ => None,\n");
916 out.push_str(" });\n");
917 out.push_str(" mbus_app_unlock();\n");
918 out.push_str(" match val {\n");
919 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
920 out.push_str(" None => MbusHookStatus::MbusHookIllegalDataAddress,\n");
921 out.push_str(" }\n");
922 out.push_str(" }\n");
923 }
924 out.push_str("}\n");
925 out
926}
927
928fn render_hook_to_exception_code() -> String {
929 let mut out = String::new();
930 out.push_str("#[inline(always)]\n");
931 out.push_str("fn hook_to_exception(st: MbusHookStatus) -> MbusServerExceptionCode {\n");
932 out.push_str(" match st {\n");
933 out.push_str(" MbusHookStatus::MbusHookOk => MbusServerExceptionCode::Ok,\n");
934 out.push_str(" MbusHookStatus::MbusHookIllegalDataAddress => MbusServerExceptionCode::IllegalDataAddress,\n");
935 out.push_str(" MbusHookStatus::MbusHookIllegalDataValue => MbusServerExceptionCode::IllegalDataValue,\n");
936 out.push_str(" _ => MbusServerExceptionCode::ServerDeviceFailure,\n");
937 out.push_str(" }\n");
938 out.push_str("}\n");
939 out
940}
941
942fn render_server_handler_callbacks(config: &ServerAppConfig) -> String {
943 let coil_write_entries: Vec<&MapEntry> = config
944 .memory_map
945 .coils
946 .iter()
947 .filter(|e| e.access.is_writable())
948 .collect();
949 let holding_write_entries: Vec<&MapEntry> = config
950 .memory_map
951 .holding_registers
952 .iter()
953 .filter(|e| e.access.is_writable())
954 .collect();
955 let has_coils = !config.memory_map.coils.is_empty();
956 let has_discrete = !config.memory_map.discrete_inputs.is_empty();
957 let has_holding = !config.memory_map.holding_registers.is_empty();
958 let has_input = !config.memory_map.input_registers.is_empty();
959
960 let mut out = String::new();
961 out.push_str(
962 "// ---------------------------------------------------------------------------\n",
963 );
964 out.push_str("// Handler callbacks matching MbusServerHandlers signatures.\n");
965 out.push_str(
966 "// Wire these into a MbusServerHandlers struct or use mbus_server_default_handlers().\n",
967 );
968 out.push_str(
969 "// ---------------------------------------------------------------------------\n\n",
970 );
971
972 if has_coils {
974 out.push_str("/// Handler for FC 0x01 — Read Coils. Reads from model state.\n");
975 out.push_str("#[unsafe(no_mangle)]\n");
976 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_read_coils(\n");
977 out.push_str(" req: *mut MbusServerReadCoilsReq,\n");
978 out.push_str(" _userdata: *mut c_void,\n");
979 out.push_str(") -> MbusServerExceptionCode {\n");
980 out.push_str(
981 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
982 );
983 out.push_str(" let req = unsafe { &mut *req };\n");
984 out.push_str(" unsafe {\n");
985 out.push_str(" mbus_app_lock();\n");
986 out.push_str(" let result = (&*ptr::addr_of!(APP_MODEL))\n");
987 out.push_str(" .as_ref()\n");
988 out.push_str(" .and_then(|app| {\n");
989 out.push_str(" let out = core::slice::from_raw_parts_mut(req.out_data, req.out_data_len);\n");
990 out.push_str(" app.coils.encode(req.address, req.quantity, out).ok()\n");
991 out.push_str(" });\n");
992 out.push_str(" mbus_app_unlock();\n");
993 out.push_str(" match result {\n");
994 out.push_str(
995 " Some(n) => { req.out_byte_count = n; MbusServerExceptionCode::Ok }\n",
996 );
997 out.push_str(" None => MbusServerExceptionCode::IllegalDataAddress,\n");
998 out.push_str(" }\n");
999 out.push_str(" }\n");
1000 out.push_str("}\n\n");
1001 }
1002
1003 if !coil_write_entries.is_empty() {
1005 out.push_str("/// Handler for FC 0x05 — Write Single Coil.\n");
1006 out.push_str("#[unsafe(no_mangle)]\n");
1007 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_write_single_coil(\n");
1008 out.push_str(" req: *const MbusServerWriteSingleCoilReq,\n");
1009 out.push_str(" userdata: *mut c_void,\n");
1010 out.push_str(") -> MbusServerExceptionCode {\n");
1011 out.push_str(
1012 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1013 );
1014 out.push_str(" let req = unsafe { &*req };\n");
1015 out.push_str(" let st = dispatch_write_coil(userdata, req.address, req.value as u8);\n");
1016 out.push_str(" if st != MbusHookStatus::MbusHookOk { return hook_to_exception(st); }\n");
1017 out.push_str(" unsafe {\n");
1018 out.push_str(" mbus_app_lock();\n");
1019 out.push_str(
1020 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {\n",
1021 );
1022 out.push_str(" let _ = app.coils.write_single(req.address, req.value);\n");
1023 out.push_str(" }\n");
1024 out.push_str(" mbus_app_unlock();\n");
1025 out.push_str(" }\n");
1026 out.push_str(" MbusServerExceptionCode::Ok\n");
1027 out.push_str("}\n\n");
1028
1029 out.push_str("/// Handler for FC 0x0F — Write Multiple Coils.\n");
1031 out.push_str("#[unsafe(no_mangle)]\n");
1032 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_write_multiple_coils(\n");
1033 out.push_str(" req: *const MbusServerWriteMultipleCoilsReq,\n");
1034 out.push_str(" userdata: *mut c_void,\n");
1035 out.push_str(") -> MbusServerExceptionCode {\n");
1036 out.push_str(
1037 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1038 );
1039 out.push_str(" let req = unsafe { &*req };\n");
1040 out.push_str(" let values = unsafe { core::slice::from_raw_parts(req.values, req.values_len) };\n");
1041 out.push_str(" for i in 0..req.quantity {\n");
1042 out.push_str(" let byte_idx = i as usize / 8;\n");
1043 out.push_str(" let bit = if byte_idx < values.len() { (values[byte_idx] >> (i % 8)) & 1 } else { 0 };\n");
1044 out.push_str(" let st = dispatch_write_coil(userdata, req.starting_address.wrapping_add(i), bit);\n");
1045 out.push_str(
1046 " if st != MbusHookStatus::MbusHookOk { return hook_to_exception(st); }\n",
1047 );
1048 out.push_str(" }\n");
1049 out.push_str(" unsafe {\n");
1050 out.push_str(" mbus_app_lock();\n");
1051 out.push_str(
1052 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {\n",
1053 );
1054 out.push_str(" let _ = app.coils.write_many_from_packed(\n");
1055 out.push_str(" req.starting_address, req.quantity, values, 0,\n");
1056 out.push_str(" );\n");
1057 out.push_str(" }\n");
1058 out.push_str(" mbus_app_unlock();\n");
1059 out.push_str(" }\n");
1060 out.push_str(" MbusServerExceptionCode::Ok\n");
1061 out.push_str("}\n\n");
1062 }
1063
1064 if has_discrete {
1066 out.push_str("/// Handler for FC 0x02 — Read Discrete Inputs.\n");
1067 out.push_str("#[unsafe(no_mangle)]\n");
1068 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_read_discrete_inputs(\n");
1069 out.push_str(" req: *mut MbusServerReadDiscreteInputsReq,\n");
1070 out.push_str(" _userdata: *mut c_void,\n");
1071 out.push_str(") -> MbusServerExceptionCode {\n");
1072 out.push_str(
1073 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1074 );
1075 out.push_str(" let req = unsafe { &mut *req };\n");
1076 out.push_str(" unsafe {\n");
1077 out.push_str(" mbus_app_lock();\n");
1078 out.push_str(" let result = (&*ptr::addr_of!(APP_MODEL))\n");
1079 out.push_str(" .as_ref()\n");
1080 out.push_str(" .and_then(|app| {\n");
1081 out.push_str(" let out = core::slice::from_raw_parts_mut(req.out_data, req.out_data_len);\n");
1082 out.push_str(
1083 " app.discrete_inputs.encode(req.address, req.quantity, out).ok()\n",
1084 );
1085 out.push_str(" });\n");
1086 out.push_str(" mbus_app_unlock();\n");
1087 out.push_str(" match result {\n");
1088 out.push_str(
1089 " Some(n) => { req.out_byte_count = n; MbusServerExceptionCode::Ok }\n",
1090 );
1091 out.push_str(" None => MbusServerExceptionCode::IllegalDataAddress,\n");
1092 out.push_str(" }\n");
1093 out.push_str(" }\n");
1094 out.push_str("}\n\n");
1095 }
1096
1097 if has_holding {
1099 out.push_str("/// Handler for FC 0x03 — Read Holding Registers.\n");
1100 out.push_str("#[unsafe(no_mangle)]\n");
1101 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_read_holding_registers(\n");
1102 out.push_str(" req: *mut MbusServerReadHoldingRegistersReq,\n");
1103 out.push_str(" _userdata: *mut c_void,\n");
1104 out.push_str(") -> MbusServerExceptionCode {\n");
1105 out.push_str(
1106 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1107 );
1108 out.push_str(" let req = unsafe { &mut *req };\n");
1109 out.push_str(" unsafe {\n");
1110 out.push_str(" mbus_app_lock();\n");
1111 out.push_str(" let result = (&*ptr::addr_of!(APP_MODEL))\n");
1112 out.push_str(" .as_ref()\n");
1113 out.push_str(" .and_then(|app| {\n");
1114 out.push_str(" let out = core::slice::from_raw_parts_mut(req.out_data, req.out_data_len);\n");
1115 out.push_str(" app.holding.encode(req.address, req.quantity, out).ok()\n");
1116 out.push_str(" });\n");
1117 out.push_str(" mbus_app_unlock();\n");
1118 out.push_str(" match result {\n");
1119 out.push_str(
1120 " Some(n) => { req.out_byte_count = n; MbusServerExceptionCode::Ok }\n",
1121 );
1122 out.push_str(" None => MbusServerExceptionCode::IllegalDataAddress,\n");
1123 out.push_str(" }\n");
1124 out.push_str(" }\n");
1125 out.push_str("}\n\n");
1126 }
1127
1128 if !holding_write_entries.is_empty() {
1130 out.push_str("/// Handler for FC 0x06 — Write Single Register.\n");
1131 out.push_str("#[unsafe(no_mangle)]\n");
1132 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_write_single_register(\n");
1133 out.push_str(" req: *const MbusServerWriteSingleRegisterReq,\n");
1134 out.push_str(" userdata: *mut c_void,\n");
1135 out.push_str(") -> MbusServerExceptionCode {\n");
1136 out.push_str(
1137 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1138 );
1139 out.push_str(" let req = unsafe { &*req };\n");
1140 out.push_str(" let st = dispatch_write_holding(userdata, req.address, req.value);\n");
1141 out.push_str(" if st != MbusHookStatus::MbusHookOk { return hook_to_exception(st); }\n");
1142 out.push_str(" unsafe {\n");
1143 out.push_str(" mbus_app_lock();\n");
1144 out.push_str(
1145 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {\n",
1146 );
1147 out.push_str(" let _ = app.holding.write_single(req.address, req.value);\n");
1148 out.push_str(" }\n");
1149 out.push_str(" mbus_app_unlock();\n");
1150 out.push_str(" }\n");
1151 out.push_str(" MbusServerExceptionCode::Ok\n");
1152 out.push_str("}\n\n");
1153
1154 out.push_str("/// Handler for FC 0x10 — Write Multiple Registers.\n");
1156 out.push_str("#[unsafe(no_mangle)]\n");
1157 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_write_multiple_registers(\n");
1158 out.push_str(" req: *const MbusServerWriteMultipleRegistersReq,\n");
1159 out.push_str(" userdata: *mut c_void,\n");
1160 out.push_str(") -> MbusServerExceptionCode {\n");
1161 out.push_str(
1162 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1163 );
1164 out.push_str(" let req = unsafe { &*req };\n");
1165 out.push_str(" let values = unsafe { core::slice::from_raw_parts(req.values, req.values_len) };\n");
1166 out.push_str(" for (i, &v) in values.iter().enumerate() {\n");
1167 out.push_str(" let addr = req.starting_address.wrapping_add(i as u16);\n");
1168 out.push_str(" let st = dispatch_write_holding(userdata, addr, v);\n");
1169 out.push_str(
1170 " if st != MbusHookStatus::MbusHookOk { return hook_to_exception(st); }\n",
1171 );
1172 out.push_str(" }\n");
1173 out.push_str(" unsafe {\n");
1174 out.push_str(" mbus_app_lock();\n");
1175 out.push_str(
1176 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {\n",
1177 );
1178 out.push_str(" let _ = app.holding.write_many(req.starting_address, values);\n");
1179 out.push_str(" }\n");
1180 out.push_str(" mbus_app_unlock();\n");
1181 out.push_str(" }\n");
1182 out.push_str(" MbusServerExceptionCode::Ok\n");
1183 out.push_str("}\n\n");
1184 }
1185
1186 if has_input {
1188 out.push_str("/// Handler for FC 0x04 — Read Input Registers.\n");
1189 out.push_str("#[unsafe(no_mangle)]\n");
1190 out.push_str("pub unsafe extern \"C\" fn mbus_gen_on_read_input_registers(\n");
1191 out.push_str(" req: *mut MbusServerReadInputRegistersReq,\n");
1192 out.push_str(" _userdata: *mut c_void,\n");
1193 out.push_str(") -> MbusServerExceptionCode {\n");
1194 out.push_str(
1195 " if req.is_null() { return MbusServerExceptionCode::ServerDeviceFailure; }\n",
1196 );
1197 out.push_str(" let req = unsafe { &mut *req };\n");
1198 out.push_str(" unsafe {\n");
1199 out.push_str(" mbus_app_lock();\n");
1200 out.push_str(" let result = (&*ptr::addr_of!(APP_MODEL))\n");
1201 out.push_str(" .as_ref()\n");
1202 out.push_str(" .and_then(|app| {\n");
1203 out.push_str(" let out = core::slice::from_raw_parts_mut(req.out_data, req.out_data_len);\n");
1204 out.push_str(" app.input.encode(req.address, req.quantity, out).ok()\n");
1205 out.push_str(" });\n");
1206 out.push_str(" mbus_app_unlock();\n");
1207 out.push_str(" match result {\n");
1208 out.push_str(
1209 " Some(n) => { req.out_byte_count = n; MbusServerExceptionCode::Ok }\n",
1210 );
1211 out.push_str(" None => MbusServerExceptionCode::IllegalDataAddress,\n");
1212 out.push_str(" }\n");
1213 out.push_str(" }\n");
1214 out.push_str("}\n\n");
1215 }
1216
1217 out
1218}
1219
1220fn render_model_init_and_default_handlers(config: &ServerAppConfig) -> String {
1221 let coil_write_entries: Vec<&MapEntry> = config
1222 .memory_map
1223 .coils
1224 .iter()
1225 .filter(|e| e.access.is_writable())
1226 .collect();
1227 let holding_write_entries: Vec<&MapEntry> = config
1228 .memory_map
1229 .holding_registers
1230 .iter()
1231 .filter(|e| e.access.is_writable())
1232 .collect();
1233 let has_coils = !config.memory_map.coils.is_empty();
1234 let has_discrete = !config.memory_map.discrete_inputs.is_empty();
1235 let has_holding = !config.memory_map.holding_registers.is_empty();
1236 let has_input = !config.memory_map.input_registers.is_empty();
1237
1238 let mut out = String::new();
1239
1240 out.push_str("/// Initialise the generated Modbus register/coil model.\n");
1241 out.push_str("///\n");
1242 out.push_str(
1243 "/// Must be called once before creating the server with `mbus_tcp_server_new` or\n",
1244 );
1245 out.push_str("/// `mbus_serial_server_new`.\n");
1246 out.push_str("#[unsafe(no_mangle)]\n");
1247 out.push_str("pub extern \"C\" fn mbus_server_model_init() {\n");
1248 out.push_str(" unsafe { APP_MODEL = Some(AppModel::default()); }\n");
1249 out.push_str("}\n\n");
1250
1251 out.push_str("/// Returns a fully-populated `MbusServerHandlers` struct pointing to the\n");
1252 out.push_str("/// generated handler callbacks. Pass the returned struct to\n");
1253 out.push_str("/// `mbus_tcp_server_new` or `mbus_serial_server_new`.\n");
1254 out.push_str("///\n");
1255 out.push_str(
1256 "/// `userdata` is forwarded to write-notification hooks as their first argument.\n",
1257 );
1258 out.push_str("#[unsafe(no_mangle)]\n");
1259 out.push_str("pub extern \"C\" fn mbus_server_default_handlers(\n");
1260 out.push_str(" userdata: *mut c_void,\n");
1261 out.push_str(") -> MbusServerHandlers {\n");
1262 out.push_str(" MbusServerHandlers {\n");
1263 out.push_str(" userdata,\n");
1264
1265 if has_coils {
1270 out.push_str(" on_read_coils: Some(mbus_gen_on_read_coils),\n");
1271 } else {
1272 out.push_str(" #[cfg(feature = \"coils\")]\n");
1273 out.push_str(" on_read_coils: None,\n");
1274 }
1275 if !coil_write_entries.is_empty() {
1276 out.push_str(" on_write_single_coil: Some(mbus_gen_on_write_single_coil),\n");
1277 out.push_str(" on_write_multiple_coils: Some(mbus_gen_on_write_multiple_coils),\n");
1278 } else {
1279 out.push_str(" #[cfg(feature = \"coils\")]\n");
1280 out.push_str(" on_write_single_coil: None,\n");
1281 out.push_str(" #[cfg(feature = \"coils\")]\n");
1282 out.push_str(" on_write_multiple_coils: None,\n");
1283 }
1284 if has_discrete {
1285 out.push_str(" on_read_discrete_inputs: Some(mbus_gen_on_read_discrete_inputs),\n");
1286 } else {
1287 out.push_str(" #[cfg(feature = \"discrete-inputs\")]\n");
1288 out.push_str(" on_read_discrete_inputs: None,\n");
1289 }
1290 if has_holding {
1291 out.push_str(
1292 " on_read_holding_registers: Some(mbus_gen_on_read_holding_registers),\n",
1293 );
1294 } else {
1295 out.push_str(" #[cfg(feature = \"registers\")]\n");
1296 out.push_str(" on_read_holding_registers: None,\n");
1297 }
1298 if !holding_write_entries.is_empty() {
1299 out.push_str(
1300 " on_write_single_register: Some(mbus_gen_on_write_single_register),\n",
1301 );
1302 out.push_str(
1303 " on_write_multiple_registers: Some(mbus_gen_on_write_multiple_registers),\n",
1304 );
1305 } else {
1306 out.push_str(" #[cfg(feature = \"registers\")]\n");
1307 out.push_str(" on_write_single_register: None,\n");
1308 out.push_str(" #[cfg(feature = \"registers\")]\n");
1309 out.push_str(" on_write_multiple_registers: None,\n");
1310 }
1311 out.push_str(" #[cfg(feature = \"registers\")]\n");
1312 out.push_str(" on_mask_write_register: None,\n");
1313 out.push_str(" #[cfg(feature = \"registers\")]\n");
1314 out.push_str(" on_read_write_multiple_registers: None,\n");
1315 if has_input {
1316 out.push_str(" on_read_input_registers: Some(mbus_gen_on_read_input_registers),\n");
1317 } else {
1318 out.push_str(" #[cfg(feature = \"registers\")]\n");
1319 out.push_str(" on_read_input_registers: None,\n");
1320 }
1321 out.push_str(" #[cfg(feature = \"fifo\")]\n");
1322 out.push_str(" on_read_fifo_queue: None,\n");
1323 out.push_str(" #[cfg(feature = \"file-record\")]\n");
1324 out.push_str(" on_read_file_record: None,\n");
1325 out.push_str(" #[cfg(feature = \"file-record\")]\n");
1326 out.push_str(" on_write_file_record: None,\n");
1327 out.push_str(" #[cfg(feature = \"diagnostics\")]\n");
1328 out.push_str(" on_read_exception_status: None,\n");
1329 out.push_str(" #[cfg(feature = \"diagnostics\")]\n");
1330 out.push_str(" on_diagnostics: None,\n");
1331 out.push_str(" #[cfg(feature = \"diagnostics\")]\n");
1332 out.push_str(" on_get_comm_event_counter: None,\n");
1333 out.push_str(" #[cfg(feature = \"diagnostics\")]\n");
1334 out.push_str(" on_get_comm_event_log: None,\n");
1335 out.push_str(" #[cfg(feature = \"diagnostics\")]\n");
1336 out.push_str(" on_report_server_id: None,\n");
1337 out.push_str(" #[cfg(feature = \"diagnostics\")]\n");
1338 out.push_str(" on_read_device_identification: None,\n");
1339 out.push_str(" }\n");
1340 out.push_str("}\n");
1341 out
1342}
1343
1344fn render_named_ffi(config: &ServerAppConfig) -> String {
1345 let mut out = String::new();
1346 out.push_str("// Named field accessors — push/pull values from your application code.\n");
1347 out.push_str(
1348 "// These bypass write-notification hooks (app-side push, not a Modbus write).\n\n",
1349 );
1350
1351 for entry in &config.memory_map.coils {
1352 let name = &entry.name;
1353 out.push_str("#[unsafe(no_mangle)]\n");
1354 out.push_str(&format!(
1355 "pub extern \"C\" fn mbus_server_set_{name}(value: u8) {{\n"
1356 ));
1357 out.push_str(" unsafe {\n");
1358 out.push_str(" mbus_app_lock();\n");
1359 out.push_str(&format!(
1360 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {{ app.coils.{name} = value != 0; }}\n"
1361 ));
1362 out.push_str(" mbus_app_unlock();\n");
1363 out.push_str(" }\n");
1364 out.push_str("}\n\n");
1365 out.push_str("#[unsafe(no_mangle)]\n");
1366 out.push_str(&format!(
1367 "pub extern \"C\" fn mbus_server_get_{name}(out_value: *mut u8) -> MbusHookStatus {{\n"
1368 ));
1369 out.push_str(
1370 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
1371 );
1372 out.push_str(" unsafe {\n");
1373 out.push_str(" mbus_app_lock();\n");
1374 out.push_str(&format!(
1375 " let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().map(|app| app.coils.{name} as u8);\n"
1376 ));
1377 out.push_str(" mbus_app_unlock();\n");
1378 out.push_str(" match val {\n");
1379 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
1380 out.push_str(" None => MbusHookStatus::MbusHookDeviceFailure,\n");
1381 out.push_str(" }\n");
1382 out.push_str(" }\n");
1383 out.push_str("}\n\n");
1384 }
1385
1386 for entry in &config.memory_map.discrete_inputs {
1387 let name = &entry.name;
1388 out.push_str("#[unsafe(no_mangle)]\n");
1389 out.push_str(&format!(
1390 "pub extern \"C\" fn mbus_server_set_{name}(value: u8) {{\n"
1391 ));
1392 out.push_str(" unsafe {\n");
1393 out.push_str(" mbus_app_lock();\n");
1394 out.push_str(&format!(
1395 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {{ app.discrete_inputs.{name} = value != 0; }}\n"
1396 ));
1397 out.push_str(" mbus_app_unlock();\n");
1398 out.push_str(" }\n");
1399 out.push_str("}\n\n");
1400 out.push_str("#[unsafe(no_mangle)]\n");
1401 out.push_str(&format!(
1402 "pub extern \"C\" fn mbus_server_get_{name}(out_value: *mut u8) -> MbusHookStatus {{\n"
1403 ));
1404 out.push_str(
1405 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
1406 );
1407 out.push_str(" unsafe {\n");
1408 out.push_str(" mbus_app_lock();\n");
1409 out.push_str(&format!(
1410 " let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().map(|app| app.discrete_inputs.{name} as u8);\n"
1411 ));
1412 out.push_str(" mbus_app_unlock();\n");
1413 out.push_str(" match val {\n");
1414 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
1415 out.push_str(" None => MbusHookStatus::MbusHookDeviceFailure,\n");
1416 out.push_str(" }\n");
1417 out.push_str(" }\n");
1418 out.push_str("}\n\n");
1419 }
1420
1421 for entry in &config.memory_map.holding_registers {
1422 let name = &entry.name;
1423 out.push_str("#[unsafe(no_mangle)]\n");
1424 out.push_str(&format!(
1425 "pub extern \"C\" fn mbus_server_set_{name}(value: u16) {{\n"
1426 ));
1427 out.push_str(" unsafe {\n");
1428 out.push_str(" mbus_app_lock();\n");
1429 out.push_str(&format!(
1430 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {{ app.holding.set_{name}(value); }}\n"
1431 ));
1432 out.push_str(" mbus_app_unlock();\n");
1433 out.push_str(" }\n");
1434 out.push_str("}\n\n");
1435 out.push_str("#[unsafe(no_mangle)]\n");
1436 out.push_str(&format!(
1437 "pub extern \"C\" fn mbus_server_get_{name}(out_value: *mut u16) -> MbusHookStatus {{\n"
1438 ));
1439 out.push_str(
1440 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
1441 );
1442 out.push_str(" unsafe {\n");
1443 out.push_str(" mbus_app_lock();\n");
1444 out.push_str(&format!(
1445 " let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().map(|app| app.holding.{name}());\n"
1446 ));
1447 out.push_str(" mbus_app_unlock();\n");
1448 out.push_str(" match val {\n");
1449 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
1450 out.push_str(" None => MbusHookStatus::MbusHookDeviceFailure,\n");
1451 out.push_str(" }\n");
1452 out.push_str(" }\n");
1453 out.push_str("}\n\n");
1454 }
1455
1456 for entry in &config.memory_map.input_registers {
1457 let name = &entry.name;
1458 out.push_str("#[unsafe(no_mangle)]\n");
1459 out.push_str(&format!(
1460 "pub extern \"C\" fn mbus_server_set_{name}(value: u16) {{\n"
1461 ));
1462 out.push_str(" unsafe {\n");
1463 out.push_str(" mbus_app_lock();\n");
1464 out.push_str(&format!(
1465 " if let Some(app) = (&mut *ptr::addr_of_mut!(APP_MODEL)).as_mut() {{ app.input.set_{name}(value); }}\n"
1466 ));
1467 out.push_str(" mbus_app_unlock();\n");
1468 out.push_str(" }\n");
1469 out.push_str("}\n\n");
1470 out.push_str("#[unsafe(no_mangle)]\n");
1471 out.push_str(&format!(
1472 "pub extern \"C\" fn mbus_server_get_{name}(out_value: *mut u16) -> MbusHookStatus {{\n"
1473 ));
1474 out.push_str(
1475 " if out_value.is_null() { return MbusHookStatus::MbusHookDeviceFailure; }\n",
1476 );
1477 out.push_str(" unsafe {\n");
1478 out.push_str(" mbus_app_lock();\n");
1479 out.push_str(&format!(
1480 " let val = (&*ptr::addr_of!(APP_MODEL)).as_ref().map(|app| app.input.{name}());\n"
1481 ));
1482 out.push_str(" mbus_app_unlock();\n");
1483 out.push_str(" match val {\n");
1484 out.push_str(" Some(v) => { *out_value = v; MbusHookStatus::MbusHookOk }\n");
1485 out.push_str(" None => MbusHookStatus::MbusHookDeviceFailure,\n");
1486 out.push_str(" }\n");
1487 out.push_str(" }\n");
1488 out.push_str("}\n\n");
1489 }
1490
1491 out
1492}