| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- #!/usr/bin/env python3
- """Local S7comm PLC simulator (server).
- Stands in for a physical Siemens S7 PLC so that rtsp_multi_view can READ and
- plc_sim can WRITE a small "process image" (byte array) over S7comm / ISO-on-TCP.
- Handles: COTP Connect Request/Confirm, S7 Setup Communication, S7 Read Var
- (function 0x04) and S7 Write Var (function 0x05). Multiple clients are served
- on separate threads.
- Usage:
- plc_server.py [-p PORT] [-l LEN] [--cycle] [-t CYCLE_MS]
- -p PORT listen port (default 102; needs root on Linux)
- -l LEN process image size in bytes (default 16)
- --cycle also rotate the 6 layout-switch states every cycle
- -t MS state cycle interval in ms (default 10000)
- With --cycle the server drives byte0 through 0x00,0x03,0x0C,0x30,0x0F,0x3F
- all by itself, so plc_sim is not required.
- """
- import argparse
- import socket
- import sys
- import threading
- import time
- TS_BYTE = 0x02
- CYCLE_STATES = [0x00, 0x03, 0x0C, 0x30, 0x0F, 0x3F]
- STATE_NAMES = [
- "state 0: all bits 0 (default layout)",
- "state 1: M0.0|M0.1 (layout 0)",
- "state 2: M0.2|M0.3 (layout 1)",
- "state 3: M0.4|M0.5 (layout 2)",
- "state 4: M0.0,0.1;M0.2,0.3 (layout 3)",
- "state 5: M0.0,0.1;M0.2,0.3;M0.4,0.5 (layout 4)",
- ]
- CC = bytes([0x03, 0x00, 0x00, 0x15]) + bytes([
- 0x0B, 0xD0, 0x00, 0x00, 0x01, 0x00,
- 0xC0, 0x01, 0x0A,
- 0xC1, 0x02, 0x01, 0x00,
- 0xC2, 0x02, 0x01, 0x02,
- ])
- def put16(v):
- return bytes([(v >> 8) & 0xFF, v & 0xFF])
- def tpkt(s7):
- total = 4 + 3 + len(s7)
- return bytes([0x03, 0x00, (total >> 8) & 0xFF, total & 0xFF,
- 0x02, 0xF0, 0x80]) + s7
- def recv_exact(c, n):
- buf = b''
- while len(buf) < n:
- r = c.recv(n - len(buf))
- if not r:
- return None
- buf += r
- return buf
- def read_tpkt(c):
- h = recv_exact(c, 4)
- if not h:
- return None
- total = (h[2] << 8) | h[3]
- if total < 7 or total > 4096:
- return None
- body = recv_exact(c, total - 4)
- if body is None:
- return None
- return h + body
- class PlcServer:
- def __init__(self, area_len, cycle, cycle_ms):
- self.lock = threading.Lock()
- self.area = bytearray(area_len)
- self.cycle = cycle
- self.cycle_ms = cycle_ms
- self.state_idx = 0
- def setup_resp(self, seq):
- return bytes([0x32, 0x03, 0x00, 0x00]) + seq + \
- bytes([0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
- 0xF0, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0xE0])
- def read_var(self, seq, s7):
- ts = s7[15]
- n = (s7[16] << 8) | s7[17]
- if n > len(self.area):
- n = len(self.area)
- with self.lock:
- data = bytes(self.area[:n])
- blen = n if ts == TS_BYTE else n * 8
- resp = bytes([0x32, 0x03, 0x00, 0x00]) + seq + \
- put16(2) + put16(4 + n) + bytes([0x00, 0x00])
- resp += bytes([0x04, 0x01])
- resp += bytes([0xFF, ts]) + put16(blen) + data
- return resp
- def write_var(self, seq, s7):
- ts = s7[15]
- n = (s7[16] << 8) | s7[17]
- item = s7[24:]
- blen = (item[2] << 8) | item[3]
- nbytes = blen if ts == TS_BYTE else blen // 8
- if nbytes > len(self.area):
- nbytes = len(self.area)
- data = item[4:4 + nbytes]
- with self.lock:
- self.area[:nbytes] = data
- print("[plc] write %d bytes @%d -> byte0=0x%02X" %
- (nbytes, 0, data[0] if data else 0), flush=True)
- return bytes([0x32, 0x03, 0x00, 0x00]) + seq + \
- bytes([0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0xFF])
- def handle(self, conn, addr):
- try:
- conn.settimeout(120)
- while True:
- frame = read_tpkt(conn)
- if frame is None:
- break
- payload = frame[4:]
- if payload[1] == 0xE0: # COTP Connect Request
- conn.sendall(CC)
- elif payload[1] == 0xF0: # COTP Data
- s7 = payload[3:]
- if len(s7) < 12:
- continue
- func = s7[10]
- seq = s7[4:6]
- if func == 0xF0: # S7 Setup Communication
- conn.sendall(tpkt(self.setup_resp(seq)))
- elif func == 0x04: # S7 Read Var
- conn.sendall(tpkt(self.read_var(seq, s7)))
- elif func == 0x05: # S7 Write Var
- conn.sendall(tpkt(self.write_var(seq, s7)))
- # else: ignore unknown requests
- except Exception:
- pass
- finally:
- conn.close()
- print("[plc] client %s:%d disconnected" % addr, flush=True)
- def cycle_loop(self):
- while True:
- time.sleep(self.cycle_ms / 1000.0)
- v = CYCLE_STATES[self.state_idx]
- with self.lock:
- self.area[0] = v
- print("[plc] %s (byte0=0x%02X)" %
- (STATE_NAMES[self.state_idx], v), flush=True)
- self.state_idx = (self.state_idx + 1) % len(CYCLE_STATES)
- def main():
- ap = argparse.ArgumentParser(description="Local S7comm PLC simulator")
- ap.add_argument("-p", "--port", type=int, default=102)
- ap.add_argument("-l", "--len", type=int, default=16)
- ap.add_argument("--cycle", action="store_true",
- help="self-rotate the 6 layout states every cycle")
- ap.add_argument("-t", "--cycle-ms", type=int, default=10000)
- args = ap.parse_args()
- server = PlcServer(args.len, args.cycle, args.cycle_ms)
- if args.cycle:
- threading.Thread(target=server.cycle_loop, daemon=True).start()
- srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- srv.bind(("0.0.0.0", args.port))
- srv.listen(8)
- print("[plc] local PLC listening on 0.0.0.0:%d area_len=%d%s" %
- (args.port, args.len, " (cycling)" if args.cycle else ""), flush=True)
- while True:
- conn, addr = srv.accept()
- print("[plc] client %s:%d connected" % addr, flush=True)
- threading.Thread(target=server.handle, args=(conn, addr),
- daemon=True).start()
- if __name__ == "__main__":
- try:
- main()
- except KeyboardInterrupt:
- sys.exit(0)
|