#!/usr/bin/env python3
"""Dryer #2 Modbus TCP simulator for PLC profiling tests (read-only, no dependencies).

Serves function codes 1/2/3/4. Writes (5/6/15/16) are refused with exception 01.
Unknown addresses inside the served blocks read as 0, so a profiling scan sees a realistic
mostly-empty register map with a few live and constant points.

Usage:  python3 dryer2_sim.py [port]        (default port 5020, unit id ignored)
"""
import math
import socketserver
import struct
import sys
import threading
import time

START = time.time()
SIZE = 2000  # each table has addresses 0..1999


def holding(addr):
    t = time.time() - START
    if addr == 0:   # dryer temperature now, x10 (degC)  ~ 99.0 .. 102.0
        return int(1005 + 15 * math.sin(t / 30.0))
    if addr == 1:   # exhaust damper opening now, x10 (%)
        return int(980 + 40 * math.sin(t / 45.0 + 1.0))
    if addr == 2:   # batch number
        return 35
    if addr == 3:   # production counter, +1 every 3 s, wraps at 65536
        return int(t // 3) % 65536
    if addr == 4:   # total running hours
        return 111
    if addr == 5:   # recipe number
        return 640
    if addr == 10:  # dryer temperature setpoint, x10 -> 100.8 degC
        return 1008
    if addr == 11:  # exhaust damper setpoint, x10 -> 100.9 %
        return 1009
    if addr == 12:  # supply pressure setpoint, x100 -> 10.15 bar
        return 1015
    if addr == 13:  # conveyor speed setpoint, x100 -> 10.16 m/min
        return 1016
    if addr == 14:  # high temperature alarm setpoint, x10 -> 102.0 degC
        return 1020
    if addr in (20, 21):  # supply pressure now, FLOAT32 high word first, ~10.0 .. 10.3 bar
        v = 10.15 + 0.15 * math.sin(t / 20.0)
        hi, lo = struct.unpack('>HH', struct.pack('>f', v))
        return hi if addr == 20 else lo
    return 0


def input_reg(addr):
    if addr == 0:   # production quantity shown on the HMI
        return 999
    if addr == 1:   # running hours shown on the HMI
        return 111
    return 0


def coil(addr):
    # 0 run, 1 stop, 2 auto, 3 fault
    return {0: 1, 1: 0, 2: 1, 3: 0}.get(addr, 0)


def discrete(addr):
    # 0 door closed, 1 emergency stop pressed, 2 burner flame on (blinks every 5 s)
    if addr == 0:
        return 1
    if addr == 2:
        return int((time.time() - START) // 5) % 2
    return 0


class Handler(socketserver.BaseRequestHandler):
    def handle(self):
        sock = self.request
        while True:
            head = self._recv(sock, 7)
            if not head:
                return
            tid, pid, length, unit = struct.unpack('>HHHB', head)
            body = self._recv(sock, length - 1)
            if not body:
                return
            fc = body[0]
            if fc in (1, 2, 3, 4) and len(body) >= 5:
                start, count = struct.unpack('>HH', body[1:5])
                if count < 1 or start + count > SIZE or (fc in (3, 4) and count > 125) or count > 2000:
                    pdu = struct.pack('>BB', fc | 0x80, 2)  # illegal data address
                elif fc in (3, 4):
                    fn = holding if fc == 3 else input_reg
                    vals = [fn(a) & 0xFFFF for a in range(start, start + count)]
                    pdu = struct.pack('>BB', fc, 2 * count) + struct.pack('>%dH' % count, *vals)
                else:
                    fn = coil if fc == 1 else discrete
                    bits = [fn(a) for a in range(start, start + count)]
                    data = bytearray((count + 7) // 8)
                    for i, b in enumerate(bits):
                        if b:
                            data[i // 8] |= 1 << (i % 8)
                    pdu = struct.pack('>BB', fc, len(data)) + bytes(data)
            else:
                pdu = struct.pack('>BB', (fc | 0x80) & 0xFF, 1)  # illegal function (read-only)
            sock.sendall(struct.pack('>HHHB', tid, pid, len(pdu) + 1, unit) + pdu)

    @staticmethod
    def _recv(sock, n):
        buf = b''
        while len(buf) < n:
            chunk = sock.recv(n - len(buf))
            if not chunk:
                return None
            buf += chunk
        return buf


class Server(socketserver.ThreadingTCPServer):
    allow_reuse_address = True
    daemon_threads = True


if __name__ == '__main__':
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 5020
    with Server(('0.0.0.0', port), Handler) as srv:
        print('dryer2 simulator listening on :%d' % port, flush=True)
        srv.serve_forever()
