Skip to main content

mbus_codegen/
lib.rs

1//! Code-generation utilities for the mbus-ffi server-app layer.
2//!
3//! Used by both `mbus-ffi/build.rs` (as a build-dependency) and `xtask`
4//! (as a regular dependency) so the logic lives in exactly one place.
5
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8
9// ── Config types ──────────────────────────────────────────────────────────
10
11#[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/// Optional global write-notification hook fallbacks.
28/// Per-entry `on_write` takes priority over these.
29#[derive(Debug, Clone, Default, Deserialize, Serialize)]
30pub struct HooksConfig {
31    /// Global coil write-notification fallback (used when no per-entry on_write).
32    pub on_write_coil: Option<String>,
33    /// Global holding-register write-notification fallback.
34    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    /// Write-notification callback. Called BEFORE the value is stored in Rust.
55    /// Return `MBUS_HOOK_OK` to allow the write; anything else rejects it.
56    /// If absent, Modbus writes to this address succeed silently.
57    #[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
79// ── Public API ────────────────────────────────────────────────────────────
80
81/// Parse a YAML server-app config.
82pub fn parse_yaml(text: &str) -> Result<ServerAppConfig, String> {
83    serde_yaml::from_str(text).map_err(|e| format!("invalid YAML: {e}"))
84}
85
86/// Validate a parsed config, returning a human-readable error on failure.
87pub 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
120/// Generate the Rust dispatcher source that implements the server app model.
121pub 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    // Collect extern "C" declarations for write-notification hooks only.
165    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    // File header
184    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    // use imports (only what is needed)
193    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    // MbusHookStatus enum
218    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    // extern "C" block
229    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    // Model structs
244    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    // AppModel struct with #[modbus_app]
268    out.push_str(&render_app_struct(config));
269    out.push_str("\n\n");
270
271    // Static state — APP_MODEL is initialised in mbus_server_model_init.
272    out.push_str("static mut APP_MODEL: Option<AppModel> = None;\n\n");
273
274    // Write dispatch helpers
275    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    // Address-based FFI exports
293    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    // Hook-to-exception-code helper
310    out.push_str(&render_hook_to_exception_code());
311    out.push('\n');
312
313    // Handler callback functions matching MbusServerHandlers signatures
314    out.push_str(&render_server_handler_callbacks(config));
315    out.push('\n');
316
317    // Model init + default handlers convenience
318    out.push_str(&render_model_init_and_default_handlers(config));
319    out.push('\n');
320
321    // Named field FFI exports
322    out.push_str(&render_named_ffi(config));
323
324    out
325}
326
327/// Generate the C header declaring the app-layer API.
328pub 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    // Server model init + default handlers
438    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    // Generated handler callbacks (for users who want to build MbusServerHandlers manually)
458    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
541// ── Private rendering helpers ─────────────────────────────────────────────
542
543fn 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    // FC01 — Read Coils
973    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    // FC05 — Write Single Coil
1004    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        // FC0F — Write Multiple Coils
1030        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    // FC02 — Read Discrete Inputs
1065    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    // FC03 — Read Holding Registers
1098    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    // FC06 — Write Single Register
1129    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        // FC10 — Write Multiple Registers
1155        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    // FC04 — Read Input Registers
1187    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    // Each field must be gated by its feature so the generated code compiles
1266    // when building with a minimal feature set (e.g. `--features c-server,coils`).
1267    // The `else` branches emit `None` with a `#[cfg]` gate because the field
1268    // does not exist in `MbusServerHandlers` when the feature is disabled.
1269    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}