// 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,
}
