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

Nessuna libreria esterna: solo Python 3, gira su Linux e su Windows.

Tiene 100 holding register e 100 input register:
  - gli input register 0 e 1 cambiano da soli (contatore e secondi), cosi'
    dal PLC si vede subito se la lettura funziona;
  - quello che il PLC scrive negli holding register viene stampato.

  python3 modbus_server.py            # porta 502 (su Linux serve sudo)
  python3 modbus_server.py --porta 5020
"""
import argparse
import socket
import struct
import threading
import time

N_REG = 100
holding = [0] * N_REG
inputs = [0] * N_REG
lock = threading.Lock()


def aggiorna_input():
    n = 0
    while True:
        with lock:
            inputs[0] = n & 0xFFFF
            inputs[1] = int(time.time()) % 60
        n += 1
        time.sleep(0.1)


def eccezione(funzione, codice):
    return struct.pack(">BB", funzione | 0x80, codice)


def gestisci(pdu, peer):
    funzione = pdu[0]
    if funzione in (3, 4):
        inizio, quanti = struct.unpack(">HH", pdu[1:5])
        if quanti < 1 or quanti > 125 or inizio + quanti > N_REG:
            return eccezione(funzione, 2)
        with lock:
            regs = (holding if funzione == 3 else inputs)[inizio:inizio + quanti]
        return struct.pack(">BB", funzione, 2 * quanti) + struct.pack(">%dH" % quanti, *regs)
    if funzione == 6:
        indirizzo, valore = struct.unpack(">HH", pdu[1:5])
        if indirizzo >= N_REG:
            return eccezione(funzione, 2)
        with lock:
            holding[indirizzo] = valore
        print("%s scrive holding[%d] = %d" % (peer, indirizzo, valore))
        return pdu[:5]
    if funzione == 16:
        inizio, quanti, _nbyte = struct.unpack(">HHB", pdu[1:6])
        if inizio + quanti > N_REG:
            return eccezione(funzione, 2)
        valori = struct.unpack(">%dH" % quanti, pdu[6:6 + 2 * quanti])
        with lock:
            holding[inizio:inizio + quanti] = valori
        print("%s scrive holding[%d..%d] = %s" % (peer, inizio, inizio + quanti - 1, list(valori)))
        return struct.pack(">BHH", 16, inizio, quanti)
    if funzione == 23:
        r_ini, r_n, w_ini, w_n, _nb = struct.unpack(">HHHHB", pdu[1:10])
        if r_ini + r_n > N_REG or w_ini + w_n > N_REG:
            return eccezione(funzione, 2)
        valori = struct.unpack(">%dH" % w_n, pdu[10:10 + 2 * w_n])
        with lock:
            holding[w_ini:w_ini + w_n] = valori
            regs = holding[r_ini:r_ini + r_n]
        print("%s scrive holding[%d..%d] = %s" % (peer, w_ini, w_ini + w_n - 1, list(valori)))
        return struct.pack(">BB", 23, 2 * r_n) + struct.pack(">%dH" % r_n, *regs)
    return eccezione(funzione, 1)


def ricevi(conn, n):
    buf = b""
    while len(buf) < n:
        parte = conn.recv(n - len(buf))
        if not parte:
            raise ConnectionError
        buf += parte
    return buf


def cliente(conn, indirizzo):
    peer = "%s:%d" % indirizzo
    print("Connesso: %s" % peer)
    letture = 0
    try:
        while True:
            tid, proto, lunghezza, unit = struct.unpack(">HHHB", ricevi(conn, 7))
            pdu = ricevi(conn, lunghezza - 1)
            risposta = gestisci(pdu, peer)
            if pdu[0] in (3, 4):
                letture += 1
                if letture % 50 == 1:
                    print("%s legge (funzione %d), %d letture finora" % (peer, pdu[0], letture))
            conn.sendall(struct.pack(">HHHB", tid, proto, len(risposta) + 1, unit) + risposta)
    except (ConnectionError, OSError):
        pass
    finally:
        conn.close()
        print("Disconnesso: %s" % peer)


def main():
    p = argparse.ArgumentParser(description="Server Modbus TCP di prova")
    p.add_argument("--porta", type=int, default=502)
    a = p.parse_args()

    threading.Thread(target=aggiorna_input, daemon=True).start()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", a.porta))
    s.listen(4)
    print("Server Modbus TCP in ascolto sulla porta %d (Ctrl+C per uscire)" % a.porta)
    print("Input register 0 = contatore, 1 = secondi; holding register 0..%d scrivibili" % (N_REG - 1))
    try:
        while True:
            conn, indirizzo = s.accept()
            threading.Thread(target=cliente, args=(conn, indirizzo), daemon=True).start()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
