Protocols

Siemens S7 PLCs

Siemens S7-300, S7-400, S7-1200 and S7-1500 PLCs over ISO-on-TCP, in pure Rust: data blocks, inputs, outputs, merkers, timers and counters read and written as typed values in STEP 7 addresses, many per call split to the PDU, and the CPU's mode and identity.

Version 1.0.0 · 14 functions · 0 crates

#plc#industrial#devices#network#iot

Functions

What use s7.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use s7_connect, s7_read from s7 (Quillo 0.6).

s7_connect(name: str, host: str, rack: int, slot: int, options: any) → anyor an error

Opens a connection named name to the PLC at host (port 102, or host:port) by rack and slot, or keeps the one open under that name with the same settings (call it before each use); options: timeout_ms (5000), connection ("pg", "op" or "basic"), pdu_size (960 asked), local_tsap and remote_tsap (for LOGO! and S7-200, e.g. "02.00").

s7_read(name: str, addresses: list) → anyor an error

Reads several addresses at once (DB1.DBX0.0, DB1.DBW2:INT, DB1.DBD4:REAL, DB1.DBB12:STRING[20], DB2.DBD0:REAL[10], IW64, Q0.1, MD20:DINT, T1, C1), in as few requests as the PDU allows: a map from each address to its value. One that fails fails the call; s7_read_each gives an error per address instead.

s7_read_each(name: str, addresses: list) → listor an error

Reads several addresses, each giving { address, value } or { address, error }, so one missing data block does not hide the rest.

s7_get(name: str, address: str) → anyor an error

Reads one address: its value.

s7_write(name: str, values: map) → anyor an error

Writes values to addresses ({ "DB1.DBW34:INT": 1500, "M0.1": true, "DB1.DBB12:STRING[20]": "Line 3" }): every value is checked against its type before anything is sent; then { written }, the addresses written. Bits are written one by one, never by rewriting their byte.

s7_read_area(name: str, area: str, start: int, count: int) → listor an error

Reads count raw bytes of an area ("DB5", "I", "Q", "M", "P"; for "T" and "C", count timers or counters, two bytes each) from byte start: a list of byte values.

s7_write_area(name: str, area: str, start: int, bytes: list) → intor an error

Writes raw bytes to an area from byte start (big data blocks split to the PDU): how many written.

s7_szl(name: str, id: int, index: int) → anyor an error

Reads a system status list by its ID and index (e.g. 0x0011 = 17 for module identification, 0x001C = 28 for component identification, 0x0424 = 1060 for the mode): its records as hex.

s7_cpu_state(name: str) → anyor an error

The CPU's operating mode: { state: "run" | "stop" | "startup" | "hold" | "defect" | "unknown", running, code, previous } (from SZL 0x0424; previous is null when the CPU does not say).

s7_cpu_info(name: str) → anyor an error

The CPU's identity: order code and firmware (SZL 0x0011), and its names, serial number and module type (SZL 0x001C); what a CPU does not offer is left out.

s7_close(name: str) → bool

Closes the named connection: true when there was one.

s7_address(address: str) → anyor an error

What an address means, without a PLC: { area, db, byte, bit, type, count, bytes }, or why it is wrong.

s7_decode(kind: str, bytes: list) → anyor an error

A typed value from its bytes as the PLC keeps them (big-endian): s7_decode("REAL", [66, 145, 0, 0]) is 72.5.

s7_encode(kind: str, value: any) → listor an error

A typed value as the bytes the PLC keeps (big-endian): s7_encode("INT", -2) is [255, 254].

Get it

$ quillo install s7               # s7.qwer into this folder$ quillo install s7 --example     # and its example, s7.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/s7.qwer$ curl -O https://quillolang.org/plugins/files/s7.qwe

Then, in your program, all of it or only what you call:

main.qwe
use s7.qweruse s7_connect, s7_read from s7      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

s7.qwe
// A filling line's PLC, a Siemens S7-1200 at rack 0, slot 1: the line's state read in one// call (bits, INT, REAL, DINT, a STRING, inputs, outputs, merkers, a timer and a counter), a// new set point written and read back, a recipe longer than one PDU, the CPU's mode and// identity, a sample every second kept for a trend, and what a wrong address says.//// It needs a PLC: S7_HOST (with S7_RACK and S7_SLOT, 0 and 1 unless given). On an S7-1200 or// S7-1500, allow PUT/GET communication in the CPU's protection settings and turn off// "optimized block access" for the data blocks you read.////   S7_HOST=192.168.0.10 quillo run s7.qwe      then open http://localhost:8080/line//   quillo run s7.qwe      and http://localhost:8080/offline (no PLC needed)use "s7.qwer"fn plc() {    let host = env("S7_HOST", null) ?? err("set S7_HOST to the PLC's address (host or host:port)")    // The first call connects; the next ones keep that connection (and the plugin reconnects    // by itself when the PLC restarts or a cable comes back).    s7_connect("line3", host, int(env("S7_RACK", "0")), int(env("S7_SLOT", "1")), { timeout_ms: 3000 })}route GET /line {    plc()?    let v = s7_read("line3", [        "DB1.DBX0.0", "DB1.DBX0.1", "DB1.DBW2:INT", "DB1.DBD4:REAL", "DB1.DBD8:DINT",        "DB1.DBB12:STRING[20]", "DB1.DBB36:DT", "I0.0", "I0.1", "Q0.0", "MW10:INT", "MD20:REAL", "T1", "C1",    ])?    {        running: v["DB1.DBX0.0"], fault: v["DB1.DBX0.1"], speed: v["DB1.DBW2:INT"],        temperature: v["DB1.DBD4:REAL"], bottles: v["DB1.DBD8:DINT"], name: v["DB1.DBB12:STRING[20]"],        shift_start: v["DB1.DBB36:DT"], start_button: v["I0.0"], stop_button: v["I0.1"], motor: v["Q0.0"],        batches: v["MW10:INT"], level: v["MD20:REAL"], fill_timer_ms: v["T1"], rejects: v["C1"],    }}// POST {"speed": 1600}: the set point and the motor written in one call, then read back.route POST /setpoint { json: { speed: int } } {    plc()?    let done = s7_write("line3", { "DB1.DBW34:INT": speed, "Q0.0": true, "M0.1": true })?    { written: done.written, setpoint: s7_get("line3", "DB1.DBW34:INT")?, motor: s7_get("line3", "Q0.0")? }}// 242 bytes, more than a 240-byte PDU holds: the plugin splits the read.route GET /recipe {    plc()?    let r = s7_read("line3", ["DB2.DBD0:REAL[10]", "DB2.DBB40:STRING[200]", "DB2.DBD242:TIME", "DB2.DBW246:DATE"])?    { weights: r["DB2.DBD0:REAL[10]"], text: r["DB2.DBB40:STRING[200]"], mix_ms: r["DB2.DBD242:TIME"], made: r["DB2.DBW246:DATE"] }}route GET /cpu {    plc()?    { mode: s7_cpu_state("line3")?, info: s7_cpu_info("line3")?, raw_inputs: s7_read_area("line3", "I", 0, 4)? }}// What goes wrong says what to do: a missing data block, an address past the end, a typo,// a write to a block the PLC protects, a string too long, and the wrong slot.route GET /errors {    plc()?    {        each: s7_read_each("line3", ["DB99.DBW0:INT", "DB1.DBW100:INT", "DB1.DBW2:REAL", "DB1.DBW2:INT"])?,        protected: s7_write("line3", { "DB3.DBB0": 1 }).error,        too_long: s7_write("line3", { "DB1.DBB12:STRING[20]": "a name longer than twenty" }).error,        wrong_slot: s7_connect("other", env("S7_HOST"), 0, 3, { timeout_ms: 2000 }).error,    }}// A sample every second, the last 60 kept: a trend without a historian.let trend = []fn sample() {    if !plc() { return null }    let v = s7_read("line3", ["DB1.DBD8:DINT", "DB1.DBD4:REAL"])    if v {        trend = slice(trend + [{ at: now(), bottles: v["DB1.DBD8:DINT"], temperature: v["DB1.DBD4:REAL"] }], -60)    }}every(1000, sample)route GET /trend = { samples: len(trend), last: trend[len(trend) - 1] ?? null }// No PLC needed: what addresses mean, and values as the PLC keeps them.route GET /offline = {    address: s7_address("DB1.DBB12:STRING[20]")?,    bit: s7_address("E1.7")?,    real: s7_decode("REAL", [66, 145, 0, 0])?,    int_bytes: s7_encode("INT", -2)?,    dt: s7_decode("DT", s7_encode("DT", "2026-10-30T08:15:00.250")?)?,    wrong: s7_address("DB1.DBW4:REAL").error,}
$ quillo run s7.qwe$ curl 'localhost:8080/line'$ curl 'localhost:8080/setpoint'$ curl 'localhost:8080/recipe'$ curl 'localhost:8080/cpu'$ curl 'localhost:8080/errors'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

s7.qwer
//! Siemens S7 PLCs (S7-300, S7-400, S7-1200, S7-1500, ET 200SP, LOGO! 8) over ISO-on-TCP (RFC 1006, COTP, S7comm), in pure Rust: connect by rack and slot, read and write data blocks, inputs, outputs, merkers, timers and counters as typed values (BOOL, INT, DINT, REAL, STRING, DATE_AND_TIME and more, arrays too), many addresses per call split to the PLC's PDU size, and the CPU's state, order code and identity.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Map, Value};use std::collections::HashMap;use std::io::{Read, Write};use std::net::{SocketAddr, TcpStream, ToSocketAddrs};use std::sync::{Arc, Mutex, OnceLock};use std::time::Duration;// ---------------------------------------------------------------------------------------------// Addresses and typesconst AREA_P: u8 = 0x80; // peripheral I/Oconst AREA_I: u8 = 0x81;const AREA_Q: u8 = 0x82;const AREA_M: u8 = 0x83;const AREA_DB: u8 = 0x84;const AREA_C: u8 = 0x1C;const AREA_T: u8 = 0x1D;#[derive(Clone, Debug, PartialEq)]enum Ty {    Bool,    Byte,    Word,    DWord,    LWord,    Char,    SInt,    USInt,    Int,    UInt,    DInt,    UDInt,    LInt,    ULInt,    Real,    LReal,    Str(usize),    WStr(usize),    Time,    S5Time,    Date,    Tod,    Dt,    Dtl,    Timer,    Counter,}impl Ty {    fn size(&self) -> usize {        match self {            Ty::Bool => 1,            Ty::Byte | Ty::Char | Ty::SInt | Ty::USInt => 1,            Ty::Word | Ty::Int | Ty::UInt | Ty::S5Time | Ty::Date | Ty::Timer | Ty::Counter => 2,            Ty::DWord | Ty::DInt | Ty::UDInt | Ty::Real | Ty::Time | Ty::Tod => 4,            Ty::LWord | Ty::LInt | Ty::ULInt | Ty::LReal | Ty::Dt => 8,            Ty::Dtl => 12,            Ty::Str(n) => n + 2,            Ty::WStr(n) => 2 * n + 4,        }    }    fn name(&self) -> String {        match self {            Ty::Bool => "BOOL".into(),            Ty::Byte => "BYTE".into(),            Ty::Word => "WORD".into(),            Ty::DWord => "DWORD".into(),            Ty::LWord => "LWORD".into(),            Ty::Char => "CHAR".into(),            Ty::SInt => "SINT".into(),            Ty::USInt => "USINT".into(),            Ty::Int => "INT".into(),            Ty::UInt => "UINT".into(),            Ty::DInt => "DINT".into(),            Ty::UDInt => "UDINT".into(),            Ty::LInt => "LINT".into(),            Ty::ULInt => "ULINT".into(),            Ty::Real => "REAL".into(),            Ty::LReal => "LREAL".into(),            Ty::Str(n) => format!("STRING[{}]", n),            Ty::WStr(n) => format!("WSTRING[{}]", n),            Ty::Time => "TIME".into(),            Ty::S5Time => "S5TIME".into(),            Ty::Date => "DATE".into(),            Ty::Tod => "TIME_OF_DAY".into(),            Ty::Dt => "DATE_AND_TIME".into(),            Ty::Dtl => "DTL".into(),            Ty::Timer => "TIMER".into(),            Ty::Counter => "COUNTER".into(),        }    }}fn parse_type(t: &str) -> Result<(Ty, Option<usize>), String> {    let t = t.trim().to_uppercase();    // `STRING[20]` is a length; `INT[10]` is ten of them.    let (base, bracket) = match t.find('[') {        Some(i) => {            if !t.ends_with(']') {                return Err(format!("`{}`: a `[` needs its `]`", t));            }            let n: usize = t[i + 1..t.len() - 1].trim().parse().map_err(|_| format!("`{}`: the number in [] must be a whole number", t))?;            (t[..i].trim().to_string(), Some(n))        }        None => (t.clone(), None),    };    let ty = match base.as_str() {        "BOOL" | "X" => Ty::Bool,        "BYTE" | "B" => Ty::Byte,        "WORD" | "W" => Ty::Word,        "DWORD" | "D" => Ty::DWord,        "LWORD" => Ty::LWord,        "CHAR" => Ty::Char,        "SINT" => Ty::SInt,        "USINT" => Ty::USInt,        "INT" => Ty::Int,        "UINT" => Ty::UInt,        "DINT" => Ty::DInt,        "UDINT" => Ty::UDInt,        "LINT" => Ty::LInt,        "ULINT" => Ty::ULInt,        "REAL" => Ty::Real,        "LREAL" => Ty::LReal,        "TIME" => Ty::Time,        "S5TIME" => Ty::S5Time,        "DATE" => Ty::Date,        "TOD" | "TIME_OF_DAY" => Ty::Tod,        "DT" | "DATE_AND_TIME" => Ty::Dt,        "DTL" => Ty::Dtl,        "STRING" => {            let n = bracket.unwrap_or(254);            if n == 0 || n > 254 {                return Err(format!("`{}`: a STRING holds 1 to 254 characters", t));            }            return Ok((Ty::Str(n), None));        }        "WSTRING" => {            let n = bracket.unwrap_or(254);            if n == 0 || n > 16382 {                return Err(format!("`{}`: a WSTRING holds 1 to 16382 characters", t));            }            return Ok((Ty::WStr(n), None));        }        _ => return Err(format!("unknown S7 type `{}`: use BOOL, BYTE, WORD, DWORD, LWORD, CHAR, SINT, USINT, INT, UINT, DINT, UDINT, LINT, ULINT, REAL, LREAL, STRING[n], WSTRING[n], TIME, S5TIME, DATE, TOD, DT or DTL", base)),    };    if let Some(n) = bracket {        if n == 0 || n > 65535 {            return Err(format!("`{}`: an array has 1 to 65535 elements", t));        }    }    Ok((ty, bracket))}#[derive(Clone, Debug)]struct Addr {    text: String,    area: u8,    db: u16,    byte: u32,    bit: u8,    ty: Ty,    count: Option<usize>,}impl Addr {    /// Bytes the address covers (for BOOL, the bytes holding its bits).    fn byte_len(&self) -> usize {        let n = self.count.unwrap_or(1);        match self.ty {            Ty::Bool => (self.bit as usize + n + 7) / 8,            Ty::Timer | Ty::Counter => 2 * n,            _ => self.ty.size() * n,        }    }}fn area_name(a: u8) -> &'static str {    match a {        AREA_DB => "DB",        AREA_I => "I",        AREA_Q => "Q",        AREA_M => "M",        AREA_P => "P",        AREA_T => "T",        AREA_C => "C",        _ => "?",    }}fn num<T: std::str::FromStr>(s: &str, what: &str, full: &str) -> Result<T, String> {    s.trim().parse::<T>().map_err(|_| format!("`{}`: `{}` is not a valid {}", full, s, what))}/// Parses a STEP 7 address (`DB1.DBX0.0`, `DB1.DBW2:INT`, `MD10:REAL`, `I0.3`, `QB4`, `T5`, `C2`;/// German `E`/`A` for I/Q and `Z` for counters; a leading `%` as TIA writes it).fn parse_addr(text: &str) -> Result<Addr, String> {    let full = text.trim();    if full.is_empty() {        return Err("an empty S7 address: write one like DB1.DBW2:INT, M0.1 or IW64".into());    }    let (place, ty_text) = match full.split_once(':') {        Some((p, t)) => (p.trim(), Some(t.trim())),        None => (full, None),    };    let p = place.trim_start_matches('%').to_uppercase().replace(' ', "");    let explicit = match ty_text {        Some(t) => Some(parse_type(t)?),        None => None,    };    // width: 'X' a bit, 'B' a byte (any type may start there), 'W' two bytes, 'D' four.    let (area, db, rest): (u8, u16, String) = if let Some(r) = p.strip_prefix("DB") {        let (dbn, rest) = r.split_once('.').ok_or_else(|| format!("`{}`: a data block address is like DB1.DBX0.0, DB1.DBB2, DB1.DBW4 or DB1.DBD8", full))?;        let n: u16 = num(dbn, "data block number", full)?;        if n == 0 {            return Err(format!("`{}`: data blocks are numbered from 1", full));        }        let rest = rest.strip_prefix("DB").ok_or_else(|| format!("`{}`: after DB{}. comes DBX, DBB, DBW or DBD", full, n))?;        (AREA_DB, n, rest.to_string())    } else if let Some(r) = p.strip_prefix("PI").or_else(|| p.strip_prefix("PE")).or_else(|| p.strip_prefix("PQ")).or_else(|| p.strip_prefix("PA")) {        (AREA_P, 0, r.to_string())    } else if let Some(r) = p.strip_prefix('I').or_else(|| p.strip_prefix('E')) {        (AREA_I, 0, r.to_string())    } else if let Some(r) = p.strip_prefix('Q').or_else(|| p.strip_prefix('A')) {        (AREA_Q, 0, r.to_string())    } else if let Some(r) = p.strip_prefix('M') {        (AREA_M, 0, r.to_string())    } else if let Some(r) = p.strip_prefix('T') {        let n: u32 = num(r, "timer number", full)?;        let (ty, count) = explicit.unwrap_or((Ty::Timer, None));        if ty != Ty::Timer && ty != Ty::S5Time {            return Err(format!("`{}`: a timer is read as TIMER (its time in milliseconds)", full));        }        return Ok(Addr { text: full.to_string(), area: AREA_T, db: 0, byte: n, bit: 0, ty: Ty::Timer, count });    } else if let Some(r) = p.strip_prefix('C').or_else(|| p.strip_prefix('Z')) {        let n: u32 = num(r, "counter number", full)?;        let (ty, count) = explicit.unwrap_or((Ty::Counter, None));        if ty != Ty::Counter {            return Err(format!("`{}`: a counter is read as COUNTER (0 to 999)", full));        }        return Ok(Addr { text: full.to_string(), area: AREA_C, db: 0, byte: n, bit: 0, ty: Ty::Counter, count });    } else {        return Err(format!("`{}` is not an S7 address: write DB1.DBW2:INT, I0.0 (E0.0), Q0.1 (A0.1), MB10, MW12:INT, MD14:REAL, T1 or C1", full));    };    let (width, digits) = match rest.chars().next() {        Some(c @ ('X' | 'B' | 'W' | 'D')) => (c, rest[1..].to_string()),        Some(c) if c.is_ascii_digit() => ('X', rest.clone()),        _ => return Err(format!("`{}`: after the area comes X (a bit, as in M0.1), B, W or D and the byte number", full)),    };    if width == 'X' {        let (b, bit) = digits.split_once('.').ok_or_else(|| format!("`{}`: a bit address is byte.bit, as in {}0.1", full, if area == AREA_DB { "DB1.DBX" } else { area_name(area) }))?;        let byte: u32 = num(b, "byte number", full)?;        let bit: u8 = num(bit, "bit number", full)?;        if bit > 7 {            return Err(format!("`{}`: bits are numbered 0 to 7", full));        }        let (ty, count) = explicit.unwrap_or((Ty::Bool, None));        if ty != Ty::Bool {            return Err(format!("`{}`: a bit address holds a BOOL; for {} start at a byte, as in {}", full, ty.name(), if area == AREA_DB { format!("DB{}.DBB{}:{}", db, byte, ty.name()) } else { format!("{}B{}:{}", area_name(area), byte, ty.name()) }));        }        if byte > 0xFFFF {            return Err(format!("`{}`: the byte number is too large", full));        }        return Ok(Addr { text: full.to_string(), area, db, byte, bit, ty, count });    }    if digits.contains('.') {        return Err(format!("`{}`: only bit addresses (X) have a .bit", full));    }    let byte: u32 = num(&digits, "byte number", full)?;    if byte > 0xFFFF {        return Err(format!("`{}`: the byte number is too large", full));    }    let default = match width {        'B' => Ty::Byte,        'W' => Ty::Word,        _ => Ty::DWord,    };    let (ty, count) = explicit.unwrap_or((default, None));    let need = match width {        'W' => Some(2),        'D' => Some(4),        _ => None,    };    if ty == Ty::Bool {        return Err(format!("`{}`: a BOOL has a bit address, as in {}", full, if area == AREA_DB { format!("DB{}.DBX{}.0", db, byte) } else { format!("{}{}.0", area_name(area), byte) }));    }    if ty == Ty::Timer || ty == Ty::Counter {        return Err(format!("`{}`: TIMER and COUNTER are for T and C addresses (T1, C1)", full));    }    if let Some(n) = need {        if ty.size() != n {            let w = match ty.size() { 2 => 'W', 4 => 'D', _ => 'B' };            let b = if area == AREA_DB { format!("DB{}.DB{}{}", db, w, byte) } else { format!("{}{}{}", area_name(area), w, byte) };            return Err(format!("`{}`: {} is {} bytes, not {}: write {}:{}", full, ty.name(), ty.size(), n, b, ty.name()));        }    }    Ok(Addr { text: full.to_string(), area, db, byte, bit: 0, ty, count })}// ---------------------------------------------------------------------------------------------// Values: bytes to JSON and backfn bcd(b: u8) -> Result<u32, String> {    let (h, l) = (b >> 4, b & 0x0F);    if h > 9 || l > 9 {        return Err(format!("0x{:02X} is not a BCD byte", b));    }    Ok((h * 10 + l) as u32)}fn to_bcd(n: u32) -> u8 {    (((n / 10) % 10) << 4 | (n % 10)) as u8}fn days_to_date(days: i64) -> (i64, u32, u32) {    // days since 1970-01-01 to a civil date (Howard Hinnant's algorithm)    let z = days + 719468;    let era = z.div_euclid(146097);    let doe = z.rem_euclid(146097);    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;    let y = yoe + era * 400;    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);    let mp = (5 * doy + 2) / 153;    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;    (if m <= 2 { y + 1 } else { y }, m, d)}fn date_to_days(y: i64, m: u32, d: u32) -> i64 {    let y = if m <= 2 { y - 1 } else { y };    let era = y.div_euclid(400);    let yoe = y.rem_euclid(400);    let mp = if m > 2 { m - 3 } else { m + 9 } as i64;    let doy = (153 * mp + 2) / 5 + d as i64 - 1;    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;    era * 146097 + doe - 719468}const DAYS_1990: i64 = 7305; // 1990-01-01 in days since 1970fn parse_date(s: &str) -> Result<(i64, u32, u32), String> {    let p: Vec<&str> = s.trim().split('-').collect();    if p.len() != 3 {        return Err(format!("`{}` is not a date like 2026-10-30", s));    }    let y: i64 = p[0].parse().map_err(|_| format!("`{}` is not a date like 2026-10-30", s))?;    let m: u32 = p[1].parse().map_err(|_| format!("`{}` is not a date like 2026-10-30", s))?;    let d: u32 = p[2].parse().map_err(|_| format!("`{}` is not a date like 2026-10-30", s))?;    if !(1..=12).contains(&m) || !(1..=31).contains(&d) {        return Err(format!("`{}` is not a date", s));    }    Ok((y, m, d))}/// `HH:MM[:SS[.fff]]` to nanoseconds since midnight.fn parse_tod(s: &str) -> Result<u64, String> {    let bad = || format!("`{}` is not a time of day like 08:30:00.250", s);    let p: Vec<&str> = s.trim().split(':').collect();    if p.len() < 2 || p.len() > 3 {        return Err(bad());    }    let h: u64 = p[0].parse().map_err(|_| bad())?;    let m: u64 = p[1].parse().map_err(|_| bad())?;    let (sec, frac) = match p.get(2) {        Some(x) => match x.split_once('.') {            Some((a, b)) => {                let mut f = b.to_string();                if f.len() > 9 || !f.chars().all(|c| c.is_ascii_digit()) {                    return Err(bad());                }                while f.len() < 9 {                    f.push('0');                }                (a.parse::<u64>().map_err(|_| bad())?, f.parse::<u64>().map_err(|_| bad())?)            }            None => (x.parse::<u64>().map_err(|_| bad())?, 0),        },        None => (0, 0),    };    if h > 23 || m > 59 || sec > 59 {        return Err(bad());    }    Ok(((h * 60 + m) * 60 + sec) * 1_000_000_000 + frac)}fn split_datetime(s: &str) -> Result<((i64, u32, u32), u64), String> {    let t = s.trim().trim_end_matches('Z');    let (d, tm) = t.split_once(|c| c == 'T' || c == ' ').ok_or_else(|| format!("`{}` is not a date and time like 2026-10-30T08:15:00", s))?;    Ok((parse_date(d)?, parse_tod(tm)?))}fn fmt_tod(ns: u64) -> String {    let ms_total = ns / 1_000_000;    let (h, m, sec, ms) = (ms_total / 3_600_000, (ms_total / 60_000) % 60, (ms_total / 1000) % 60, ms_total % 1000);    let sub = ns % 1_000_000;    if sub != 0 {        format!("{:02}:{:02}:{:02}.{:09}", h, m, sec, ns % 1_000_000_000)    } else {        format!("{:02}:{:02}:{:02}.{:03}", h, m, sec, ms)    }}fn latin1(b: &[u8]) -> String {    b.iter().map(|&c| c as char).collect()}fn s5time_ms(w: u16) -> Result<i64, String> {    let base = [10i64, 100, 1000, 10000][((w >> 12) & 0x3) as usize];    let (h, t, u) = (((w >> 8) & 0xF) as i64, ((w >> 4) & 0xF) as i64, (w & 0xF) as i64);    if h > 9 || t > 9 || u > 9 {        return Err(format!("0x{:04X} is not an S5TIME", w));    }    Ok((h * 100 + t * 10 + u) * base)}/// Milliseconds as an S5TIME: the finest time base that holds them (what is finer is dropped, as the PLC does).fn ms_s5time(ms: i64) -> Result<u16, String> {    for (i, base) in [10i64, 100, 1000, 10000].iter().enumerate() {        let v = ms / base;        if (0..=999).contains(&v) && ms >= 0 {            let v = v as u16;            return Ok(((i as u16) << 12) | (v / 100) << 8 | ((v / 10) % 10) << 4 | (v % 10));        }    }    Err(format!("{} ms does not fit an S5TIME (0 to 9990 s)", ms))}fn decode_one(ty: &Ty, b: &[u8]) -> Result<Value, String> {    let be16 = |i: usize| u16::from_be_bytes([b[i], b[i + 1]]);    let be32 = |i: usize| u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);    let be64 = |i: usize| u64::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3], b[i + 4], b[i + 5], b[i + 6], b[i + 7]]);    if b.len() < ty.size() && !matches!(ty, Ty::Bool) {        return Err(format!("{} needs {} bytes, got {}", ty.name(), ty.size(), b.len()));    }    Ok(match ty {        Ty::Bool => json!(b.first().map(|x| *x != 0).unwrap_or(false)),        Ty::Byte | Ty::USInt => json!(b[0]),        Ty::Char => json!(latin1(&b[..1])),        Ty::SInt => json!(b[0] as i8),        Ty::Word | Ty::UInt => json!(be16(0)),        Ty::Int => json!(be16(0) as i16),        Ty::DWord | Ty::UDInt => json!(be32(0)),        Ty::DInt => json!(be32(0) as i32),        Ty::LWord | Ty::ULInt => {            let v = be64(0);            if v > i64::MAX as u64 {                json!(v.to_string())            } else {                json!(v as i64)            }        }        Ty::LInt => json!(be64(0) as i64),        Ty::Real => {            let f = f32::from_bits(be32(0));            // f32 to the shortest decimal that reads back as it: 72.5, not 72.5000000001            let s = format!("{}", f);            json!(s.parse::<f64>().unwrap_or(f as f64))        }        Ty::LReal => json!(f64::from_bits(be64(0))),        Ty::Str(n) => {            let len = (b[1] as usize).min(*n).min(b.len() - 2);            json!(latin1(&b[2..2 + len]))        }        Ty::WStr(n) => {            let len = (be16(2) as usize).min(*n).min((b.len() - 4) / 2);            let units: Vec<u16> = (0..len).map(|i| be16(4 + 2 * i)).collect();            json!(String::from_utf16_lossy(&units))        }        Ty::Time => json!(be32(0) as i32),        Ty::S5Time | Ty::Timer => json!(s5time_ms(be16(0))?),        Ty::Counter => {            let w = be16(0);            let h = ((w >> 8) & 0x0F) as u32;            if h > 9 {                return Err(format!("0x{:04X} is not a counter value", w));            }            json!(h * 100 + bcd((w & 0xFF) as u8)?)        }        Ty::Date => {            let (y, m, d) = days_to_date(DAYS_1990 + be16(0) as i64);            json!(format!("{:04}-{:02}-{:02}", y, m, d))        }        Ty::Tod => json!(fmt_tod(be32(0) as u64 * 1_000_000)),        Ty::Dt => {            let yy = bcd(b[0])?;            let y = if yy >= 90 { 1900 + yy } else { 2000 + yy };            let ms = bcd(b[6])? * 10 + (b[7] >> 4) as u32;            json!(format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}", y, bcd(b[1])?, bcd(b[2])?, bcd(b[3])?, bcd(b[4])?, bcd(b[5])?, ms))        }        Ty::Dtl => {            let ns = be32(8) as u64;            json!(format!("{:04}-{:02}-{:02}T{}", be16(0), b[2], b[3], fmt_tod(((b[5] as u64 * 60 + b[6] as u64) * 60 + b[7] as u64) * 1_000_000_000 + ns)))        }    })}fn int_of(v: &Value, lo: i128, hi: i128, ty: &Ty) -> Result<i128, String> {    let n: i128 = if let Some(i) = v.as_i64() {        i as i128    } else if let Some(u) = v.as_u64() {        u as i128    } else if let Some(f) = v.as_f64() {        if f.fract() != 0.0 {            return Err(format!("{} takes a whole number, not {}", ty.name(), f));        }        f as i128    } else if let Some(b) = v.as_bool() {        b as i128    } else if let Some(s) = v.as_str() {        s.trim().parse::<i128>().map_err(|_| format!("{} takes a number, not {:?}", ty.name(), s))?    } else {        return Err(format!("{} takes a number, not {}", ty.name(), v));    };    if n < lo || n > hi {        return Err(format!("{} does not fit {} ({} to {})", n, ty.name(), lo, hi));    }    Ok(n)}fn encode_one(ty: &Ty, v: &Value) -> Result<Vec<u8>, String> {    let txt = |v: &Value| -> Result<String, String> { v.as_str().map(|s| s.to_string()).ok_or_else(|| format!("{} takes text, not {}", ty.name(), v)) };    Ok(match ty {        Ty::Bool => {            let b = match v {                Value::Bool(b) => *b,                Value::Number(n) if n.as_i64() == Some(0) || n.as_i64() == Some(1) => n.as_i64() == Some(1),                _ => return Err(format!("BOOL takes true or false, not {}", v)),            };            vec![b as u8]        }        Ty::Byte | Ty::USInt => vec![int_of(v, 0, 255, ty)? as u8],        Ty::SInt => vec![int_of(v, -128, 127, ty)? as i8 as u8],        Ty::Char => {            let s = txt(v)?;            let c: Vec<char> = s.chars().collect();            if c.len() != 1 || c[0] as u32 > 255 {                return Err(format!("CHAR takes one Latin-1 character, not {:?}", s));            }            vec![c[0] as u32 as u8]        }        Ty::Word | Ty::UInt => (int_of(v, 0, 65535, ty)? as u16).to_be_bytes().to_vec(),        Ty::Int => (int_of(v, -32768, 32767, ty)? as i16).to_be_bytes().to_vec(),        Ty::DWord | Ty::UDInt => (int_of(v, 0, u32::MAX as i128, ty)? as u32).to_be_bytes().to_vec(),        Ty::DInt | Ty::Time => (int_of(v, i32::MIN as i128, i32::MAX as i128, ty)? as i32).to_be_bytes().to_vec(),        Ty::LWord | Ty::ULInt => (int_of(v, 0, u64::MAX as i128, ty)? as u64).to_be_bytes().to_vec(),        Ty::LInt => (int_of(v, i64::MIN as i128, i64::MAX as i128, ty)? as i64).to_be_bytes().to_vec(),        Ty::Real => {            let f = v.as_f64().ok_or_else(|| format!("REAL takes a number, not {}", v))?;            if f.is_finite() && f.abs() > f32::MAX as f64 {                return Err(format!("{} does not fit a REAL", f));            }            (f as f32).to_bits().to_be_bytes().to_vec()        }        Ty::LReal => v.as_f64().ok_or_else(|| format!("LREAL takes a number, not {}", v))?.to_bits().to_be_bytes().to_vec(),        Ty::Str(n) => {            let s = txt(v)?;            let mut out = vec![*n as u8, 0];            for c in s.chars() {                if c as u32 > 255 {                    return Err(format!("a STRING holds Latin-1 characters only ({:?} is not one): use WSTRING", c));                }                out.push(c as u32 as u8);            }            if out.len() - 2 > *n {                return Err(format!("{:?} is {} characters, more than the STRING[{}] holds", s, out.len() - 2, n));            }            out[1] = (out.len() - 2) as u8;            out        }        Ty::WStr(n) => {            let s = txt(v)?;            let units: Vec<u16> = s.encode_utf16().collect();            if units.len() > *n {                return Err(format!("{:?} is {} characters, more than the WSTRING[{}] holds", s, units.len(), n));            }            let mut out = vec![];            out.extend_from_slice(&(*n as u16).to_be_bytes());            out.extend_from_slice(&(units.len() as u16).to_be_bytes());            for u in units {                out.extend_from_slice(&u.to_be_bytes());            }            out        }        Ty::S5Time | Ty::Timer => {            let ms = int_of(v, 0, 9_990_000, ty)? as i64;            ms_s5time(ms)?.to_be_bytes().to_vec()        }        Ty::Counter => {            let c = int_of(v, 0, 999, ty)? as u32;            vec![(c / 100) as u8, to_bcd(c % 100)]        }        Ty::Date => {            let (y, m, d) = parse_date(&txt(v)?)?;            let days = date_to_days(y, m, d) - DAYS_1990;            if !(0..=65378).contains(&days) {                return Err("a DATE is from 1990-01-01 to 2168-12-31".into());            }            (days as u16).to_be_bytes().to_vec()        }        Ty::Tod => ((parse_tod(&txt(v)?)? / 1_000_000) as u32).to_be_bytes().to_vec(),        Ty::Dt => {            let ((y, m, d), ns) = split_datetime(&txt(v)?)?;            if !(1990..=2089).contains(&y) {                return Err("a DATE_AND_TIME is from 1990 to 2089".into());            }            let ms = (ns / 1_000_000) as u32;            let weekday = ((date_to_days(y, m, d) + 4).rem_euclid(7) + 1) as u8; // 1 = Sunday            let secs = ms / 1000;            vec![to_bcd((y % 100) as u32), to_bcd(m), to_bcd(d), to_bcd(secs / 3600), to_bcd((secs / 60) % 60), to_bcd(secs % 60), to_bcd((ms % 1000) / 10), (((ms % 10) as u8) << 4) | weekday]        }        Ty::Dtl => {            let ((y, m, d), ns) = split_datetime(&txt(v)?)?;            if !(1970..=2262).contains(&y) {                return Err("a DTL is from 1970 to 2262".into());            }            let weekday = ((date_to_days(y, m, d) + 4).rem_euclid(7) + 1) as u8;            let secs = ns / 1_000_000_000;            let mut out = (y as u16).to_be_bytes().to_vec();            out.extend_from_slice(&[m as u8, d as u8, weekday, (secs / 3600) as u8, ((secs / 60) % 60) as u8, (secs % 60) as u8]);            out.extend_from_slice(&((ns % 1_000_000_000) as u32).to_be_bytes());            out        }    })}/// The value at an address, from the bytes it covers.fn decode_addr(a: &Addr, b: &[u8]) -> Result<Value, String> {    match (&a.ty, a.count) {        (Ty::Bool, None) => Ok(json!(b.first().map(|x| x >> a.bit & 1 == 1).unwrap_or(false))),        (Ty::Bool, Some(n)) => Ok(Value::Array((0..n).map(|i| {            let at = a.bit as usize + i;            json!(b.get(at / 8).map(|x| x >> (at % 8) & 1 == 1).unwrap_or(false))        }).collect())),        (ty, None) => decode_one(ty, b),        (ty, Some(n)) => {            let s = if matches!(ty, Ty::Timer | Ty::Counter) { 2 } else { ty.size() };            let mut out = vec![];            for i in 0..n {                out.push(decode_one(ty, &b[i * s..(i + 1) * s])?);            }            Ok(Value::Array(out))        }    }}fn encode_addr(a: &Addr, v: &Value) -> Result<Vec<u8>, String> {    match a.count {        None => encode_one(&a.ty, v),        Some(n) => {            let items = v.as_array().ok_or_else(|| format!("`{}` is an array of {}: give a list of {} values", a.text, n, n))?;            if items.len() != n {                return Err(format!("`{}` holds {} values; {} given", a.text, n, items.len()));            }            let mut out = vec![];            for x in items {                out.extend(encode_one(&a.ty, x).map_err(|e| format!("`{}`: {}", a.text, e))?);            }            Ok(out)        }    }}// ---------------------------------------------------------------------------------------------// The connectionstruct Conn {    host: String,    rack: i64,    slot: i64,    local_tsap: u16,    remote_tsap: u16,    want_pdu: u16,    timeout: Duration,    stream: Option<TcpStream>,    pdu: usize,    pdu_ref: u16,    max_items: usize,}fn registry() -> &'static Mutex<HashMap<String, Arc<Mutex<Conn>>>> {    static R: OnceLock<Mutex<HashMap<String, Arc<Mutex<Conn>>>>> = OnceLock::new();    R.get_or_init(|| Mutex::new(HashMap::new()))}fn get_conn(name: &str) -> Result<Arc<Mutex<Conn>>, String> {    registry().lock().map_err(|_| "the S7 registry is poisoned".to_string())?.get(name).cloned().ok_or_else(|| format!("no S7 connection named {:?}: call s7_connect({:?}, host, rack, slot, {{}}) first", name, name))}fn sock_addr(host: &str, port: u16) -> Result<SocketAddr, String> {    let h = host.trim();    let with_port = if h.starts_with('[') {        if h.contains("]:") { h.to_string() } else { format!("{}:{}", h, port) }    } else if h.matches(':').count() == 1 {        h.to_string()    } else if h.contains(':') {        format!("[{}]:{}", h, port)    } else {        format!("{}:{}", h, port)    };    with_port.to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.next().ok_or(format!("{} has no address", host))}fn tsap(v: &Value, what: &str) -> Result<Option<u16>, String> {    match v {        Value::Null => Ok(None),        Value::Number(n) => n.as_u64().filter(|x| *x <= 0xFFFF).map(|x| Some(x as u16)).ok_or_else(|| format!("{} is 0 to 65535", what)),        Value::String(s) => {            let h: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();            if h.len() != 4 {                return Err(format!("{} is four hex digits, like \"02.00\" or \"0300\"", what));            }            u16::from_str_radix(&h, 16).map(Some).map_err(|e| e.to_string())        }        _ => Err(format!("{} is a number or hex text like \"02.00\"", what)),    }}impl Conn {    fn stream(&mut self) -> Result<&mut TcpStream, String> {        self.stream.as_mut().ok_or_else(|| "not connected".to_string())    }    fn send_tpkt(&mut self, payload: &[u8]) -> Result<(), String> {        let mut p = vec![3, 0];        p.extend_from_slice(&((payload.len() + 4) as u16).to_be_bytes());        p.extend_from_slice(payload);        self.stream()?.write_all(&p).map_err(|e| format!("sending to the PLC at {}: {}", self.host, e))    }    fn read_tpkt(&mut self) -> Result<Vec<u8>, String> {        let host = self.host.clone();        let s = self.stream()?;        let mut h = [0u8; 4];        s.read_exact(&mut h).map_err(|e| match e.kind() {            std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => format!("the PLC at {} did not answer in time", host),            std::io::ErrorKind::UnexpectedEof => format!("the PLC at {} closed the connection", host),            _ => format!("reading from the PLC at {}: {}", host, e),        })?;        if h[0] != 3 {            return Err(format!("the PLC at {} sent something that is not ISO-on-TCP (TPKT version {})", host, h[0]));        }        let len = u16::from_be_bytes([h[2], h[3]]) as usize;        if len < 7 {            return Err(format!("the PLC at {} sent a TPKT of {} bytes", host, len));        }        let mut body = vec![0u8; len - 4];        s.read_exact(&mut body).map_err(|e| format!("reading from the PLC at {}: {}", host, e))?;        Ok(body)    }    /// One S7 PDU from COTP data units (a PDU may come in several, the last marked EOT).    fn read_pdu(&mut self) -> Result<Vec<u8>, String> {        let mut out = vec![];        loop {            let t = self.read_tpkt()?;            let li = t[0] as usize;            if t.len() < li + 1 || t.len() < 3 {                return Err("a COTP header longer than its packet".into());            }            match t[1] {                0xF0 => {                    out.extend_from_slice(&t[li + 1..]);                    if t[2] & 0x80 != 0 {                        return Ok(out);                    }                }                0x80 => {                    self.stream = None;                    return Err(format!("the PLC at {} closed the connection (COTP disconnect request)", self.host));                }                k => return Err(format!("the PLC sent COTP type 0x{:02X} where data was expected", k)),            }        }    }    fn connect(&mut self) -> Result<(), String> {        self.stream = None;        let a = sock_addr(&self.host, 102)?;        let s = TcpStream::connect_timeout(&a, self.timeout).map_err(|e| format!("cannot reach the PLC at {}: {} (ISO-on-TCP is port 102; is the CPU on the network and the address right?)", a, e))?;        s.set_read_timeout(Some(self.timeout)).map_err(|e| e.to_string())?;        s.set_write_timeout(Some(self.timeout)).map_err(|e| e.to_string())?;        let _ = s.set_nodelay(true);        self.stream = Some(s);        // COTP connection request: TPDU size 1024, the calling and called TSAPs.        let l = self.local_tsap.to_be_bytes();        let r = self.remote_tsap.to_be_bytes();        let cr = [17u8, 0xE0, 0, 0, 0, 1, 0, 0xC0, 1, 0x0A, 0xC1, 2, l[0], l[1], 0xC2, 2, r[0], r[1]];        self.send_tpkt(&cr)?;        let cc = self.read_tpkt().map_err(|e| format!("{} (no answer to the connection request: check rack {} and slot {})", e, self.rack, self.slot))?;        if cc.len() < 2 || cc[1] != 0xD0 {            self.stream = None;            return Err(format!(                "the PLC at {} refused the connection for rack {}, slot {} (TSAP {:02X}.{:02X}): S7-300 is usually rack 0 slot 2, S7-1200/1500 rack 0 slot 1 (or 0); S7-400 depends on the rack; a LOGO! needs local_tsap and remote_tsap",                self.host, self.rack, self.slot, r[0], r[1]            ));        }        // S7 setup communication: one job at a time each way, the PDU size we would like.        let p = self.want_pdu.to_be_bytes();        let ans = self.job(&[0xF0, 0, 0, 1, 0, 1, p[0], p[1]], &[])?;        let (params, _) = ans;        if params.len() < 8 || params[0] != 0xF0 {            return Err("the PLC's answer to setup communication is not one".into());        }        self.pdu = (u16::from_be_bytes([params[6], params[7]]) as usize).max(64);        Ok(())    }    fn header(&mut self, rosctr: u8, plen: usize, dlen: usize) -> Vec<u8> {        self.pdu_ref = self.pdu_ref.wrapping_add(1).max(1);        let r = self.pdu_ref.to_be_bytes();        let mut h = vec![0x02, 0xF0, 0x80, 0x32, rosctr, 0, 0, r[0], r[1]];        h.extend_from_slice(&(plen as u16).to_be_bytes());        h.extend_from_slice(&(dlen as u16).to_be_bytes());        h    }    /// A job (ROSCTR 1) sent, its ack-data read: (parameters, data).    fn job(&mut self, params: &[u8], data: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {        let mut p = self.header(1, params.len(), data.len());        let want = self.pdu_ref;        p.extend_from_slice(params);        p.extend_from_slice(data);        self.send_tpkt(&p)?;        loop {            let pdu = self.read_pdu()?;            if pdu.len() < 10 || pdu[0] != 0x32 {                return Err("the PLC's answer is not an S7 PDU".into());            }            let rosctr = pdu[1];            let r = u16::from_be_bytes([pdu[4], pdu[5]]);            if r != want {                continue; // a late answer to an earlier request that timed out            }            if rosctr != 2 && rosctr != 3 {                return Err(format!("the PLC answered with S7 message type {} to a job", rosctr));            }            if pdu.len() < 12 {                return Err("a short S7 ack".into());            }            let plen = u16::from_be_bytes([pdu[6], pdu[7]]) as usize;            let dlen = u16::from_be_bytes([pdu[8], pdu[9]]) as usize;            let (ec, code) = (pdu[10], pdu[11]);            if ec != 0 || code != 0 {                return Err(header_error(ec, code));            }            if pdu.len() < 12 + plen + dlen {                return Err("the PLC's answer is shorter than it says".into());            }            return Ok((pdu[12..12 + plen].to_vec(), pdu[12 + plen..12 + plen + dlen].to_vec()));        }    }    /// A user-data request (ROSCTR 7): (parameters, data) of the answer.    fn userdata(&mut self, params: &[u8], data: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {        let mut p = self.header(7, params.len(), data.len());        let want = self.pdu_ref;        p.extend_from_slice(params);        p.extend_from_slice(data);        self.send_tpkt(&p)?;        loop {            let pdu = self.read_pdu()?;            if pdu.len() < 10 || pdu[0] != 0x32 {                return Err("the PLC's answer is not an S7 PDU".into());            }            if u16::from_be_bytes([pdu[4], pdu[5]]) != want {                continue;            }            let (hlen, plen, dlen) = if pdu[1] == 7 {                (10, u16::from_be_bytes([pdu[6], pdu[7]]) as usize, u16::from_be_bytes([pdu[8], pdu[9]]) as usize)            } else if pdu[1] == 2 || pdu[1] == 3 {                if pdu.len() >= 12 && (pdu[10] != 0 || pdu[11] != 0) {                    return Err(header_error(pdu[10], pdu[11]));                }                (12, u16::from_be_bytes([pdu[6], pdu[7]]) as usize, u16::from_be_bytes([pdu[8], pdu[9]]) as usize)            } else {                return Err(format!("the PLC answered with S7 message type {}", pdu[1]));            };            if pdu.len() < hlen + plen + dlen {                return Err("the PLC's answer is shorter than it says".into());            }            return Ok((pdu[hlen..hlen + plen].to_vec(), pdu[hlen + plen..hlen + plen + dlen].to_vec()));        }    }    fn ensure(&mut self) -> Result<(), String> {        if self.stream.is_none() {            self.connect()?;        }        Ok(())    }}fn header_error(class: u8, code: u8) -> String {    let what = match (class, code) {        (0x81, 0x04) => "the PLC does not support this request in this context (on S7-1200/1500: allow PUT/GET communication in the CPU's protection settings)",        (0x85, 0x00) => "the request is longer than the PDU the PLC accepts",        (0x87, 0x04) => "the PLC refused the request",        (0x84, _) => "the PLC says the request breaks the protocol",        (0x83, _) => "no resources free in the PLC for this request",        (0x82, _) => "the PLC's object definition does not allow this",        (0xD6, _) | (0xD2, _) | (0xD0, _) => "the PLC refused the request",        _ => "the PLC refused the request",    };    format!("{} (S7 error class 0x{:02X}, code 0x{:02X})", what, class, code)}fn item_error(rc: u8, a: &str) -> String {    let what = match rc {        0x01 => "a hardware fault in the PLC",        0x03 => "access to it is not allowed (on S7-1200/1500: allow PUT/GET, and turn off optimized block access for the data block)",        0x05 => "the address is out of range (past the end of the area or data block; or the block uses optimized access)",        0x06 => "the data type is not supported there",        0x07 => "the data type does not match",        0x0A => "it does not exist (no such data block, or that area is not in this CPU)",        _ => "the PLC refused it",    };    format!("`{}`: {} (S7 return code 0x{:02X})", a, what, rc)}/// A piece of a request: a run of bytes in one area, or one bit.#[derive(Clone, Debug)]struct Piece {    area: u8,    db: u16,    start: u32,    bit: Option<u8>,    len: usize,}impl Piece {    fn spec(&self) -> [u8; 12] {        let (ts, len, addr) = match (self.area, self.bit) {            (AREA_T, _) => (0x1D, self.len / 2, self.start),            (AREA_C, _) => (0x1C, self.len / 2, self.start),            (_, Some(b)) => (0x01, 1, self.start * 8 + b as u32),            (_, None) => (0x02, self.len, self.start * 8),        };        let l = (len as u16).to_be_bytes();        let d = self.db.to_be_bytes();        [0x12, 0x0A, 0x10, ts, l[0], l[1], d[0], d[1], self.area, (addr >> 16) as u8, (addr >> 8) as u8, addr as u8]    }    fn unit(&self) -> usize {        if self.area == AREA_T || self.area == AREA_C { 2 } else { 1 }    }}/// Splits a run of bytes into pieces no longer than `max` (whole timers and counters).fn split(area: u8, db: u16, start: u32, len: usize, max: usize) -> Vec<Piece> {    let unit = if area == AREA_T || area == AREA_C { 2 } else { 1 };    let max = (max / 2 * 2).max(2);    let mut out = vec![];    let mut done = 0;    while done < len {        let n = (len - done).min(max);        let off = if unit == 2 { (done / 2) as u32 } else { done as u32 };        out.push(Piece { area, db, start: start + off, bit: None, len: n });        done += n;    }    out}/// Reads pieces in as few requests as the PDU allows; the bytes of each, in order.fn read_pieces(c: &mut Conn, pieces: &[Piece]) -> Result<Vec<Result<Vec<u8>, u8>>, String> {    let mut out = vec![];    let mut i = 0;    while i < pieces.len() {        // fit as many as the request (12 bytes each) and the answer (4 + data + 1) allow        let mut req = 10 + 2;        let mut ans = 12 + 2;        let mut j = i;        while j < pieces.len() && j - i < c.max_items {            let r = 12;            let a = 4 + pieces[j].len + pieces[j].len % 2;            if req + r > c.pdu || ans + a > c.pdu {                break;            }            req += r;            ans += a;            j += 1;        }        if j == i {            return Err(format!("a piece of {} bytes does not fit the PDU of {}", pieces[i].len, c.pdu));        }        let group = &pieces[i..j];        let mut params = vec![0x04, group.len() as u8];        for p in group {            params.extend_from_slice(&p.spec());        }        let (rp, data) = c.job(&params, &[])?;        if rp.len() < 2 || rp[0] != 0x04 {            return Err("the PLC's answer to a read is not one".into());        }        if rp[1] as usize != group.len() {            return Err(format!("asked for {} items, the PLC answered {}", group.len(), rp[1]));        }        let mut at = 0;        for (k, p) in group.iter().enumerate() {            if data.len() < at + 4 {                return Err("the PLC's answer to a read is cut short".into());            }            let rc = data[at];            let ts = data[at + 1];            let l = u16::from_be_bytes([data[at + 2], data[at + 3]]) as usize;            let bytes = if matches!(ts, 0x03 | 0x04 | 0x05) { (l + 7) / 8 } else { l };            at += 4;            if rc != 0xFF {                out.push(Err(rc));                // an error item has no data (and no fill byte, being 4 bytes long)                continue;            }            if data.len() < at + bytes {                return Err("the PLC's answer to a read is cut short".into());            }            let mut b = data[at..at + bytes].to_vec();            at += bytes;            if bytes % 2 == 1 && k + 1 < group.len() {                at += 1;            }            if p.bit.is_some() {                // a bit comes back as one byte, 0 or 1: put it where the bit is                let v = b.first().copied().unwrap_or(0) & 1;                b = vec![v << p.bit.unwrap()];            } else if b.len() != p.len {                return Err(format!("asked for {} bytes, the PLC sent {}", p.len, b.len()));            }            out.push(Ok(b));        }        i = j;    }    Ok(out)}fn write_pieces(c: &mut Conn, pieces: &[(Piece, Vec<u8>)]) -> Result<Vec<u8>, String> {    let mut out = vec![];    let mut i = 0;    while i < pieces.len() {        let mut req = 10 + 2;        let mut j = i;        while j < pieces.len() && j - i < c.max_items {            let r = 12 + 4 + pieces[j].1.len() + pieces[j].1.len() % 2;            if req + r > c.pdu {                break;            }            req += r;            j += 1;        }        if j == i {            return Err(format!("a piece of {} bytes does not fit the PDU of {}", pieces[i].1.len(), c.pdu));        }        let group = &pieces[i..j];        let mut params = vec![0x05, group.len() as u8];        let mut data = vec![];        for (k, (p, b)) in group.iter().enumerate() {            params.extend_from_slice(&p.spec());            let (ts, len) = match (p.area, p.bit) {                (AREA_T, _) | (AREA_C, _) => (0x09, b.len()),                (_, Some(_)) => (0x03, 1),                _ => (0x04, b.len() * 8),            };            data.push(0);            data.push(ts);            data.extend_from_slice(&(len as u16).to_be_bytes());            data.extend_from_slice(b);            if b.len() % 2 == 1 && k + 1 < group.len() {                data.push(0);            }        }        let (rp, rd) = c.job(&params, &data)?;        if rp.len() < 2 || rp[0] != 0x05 {            return Err("the PLC's answer to a write is not one".into());        }        if rd.len() < group.len() {            return Err(format!("wrote {} items, the PLC answered for {}", group.len(), rd.len()));        }        out.extend_from_slice(&rd[..group.len()]);        i = j;    }    Ok(out)}/// Runs `f` on the named connection, connecting first, and once more on a fresh connection/// when the old one broke (a PLC restarted, a cable pulled and put back).fn with_conn<T>(name: &str, f: &dyn Fn(&mut Conn) -> Result<T, String>) -> Result<T, String> {    let c = get_conn(name)?;    let mut c = c.lock().map_err(|_| "the S7 connection is poisoned".to_string())?;    let fresh = c.stream.is_none();    c.ensure()?;    match f(&mut c) {        Ok(v) => Ok(v),        Err(e) if c.stream.is_some() && !broken(&e) => Err(e),        Err(e) => {            if fresh {                c.stream = None;                return Err(e);            }            c.stream = None;            c.connect().map_err(|e2| format!("{}; connecting again: {}", e, e2))?;            f(&mut c).map_err(|e| {                if broken(&e) {                    c.stream = None;                }                e            })        }    }}fn broken(e: &str) -> bool {    e.contains("reading from the PLC") || e.contains("sending to the PLC") || e.contains("did not answer in time") || e.contains("closed the connection") || e.contains("not an S7 PDU") || e.contains("not ISO-on-TCP")}fn pieces_for(a: &Addr, max: usize) -> Vec<Piece> {    if a.ty == Ty::Bool && a.count.is_none() {        return vec![Piece { area: a.area, db: a.db, start: a.byte, bit: Some(a.bit), len: 1 }];    }    split(a.area, a.db, a.byte, a.byte_len(), max)}fn read_addrs(c: &mut Conn, addrs: &[Addr]) -> Result<Vec<Result<Value, String>>, String> {    let max = c.pdu.saturating_sub(18).max(2);    let mut pieces = vec![];    let mut owner = vec![];    for (i, a) in addrs.iter().enumerate() {        for p in pieces_for(a, max) {            pieces.push(p);            owner.push(i);        }    }    let got = read_pieces(c, &pieces)?;    let mut bytes: Vec<Result<Vec<u8>, u8>> = addrs.iter().map(|_| Ok(vec![])).collect();    for (k, g) in got.into_iter().enumerate() {        let i = owner[k];        match (&mut bytes[i], g) {            (Ok(b), Ok(x)) => b.extend(x),            (slot @ Ok(_), Err(rc)) => *slot = Err(rc),            _ => {}        }    }    Ok(addrs.iter().zip(bytes).map(|(a, b)| match b {        Ok(b) => decode_addr(a, &b).map_err(|e| format!("`{}`: {}", a.text, e)),        Err(rc) => Err(item_error(rc, &a.text)),    }).collect())}// ---------------------------------------------------------------------------------------------// The functions/// Opens a connection named `name` to the PLC at `host` (port 102, or `host:port`) by rack and/// slot, or keeps the one open under that name with the same settings (call it before each use); options: `timeout_ms` (5000), `connection` ("pg", "op" or "basic"),/// `pdu_size` (960 asked), `local_tsap` and `remote_tsap` (for LOGO! and S7-200, e.g. "02.00").pub fn s7_connect(name: &str, host: &str, rack: i64, slot: i64, options: serde_json::Value) -> Result<serde_json::Value, String> {    if !(0..=7).contains(&rack) || !(0..=31).contains(&slot) {        return Err("rack is 0 to 7 and slot 0 to 31".into());    }    let o = if options.is_object() { options } else { json!({}) };    let timeout = o.get("timeout_ms").and_then(|v| v.as_i64()).unwrap_or(5000).clamp(100, 120_000) as u64;    let kind = match o.get("connection").and_then(|v| v.as_str()).unwrap_or("pg").to_lowercase().as_str() {        "pg" => 1u16,        "op" => 2,        "basic" | "s7_basic" => 3,        k => return Err(format!("connection {:?}: use \"pg\", \"op\" or \"basic\"", k)),    };    let local = tsap(o.get("local_tsap").unwrap_or(&Value::Null), "local_tsap")?.unwrap_or(0x0100);    let remote = tsap(o.get("remote_tsap").unwrap_or(&Value::Null), "remote_tsap")?.unwrap_or((kind << 8) | (rack as u16 * 32 + slot as u16));    let want = o.get("pdu_size").and_then(|v| v.as_i64()).unwrap_or(960).clamp(240, 960) as u16;    let max_items = o.get("max_items").and_then(|v| v.as_i64()).unwrap_or(20).clamp(1, 20) as usize;    let host = host.trim().to_string();    // The same settings under the same name: keep the connection (connecting again if it broke).    let existing = registry().lock().map_err(|_| "the S7 registry is poisoned".to_string())?.get(name).cloned();    if let Some(e) = existing {        let mut c = e.lock().map_err(|_| "the S7 connection is poisoned".to_string())?;        if c.host == host && c.local_tsap == local && c.remote_tsap == remote && c.want_pdu == want {            c.timeout = Duration::from_millis(timeout);            c.max_items = max_items;            c.ensure()?;            return Ok(json!({ "name": name, "host": c.host, "rack": rack, "slot": slot, "pdu_size": c.pdu, "remote_tsap": format!("{:02X}.{:02X}", remote >> 8, remote & 0xFF) }));        }    }    let mut c = Conn {        host,        rack,        slot,        local_tsap: local,        remote_tsap: remote,        want_pdu: want,        timeout: Duration::from_millis(timeout),        stream: None,        pdu: 240,        pdu_ref: 0,        max_items,    };    c.connect()?;    let info = json!({ "name": name, "host": c.host, "rack": rack, "slot": slot, "pdu_size": c.pdu, "remote_tsap": format!("{:02X}.{:02X}", remote >> 8, remote & 0xFF) });    let old = registry().lock().map_err(|_| "the S7 registry is poisoned".to_string())?.insert(name.to_string(), Arc::new(Mutex::new(c)));    if let Some(old) = old {        if let Ok(mut o) = old.lock() {            o.stream = None;        }    }    Ok(info)}/// Reads several addresses at once (`DB1.DBX0.0`, `DB1.DBW2:INT`, `DB1.DBD4:REAL`,/// `DB1.DBB12:STRING[20]`, `DB2.DBD0:REAL[10]`, `IW64`, `Q0.1`, `MD20:DINT`, `T1`, `C1`), in/// as few requests as the PDU allows: a map from each address to its value. One that fails/// fails the call; `s7_read_each` gives an error per address instead.pub fn s7_read(name: &str, addresses: Vec<String>) -> Result<serde_json::Value, String> {    let addrs: Vec<Addr> = addresses.iter().map(|a| parse_addr(a)).collect::<Result<_, _>>()?;    let vals = with_conn(name, &|c| read_addrs(c, &addrs))?;    let mut m = Map::new();    for (a, v) in addrs.iter().zip(vals) {        m.insert(a.text.clone(), v?);    }    Ok(Value::Object(m))}/// Reads several addresses, each giving `{ address, value }` or `{ address, error }`, so one/// missing data block does not hide the rest.pub fn s7_read_each(name: &str, addresses: Vec<String>) -> Result<Vec<serde_json::Value>, String> {    let mut parsed = vec![];    let mut out: Vec<Option<Value>> = vec![];    for a in &addresses {        match parse_addr(a) {            Ok(x) => {                parsed.push(x);                out.push(None);            }            Err(e) => out.push(Some(json!({ "address": a, "error": e }))),        }    }    let vals = with_conn(name, &|c| read_addrs(c, &parsed))?;    let mut it = parsed.iter().zip(vals);    Ok(out.into_iter().map(|o| match o {        Some(v) => v,        None => {            let (a, v) = it.next().unwrap();            match v {                Ok(v) => json!({ "address": a.text, "value": v }),                Err(e) => json!({ "address": a.text, "error": e }),            }        }    }).collect())}/// Reads one address: its value.pub fn s7_get(name: &str, address: &str) -> Result<serde_json::Value, String> {    let a = parse_addr(address)?;    let mut v = with_conn(name, &|c| read_addrs(c, std::slice::from_ref(&a)))?;    v.remove(0)}/// Writes values to addresses (`{ "DB1.DBW34:INT": 1500, "M0.1": true, "DB1.DBB12:STRING[20]": "Line 3" }`):/// every value is checked against its type before anything is sent; then `{ written }`, the/// addresses written. Bits are written one by one, never by rewriting their byte.pub fn s7_write(name: &str, values: std::collections::HashMap<String, serde_json::Value>) -> Result<serde_json::Value, String> {    let mut keys: Vec<&String> = values.keys().collect();    keys.sort();    let mut pieces: Vec<(Piece, Vec<u8>)> = vec![];    let mut owner = vec![];    let mut addrs = vec![];    for k in keys {        let a = parse_addr(k)?;        let v = &values[k];        if a.ty == Ty::Bool {            let bits: Vec<Value> = match a.count {                None => vec![v.clone()],                Some(n) => {                    let l = v.as_array().ok_or_else(|| format!("`{}` is {} bits: give a list of {} true/false", a.text, n, n))?;                    if l.len() != n {                        return Err(format!("`{}` holds {} bits; {} given", a.text, n, l.len()));                    }                    l.clone()                }            };            for (i, b) in bits.iter().enumerate() {                let on = encode_one(&Ty::Bool, b).map_err(|e| format!("`{}`: {}", a.text, e))?[0];                let at = a.bit as usize + i;                pieces.push((Piece { area: a.area, db: a.db, start: a.byte + (at / 8) as u32, bit: Some((at % 8) as u8), len: 1 }, vec![on]));                owner.push(addrs.len());            }        } else {            let bytes = encode_addr(&a, v).map_err(|e| if e.starts_with('`') { e } else { format!("`{}`: {}", a.text, e) })?;            // the caller's PDU is not known yet: split at 200 bytes, which fits every PLC's 240            let parts = split(a.area, a.db, a.byte, bytes.len(), 200);            let mut at = 0;            for p in parts {                let chunk = bytes[at..at + p.len].to_vec();                at += p.len;                pieces.push((p, chunk));                owner.push(addrs.len());            }        }        addrs.push(a);    }    let codes = with_conn(name, &|c| {        // pieces sized for this PLC's PDU        let max = c.pdu.saturating_sub(28).max(2);        let mut sized = vec![];        let mut own = vec![];        for (k, (p, b)) in pieces.iter().enumerate() {            if p.bit.is_some() || b.len() <= max {                sized.push((p.clone(), b.clone()));                own.push(owner[k]);            } else {                let mut at = 0;                for q in split(p.area, p.db, p.start, b.len(), max) {                    sized.push((q.clone(), b[at..at + q.len].to_vec()));                    own.push(owner[k]);                    at += q.len;                }            }        }        let rc = write_pieces(c, &sized)?;        Ok((rc, own))    })?;    let (rc, own) = codes;    let mut failed: Vec<String> = vec![];    let mut seen = vec![false; addrs.len()];    for (k, code) in rc.iter().enumerate() {        let i = own[k];        if *code != 0xFF && !seen[i] {            seen[i] = true;            failed.push(item_error(*code, &addrs[i].text));        }    }    if !failed.is_empty() {        let ok: Vec<&str> = addrs.iter().enumerate().filter(|(i, _)| !seen[*i]).map(|(_, a)| a.text.as_str()).collect();        return Err(format!("{}{}", failed.join("; "), if ok.is_empty() { String::new() } else { format!(" (written: {})", ok.join(", ")) }));    }    Ok(json!({ "written": addrs.iter().map(|a| a.text.clone()).collect::<Vec<_>>() }))}fn area_code(area: &str) -> Result<(u8, u16), String> {    let a = area.trim().to_uppercase();    Ok(match a.as_str() {        "I" | "E" | "INPUTS" => (AREA_I, 0),        "Q" | "A" | "OUTPUTS" => (AREA_Q, 0),        "M" | "MERKERS" | "FLAGS" => (AREA_M, 0),        "P" | "PERIPHERY" => (AREA_P, 0),        "T" | "TIMERS" => (AREA_T, 0),        "C" | "Z" | "COUNTERS" => (AREA_C, 0),        _ => match a.strip_prefix("DB").and_then(|n| n.parse::<u16>().ok()).filter(|n| *n > 0) {            Some(n) => (AREA_DB, n),            None => return Err(format!("area {:?}: use \"DB1\" (any data block), \"I\", \"Q\", \"M\", \"P\", \"T\" or \"C\"", area)),        },    })}/// Reads `count` raw bytes of an area (`"DB5"`, `"I"`, `"Q"`, `"M"`, `"P"`; for `"T"` and `"C"`,/// `count` timers or counters, two bytes each) from byte `start`: a list of byte values.pub fn s7_read_area(name: &str, area: &str, start: i64, count: i64) -> Result<Vec<u8>, String> {    let (a, db) = area_code(area)?;    if start < 0 || start > 0xFFFF || count < 1 || count > 65536 {        return Err("start is 0 to 65535 and count 1 to 65536".into());    }    let len = if a == AREA_T || a == AREA_C { count as usize * 2 } else { count as usize };    with_conn(name, &|c| {        let pieces = split(a, db, start as u32, len, c.pdu.saturating_sub(18).max(2));        let got = read_pieces(c, &pieces)?;        let mut out = vec![];        for g in got {            match g {                Ok(b) => out.extend(b),                Err(rc) => return Err(item_error(rc, &format!("{} from byte {}", area, start))),            }        }        Ok(out)    })}/// Writes raw bytes to an area from byte `start` (big data blocks split to the PDU): how many written.pub fn s7_write_area(name: &str, area: &str, start: i64, bytes: Vec<i64>) -> Result<i64, String> {    let (a, db) = area_code(area)?;    if start < 0 || start > 0xFFFF || bytes.is_empty() {        return Err("start is 0 to 65535, and give at least one byte".into());    }    let b: Vec<u8> = bytes.iter().map(|x| u8::try_from(*x).map_err(|_| format!("{} is not a byte (0 to 255)", x))).collect::<Result<_, _>>()?;    if (a == AREA_T || a == AREA_C) && b.len() % 2 == 1 {        return Err("timers and counters are two bytes each".into());    }    with_conn(name, &|c| {        let max = c.pdu.saturating_sub(28).max(2);        let mut at = 0;        let mut ps = vec![];        for p in split(a, db, start as u32, b.len(), max) {            ps.push((p.clone(), b[at..at + p.len].to_vec()));            at += p.len;        }        let rc = write_pieces(c, &ps)?;        if let Some(bad) = rc.iter().find(|x| **x != 0xFF) {            return Err(item_error(*bad, &format!("{} from byte {}", area, start)));        }        Ok(b.len() as i64)    })}/// Reads a system status list (SZL) with every fragment: `{ id, index, record_length, records }`,/// each record as hex.fn szl(c: &mut Conn, id: u16, index: u16) -> Result<(usize, Vec<Vec<u8>>), String> {    let i = id.to_be_bytes();    let x = index.to_be_bytes();    let (p, d) = c.userdata(&[0x00, 0x01, 0x12, 0x04, 0x11, 0x44, 0x01, 0x00], &[0xFF, 0x09, 0x00, 0x04, i[0], i[1], x[0], x[1]])?;    let parse = |p: &[u8], d: &[u8], body: &mut Vec<u8>| -> Result<(bool, u8, u8), String> {        if p.len() < 12 {            return Err("the PLC's SZL answer has no parameters".into());        }        let err = u16::from_be_bytes([p[10], p[11]]);        if err != 0 {            return Err(match err {                0xD401 => format!("the CPU has no SZL 0x{:04X} index 0x{:04X}", id, index),                0xD402 | 0xD404 => format!("SZL 0x{:04X}: the CPU does not offer that list", id),                _ => format!("SZL 0x{:04X}: the CPU refused it (error 0x{:04X})", id, err),            });        }        if d.len() < 4 || d[0] != 0xFF {            return Err(format!("SZL 0x{:04X}: the CPU refused it (return code 0x{:02X})", id, d.first().copied().unwrap_or(0)));        }        let len = u16::from_be_bytes([d[2], d[3]]) as usize;        let data = &d[4..(4 + len).min(d.len())];        body.extend_from_slice(data);        Ok((p[9] != 0, p[7], p[8]))    };    let mut body = vec![];    let (mut more, mut seq, mut dur) = parse(&p, &d, &mut body)?;    let mut rounds = 0;    while more && rounds < 200 {        rounds += 1;        let (p2, d2) = c.userdata(&[0x00, 0x01, 0x12, 0x08, 0x12, 0x44, 0x01, seq, dur, 0x00, 0x00, 0x00], &[0x0A, 0x00, 0x00, 0x00])?;        let r = parse(&p2, &d2, &mut body)?;        more = r.0;        seq = r.1;        dur = r.2;    }    if body.len() < 8 {        return Err(format!("SZL 0x{:04X}: a short answer", id));    }    let rec_len = u16::from_be_bytes([body[4], body[5]]) as usize;    let n = u16::from_be_bytes([body[6], body[7]]) as usize;    let mut recs = vec![];    let mut at = 8;    for _ in 0..n {        if rec_len == 0 || at + rec_len > body.len() {            break;        }        recs.push(body[at..at + rec_len].to_vec());        at += rec_len;    }    Ok((rec_len, recs))}fn hex(b: &[u8]) -> String {    b.iter().map(|x| format!("{:02x}", x)).collect()}/// Reads a system status list by its ID and index (e.g. 0x0011 = 17 for module identification,/// 0x001C = 28 for component identification, 0x0424 = 1060 for the mode): its records as hex.pub fn s7_szl(name: &str, id: i64, index: i64) -> Result<serde_json::Value, String> {    if !(0..=0xFFFF).contains(&id) || !(0..=0xFFFF).contains(&index) {        return Err("id and index are 0 to 65535".into());    }    let (rl, recs) = with_conn(name, &|c| szl(c, id as u16, index as u16))?;    Ok(json!({ "id": id, "index": index, "record_length": rl, "records": recs.iter().map(|r| hex(r)).collect::<Vec<_>>() }))}fn mode_name(m: u8) -> &'static str {    match m {        1..=4 => "stop",        5..=7 => "startup",        8 | 9 => "run",        0x0A => "hold",        0x0D => "defect",        _ => "unknown",    }}/// The CPU's operating mode: `{ state: "run" | "stop" | "startup" | "hold" | "defect" | "unknown",/// running, code, previous }` (from SZL 0x0424; `previous` is null when the CPU does not say).pub fn s7_cpu_state(name: &str) -> Result<serde_json::Value, String> {    let (_, recs) = with_conn(name, &|c| szl(c, 0x0424, 0))?;    let r = recs.first().ok_or("the CPU sent no mode record")?;    if r.len() < 4 {        return Err("a short mode record".into());    }    let now = r[3] & 0x0F;    let before = r[3] >> 4;    Ok(json!({ "state": mode_name(now), "running": mode_name(now) == "run", "code": now, "previous": if before == 0 { Value::Null } else { json!(mode_name(before)) } }))}fn text_of(b: &[u8]) -> String {    latin1(b).trim_end_matches(|c: char| c == '\0' || c == ' ').trim().to_string()}/// The CPU's identity: order code and firmware (SZL 0x0011), and its names, serial number and/// module type (SZL 0x001C); what a CPU does not offer is left out.pub fn s7_cpu_info(name: &str) -> Result<serde_json::Value, String> {    let (a, b) = with_conn(name, &|c| Ok((szl(c, 0x0011, 0), szl(c, 0x001C, 0))))?;    let mut m = Map::new();    if let Ok((_, recs)) = a {        for r in recs {            if r.len() < 28 {                continue;            }            let idx = u16::from_be_bytes([r[0], r[1]]);            if idx == 1 {                m.insert("order_code".into(), json!(text_of(&r[2..22])));            }            if idx == 7 && r[24] == b'V' {                m.insert("firmware".into(), json!(format!("V{}.{}.{}", r[25], r[26], r[27])));            }            if idx == 6 && m.get("hardware").is_none() {                m.insert("hardware".into(), json!(u16::from_be_bytes([r[26], r[27]])));            }        }    }    if let Ok((_, recs)) = b {        for r in recs {            if r.len() < 4 {                continue;            }            let idx = u16::from_be_bytes([r[0], r[1]]);            let key = match idx {                1 => "as_name",                2 => "module_name",                3 => "plant_id",                4 => "copyright",                5 => "serial_number",                7 => "module_type",                8 => "memory_card_serial",                11 => "location",                _ => continue,            };            let t = text_of(&r[2..]);            if !t.is_empty() {                m.insert(key.into(), json!(t));            }        }    }    if m.is_empty() {        return Err("the CPU answered neither SZL 0x0011 nor 0x001C".into());    }    Ok(Value::Object(m))}/// Closes the named connection: true when there was one.pub fn s7_close(name: &str) -> bool {    let c = registry().lock().ok().and_then(|mut r| r.remove(name));    match c {        Some(c) => {            if let Ok(mut c) = c.lock() {                if let Some(s) = c.stream.take() {                    let _ = s.shutdown(std::net::Shutdown::Both);                }            }            true        }        None => false,    }}/// What an address means, without a PLC: `{ area, db, byte, bit, type, count, bytes }`, or why it is wrong.pub fn s7_address(address: &str) -> Result<serde_json::Value, String> {    let a = parse_addr(address)?;    Ok(json!({        "address": a.text, "area": area_name(a.area), "db": if a.area == AREA_DB { json!(a.db) } else { Value::Null },        "byte": a.byte, "bit": if a.ty == Ty::Bool { json!(a.bit) } else { Value::Null },        "type": a.ty.name(), "count": a.count, "bytes": a.byte_len(),    }))}/// A typed value from its bytes as the PLC keeps them (big-endian): `s7_decode("REAL", [66, 145, 0, 0])` is 72.5.pub fn s7_decode(kind: &str, bytes: Vec<i64>) -> Result<serde_json::Value, String> {    let (ty, count) = parse_type(kind)?;    let b: Vec<u8> = bytes.iter().map(|x| u8::try_from(*x).map_err(|_| format!("{} is not a byte (0 to 255)", x))).collect::<Result<_, _>>()?;    let a = Addr { text: kind.to_string(), area: AREA_DB, db: 1, byte: 0, bit: 0, ty, count };    let need = if a.ty == Ty::Bool { a.count.map(|n| (n + 7) / 8).unwrap_or(1) } else { a.byte_len() };    if b.len() < need {        return Err(format!("{} needs {} bytes, {} given", kind, need, b.len()));    }    decode_addr(&a, &b)}/// A typed value as the bytes the PLC keeps (big-endian): `s7_encode("INT", -2)` is [255, 254].pub fn s7_encode(kind: &str, value: serde_json::Value) -> Result<Vec<u8>, String> {    let (ty, count) = parse_type(kind)?;    if ty == Ty::Bool && count.is_some() {        let l = value.as_array().ok_or("a BOOL array takes a list of true/false")?;        let mut out = vec![0u8; (l.len() + 7) / 8];        for (i, v) in l.iter().enumerate() {            if encode_one(&Ty::Bool, v)?[0] == 1 {                out[i / 8] |= 1 << (i % 8);            }        }        return Ok(out);    }    let a = Addr { text: kind.to_string(), area: AREA_DB, db: 1, byte: 0, bit: 0, ty, count };    encode_addr(&a, &value)}
Bonjour and mDNS mdns.qwer · 1.0.0Bonjour on the LAN: printers, web panels and screens found by service type with their host, port, addresses and TXT records, the network's service types listed, .local names resolved, your own services advertised, and a watch that sees them come and go.networkdevicesprintingiot7 views · 9 downloadsCoAP coap.qwer · 1.0.0CoAP for IoT devices: GET, POST, PUT and DELETE with retransmission and separate responses, block-wise transfers both ways, observed resources whose notifications the program drains, /.well-known/core discovery, and a CoAP server of your own on udp.listen.iotsensorsdevicesnetwork7 views · 8 downloadsTFTP tftp.qwer · 1.0.0TFTP for provisioning phones, switches and PLCs: files fetched and sent with block size, transfer size and timeout options, lost packets asked for again, netascii text; and a small server of your own on a folder, its transfers logged.networkdevicesfilesconfigiot7 views · 8 downloadsWake-on-LAN wake_on_lan.qwer · 1.0.0Machines woken from a web page: magic packets, with SecureOn passwords, sent to a subnet's broadcast address or a host; MAC addresses read in any spelling; broadcast addresses worked out; and magic packets read back for a relay.networkdevicesadminiot7 views · 7 downloadsModbus frames modbus.qwer · 1.0.0Modbus RTU and TCP frames built and read without a device: register and coil requests, answers and exceptions explained, CRC-16 and MBAP headers, registers turned into integers, floats and text in any word order and back, and a TCP client for a real PLC or meter.iotsensorsdevicesbinarynetwork8 views · 10 downloadsNTP time ntp.qwer · 1.0.0How far this server's clock is off: SNTP queries with offset, round trip, stratum and reference, several servers asked at once and the best kept (Kiss-o'-Death and unsynchronized ones set aside), NTP timestamps read, and a small time server on udp.listen.timemonitoringnetworkdevices7 views · 9 downloads
Thread
810Useful? Like it, or add to its thread below.

Thread 0 posts

On Siemens S7 PLCs: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.