#!/usr/bin/env python3
"""Client Modbus TCP minimo per provare il Modbus TCP Server dell'nXPlc.

Nessuna libreria esterna: solo i socket di Python 3, quindi gira uguale su
Linux e su Windows.

Nel Modbus TCP Server di CODESYS:
  - Holding Registers = ingressi del PLC (%IW): il client li SCRIVE;
  - Input Registers   = uscite del PLC (%QW):  il client li LEGGE.

Esempi:
  python3 modbus_client.py 192.168.1.50 leggi-input 0 10
  python3 modbus_client.py 192.168.1.50 leggi-holding 0 10
  python3 modbus_client.py 192.168.1.50 scrivi 0 123 456 789
  python3 modbus_client.py 192.168.1.50 leggi-input 0 10 --ogni 1
"""
import argparse
import socket
import struct
import sys
import time

ECCEZIONI = {
    1: "funzione non supportata",
    2: "indirizzo non valido (fuori dai registri configurati)",
    3: "valore non valido",
    4: "errore del dispositivo",
    6: "dispositivo occupato",
}


class ModbusTcp:
    def __init__(self, ip, porta, unit, timeout):
        self.unit = unit
        self.tid = 0
        self.s = socket.create_connection((ip, porta), timeout=timeout)

    def _richiesta(self, funzione, dati):
        self.tid = (self.tid + 1) & 0xFFFF
        pdu = struct.pack(">B", funzione) + dati
        mbap = struct.pack(">HHHB", self.tid, 0, len(pdu) + 1, self.unit)
        self.s.sendall(mbap + pdu)

        testa = self._ricevi(7)
        tid, _proto, lunghezza, _unit = struct.unpack(">HHHB", testa)
        corpo = self._ricevi(lunghezza - 1)
        if tid != self.tid:
            raise IOError("risposta a un'altra richiesta (tid %d invece di %d)" % (tid, self.tid))
        if corpo[0] & 0x80:
            codice = corpo[1]
            raise IOError("eccezione Modbus %d: %s" % (codice, ECCEZIONI.get(codice, "sconosciuta")))
        return corpo[1:]

    def _ricevi(self, n):
        buf = b""
        while len(buf) < n:
            parte = self.s.recv(n - len(buf))
            if not parte:
                raise IOError("il PLC ha chiuso la connessione")
            buf += parte
        return buf

    def leggi(self, funzione, inizio, quanti):
        r = self._richiesta(funzione, struct.pack(">HH", inizio, quanti))
        return list(struct.unpack(">%dH" % (r[0] // 2), r[1:1 + r[0]]))

    def scrivi(self, inizio, valori):
        dati = struct.pack(">HHB", inizio, len(valori), 2 * len(valori))
        dati += struct.pack(">%dH" % len(valori), *valori)
        self._richiesta(16, dati)


def main():
    p = argparse.ArgumentParser(description="Client Modbus TCP per l'nXPlc")
    p.add_argument("ip")
    p.add_argument("comando", choices=["leggi-input", "leggi-holding", "scrivi"])
    p.add_argument("inizio", type=int, help="primo registro (da 0)")
    p.add_argument("valori", type=int, nargs="+",
                   help="per leggi-*: quanti registri; per scrivi: i valori")
    p.add_argument("--porta", type=int, default=502)
    p.add_argument("--unit", type=int, default=1, help="unit ID (default 1)")
    p.add_argument("--timeout", type=float, default=3.0)
    p.add_argument("--ogni", type=float, default=0,
                   help="ripete ogni N secondi (Ctrl+C per uscire)")
    a = p.parse_args()

    try:
        mb = ModbusTcp(a.ip, a.porta, a.unit, a.timeout)
    except OSError as e:
        sys.exit("Connessione a %s:%d fallita: %s" % (a.ip, a.porta, e))
    print("Connesso a %s:%d (unit %d)" % (a.ip, a.porta, a.unit))

    try:
        while True:
            if a.comando == "scrivi":
                mb.scrivi(a.inizio, a.valori)
                print("Scritti %d holding register da %d: %s" % (len(a.valori), a.inizio, a.valori))
            else:
                funzione = 4 if a.comando == "leggi-input" else 3
                regs = mb.leggi(funzione, a.inizio, a.valori[0])
                nome = "Input" if funzione == 4 else "Holding"
                print("%s %d..%d: %s" % (nome, a.inizio, a.inizio + len(regs) - 1, regs))
            if a.ogni <= 0:
                break
            time.sleep(a.ogni)
    except (OSError, IOError) as e:
        sys.exit("Errore: %s" % e)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
