melsec_mc 0.2.3

A small Rust library to talk to Mitsubishi PLCs via MC Protocol (Ethernet) - transport layer and helpers
Documentation
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;

fn main() {
    // If generated files are already checked into the repo, avoid regenerating them
    // during `cargo package`/`cargo publish` because Cargo copies the source tree
    // into `target/package` and build scripts must not modify files outside OUT_DIR.
    // To force regeneration locally set the env var `REGENERATE_DEVICES=1`.
    let regen = env::var("REGENERATE_DEVICES").is_ok();
    let gen_a = Path::new("src/device_code_gen.rs");
    let gen_b = Path::new("src/devices_gen.rs");
    if !regen && gen_a.exists() && gen_b.exists() {
        println!("cargo:warning=found generated files; skipping generation (set REGENERATE_DEVICES=1 to force)");
        println!("cargo:rerun-if-changed=src/devices.toml");
        return;
    }

    // Read canonical list from src/devices.toml
    let s = std::fs::read_to_string("src/devices.toml").expect("failed to read src/devices.toml");
    let doc: toml::Value = toml::from_str(&s).expect("failed to parse devices.toml");
    let devices = doc.get("device").and_then(|v| v.as_array()).expect("missing [[device]] entries");

    // Build DeviceCode enum and name tables
    let mut code_out = String::new();
    code_out.push_str("// THIS FILE IS AUTO-GENERATED BY build.rs - do not edit by hand\n");
    code_out.push_str("#[repr(u8)]\n");
    code_out.push_str("#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\n");
    code_out.push_str("pub enum DeviceCode {\n");
    for t in devices.iter() {
        let sym = t.get("symbol").and_then(|v| v.as_str()).expect("symbol");
        let code = t.get("code").and_then(|v| v.as_integer()).expect("code");
        code_out.push_str(&format!("    {} = 0x{:02X},\n", sym, code));
    }
    code_out.push_str("}\n\n");

    // CODE_NAME_PAIRS
    code_out.push_str("static CODE_NAME_PAIRS: &[(&str, DeviceCode)] = &[\n");
    for t in devices.iter() {
        let sym = t.get("symbol").and_then(|v| v.as_str()).unwrap();
        code_out.push_str(&format!("    (\"{}\", DeviceCode::{}),\n", sym, sym));
    }
    code_out.push_str("];\n\n");

    // NAME_BY_CODE and CODE_BY_NAME use Lazy/HashMap but those imports are in src/device.rs
    code_out.push_str("static CODE_BY_NAME: once_cell::sync::Lazy<std::collections::HashMap<&str, DeviceCode>> = once_cell::sync::Lazy::new(|| {\n");
    code_out.push_str("    let mut m = std::collections::HashMap::with_capacity(CODE_NAME_PAIRS.len());\n");
    code_out.push_str("    for &(s, code) in CODE_NAME_PAIRS.iter() {\n");
    code_out.push_str("        m.insert(s, code);\n");
    code_out.push_str("    }\n");
    code_out.push_str("    m\n});\n\n");

    code_out.push_str("static NAME_BY_CODE: once_cell::sync::Lazy<std::collections::HashMap<DeviceCode, &str>> = once_cell::sync::Lazy::new(|| {\n");
    code_out.push_str("    let mut m = std::collections::HashMap::with_capacity(CODE_NAME_PAIRS.len());\n");
    code_out.push_str("    for &(s, code) in CODE_NAME_PAIRS.iter() {\n");
    code_out.push_str("        m.insert(code, s);\n");
    code_out.push_str("    }\n");
    code_out.push_str("    m\n});\n\n");

    // impl DeviceCode conversions
    code_out.push_str("impl DeviceCode {\n    pub fn as_str(&self) -> &'static str { NAME_BY_CODE.get(self).copied().expect(\"missing name for DeviceCode\") }\n}\n\n");

    // From<DeviceCode> for u8
    code_out.push_str("impl From<DeviceCode> for u8 { fn from(dc: DeviceCode) -> Self { dc as u8 } }\n\n");

    // TryFrom<u8>
    code_out.push_str("impl TryFrom<u8> for DeviceCode { type Error = crate::error::MelsecError; fn try_from(v: u8) -> Result<Self, Self::Error> { match v {\n");
    for t in devices.iter() {
        let sym = t.get("symbol").and_then(|v| v.as_str()).unwrap();
        let code = t.get("code").and_then(|v| v.as_integer()).unwrap();
        code_out.push_str(&format!("    0x{:02X} => Ok(DeviceCode::{}),\n", code, sym));
    }
    code_out.push_str("    other => Err(crate::error::MelsecError::Protocol(format!(\"unknown device code: 0x{:02X}\", other))),\n    } } }\n");

    // Write device_code_gen.rs
    let out_path = Path::new("src/device_code_gen.rs");
    let mut f = File::create(out_path).expect("failed to create device_code_gen.rs");
    f.write_all(code_out.as_bytes()).expect("failed to write device_code_gen.rs");

    // Generate devices_gen.rs (DEVICES array)
    let mut dev_out = String::new();
    dev_out.push_str("// THIS FILE IS AUTO-GENERATED BY build.rs - do not edit by hand\n");
    dev_out.push_str("static DEVICES: &[crate::device::Device] = &[\n");
    for t in devices.iter() {
        let sym = t.get("symbol").and_then(|v| v.as_str()).unwrap();
        let cat = t.get("category").and_then(|v| v.as_str()).unwrap();
        let base = t.get("base").and_then(|v| v.as_str()).unwrap();
        let desc = t.get("description").and_then(|v| v.as_str()).unwrap();
        // Read series array if present, otherwise fall back to legacy `is_q` bool.
        let series_vec: Vec<String> = if let Some(arr) = t.get("series").and_then(|v| v.as_array()) {
            arr.iter().filter_map(|it| it.as_str().map(|s| s.to_string())).collect()
        } else if let Some(b) = t.get("is_q").and_then(|v| v.as_bool()) {
            if b { vec!["Q".to_string()] } else { vec![] }
        } else {
            vec![]
        };

        // Map to PLCSeries enum variant tokens. Unknown series names -> panic (fail fast).
        let mut series_tokens: Vec<String> = Vec::new();
        for s in series_vec.iter() {
            match s.as_str() {
                "Q" => series_tokens.push("crate::device::PLCSeries::Q".to_string()),
                "R" => series_tokens.push("crate::device::PLCSeries::R".to_string()),
                other => panic!("unknown series '{}' in devices.toml for symbol {}", other, sym),
            }
        }

        let series_literal = if series_tokens.is_empty() {
            "&[]".to_string()
        } else {
            format!("&[{}]", series_tokens.join(", "))
        };

        dev_out.push_str("    crate::device::Device {\n");
        dev_out.push_str(&format!("        category: crate::device::DeviceType::{},\n", cat));
        dev_out.push_str(&format!("        base: crate::device::NumberBase::{},\n", base));
        dev_out.push_str(&format!("        device_code: DeviceCode::{},\n", sym));
        dev_out.push_str(&format!("        description: \"{}\",\n", desc));
        dev_out.push_str(&format!("        supported_series: {},\n", series_literal));
        dev_out.push_str("    },\n");
    }
    dev_out.push_str("];\n\n");

    // Write devices_gen.rs
    let out_path2 = Path::new("src/devices_gen.rs");
    let mut f2 = File::create(out_path2).expect("failed to create devices_gen.rs");
    f2.write_all(dev_out.as_bytes()).expect("failed to write devices_gen.rs");

    // Inform Cargo to rerun build if this file changes
    println!("cargo:rerun-if-changed=src/devices.toml");
}