plc_server.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python3
  2. """Local S7comm PLC simulator (server).
  3. Stands in for a physical Siemens S7 PLC so that rtsp_multi_view can READ and
  4. plc_sim can WRITE a small "process image" (byte array) over S7comm / ISO-on-TCP.
  5. Handles: COTP Connect Request/Confirm, S7 Setup Communication, S7 Read Var
  6. (function 0x04) and S7 Write Var (function 0x05). Multiple clients are served
  7. on separate threads.
  8. Usage:
  9. plc_server.py [-p PORT] [-l LEN] [--cycle] [-t CYCLE_MS]
  10. -p PORT listen port (default 102; needs root on Linux)
  11. -l LEN process image size in bytes (default 16)
  12. --cycle also rotate the 6 layout-switch states every cycle
  13. -t MS state cycle interval in ms (default 10000)
  14. With --cycle the server drives byte0 through 0x00,0x03,0x0C,0x30,0x0F,0x3F
  15. all by itself, so plc_sim is not required.
  16. """
  17. import argparse
  18. import socket
  19. import sys
  20. import threading
  21. import time
  22. TS_BYTE = 0x02
  23. CYCLE_STATES = [0x00, 0x03, 0x0C, 0x30, 0x0F, 0x3F]
  24. STATE_NAMES = [
  25. "state 0: all bits 0 (default layout)",
  26. "state 1: M0.0|M0.1 (layout 0)",
  27. "state 2: M0.2|M0.3 (layout 1)",
  28. "state 3: M0.4|M0.5 (layout 2)",
  29. "state 4: M0.0,0.1;M0.2,0.3 (layout 3)",
  30. "state 5: M0.0,0.1;M0.2,0.3;M0.4,0.5 (layout 4)",
  31. ]
  32. CC = bytes([0x03, 0x00, 0x00, 0x15]) + bytes([
  33. 0x0B, 0xD0, 0x00, 0x00, 0x01, 0x00,
  34. 0xC0, 0x01, 0x0A,
  35. 0xC1, 0x02, 0x01, 0x00,
  36. 0xC2, 0x02, 0x01, 0x02,
  37. ])
  38. def put16(v):
  39. return bytes([(v >> 8) & 0xFF, v & 0xFF])
  40. def tpkt(s7):
  41. total = 4 + 3 + len(s7)
  42. return bytes([0x03, 0x00, (total >> 8) & 0xFF, total & 0xFF,
  43. 0x02, 0xF0, 0x80]) + s7
  44. def recv_exact(c, n):
  45. buf = b''
  46. while len(buf) < n:
  47. r = c.recv(n - len(buf))
  48. if not r:
  49. return None
  50. buf += r
  51. return buf
  52. def read_tpkt(c):
  53. h = recv_exact(c, 4)
  54. if not h:
  55. return None
  56. total = (h[2] << 8) | h[3]
  57. if total < 7 or total > 4096:
  58. return None
  59. body = recv_exact(c, total - 4)
  60. if body is None:
  61. return None
  62. return h + body
  63. class PlcServer:
  64. def __init__(self, area_len, cycle, cycle_ms):
  65. self.lock = threading.Lock()
  66. self.area = bytearray(area_len)
  67. self.cycle = cycle
  68. self.cycle_ms = cycle_ms
  69. self.state_idx = 0
  70. def setup_resp(self, seq):
  71. return bytes([0x32, 0x03, 0x00, 0x00]) + seq + \
  72. bytes([0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
  73. 0xF0, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0xE0])
  74. def read_var(self, seq, s7):
  75. ts = s7[15]
  76. n = (s7[16] << 8) | s7[17]
  77. if n > len(self.area):
  78. n = len(self.area)
  79. with self.lock:
  80. data = bytes(self.area[:n])
  81. blen = n if ts == TS_BYTE else n * 8
  82. resp = bytes([0x32, 0x03, 0x00, 0x00]) + seq + \
  83. put16(2) + put16(4 + n) + bytes([0x00, 0x00])
  84. resp += bytes([0x04, 0x01])
  85. resp += bytes([0xFF, ts]) + put16(blen) + data
  86. return resp
  87. def write_var(self, seq, s7):
  88. ts = s7[15]
  89. n = (s7[16] << 8) | s7[17]
  90. item = s7[24:]
  91. blen = (item[2] << 8) | item[3]
  92. nbytes = blen if ts == TS_BYTE else blen // 8
  93. if nbytes > len(self.area):
  94. nbytes = len(self.area)
  95. data = item[4:4 + nbytes]
  96. with self.lock:
  97. self.area[:nbytes] = data
  98. print("[plc] write %d bytes @%d -> byte0=0x%02X" %
  99. (nbytes, 0, data[0] if data else 0), flush=True)
  100. return bytes([0x32, 0x03, 0x00, 0x00]) + seq + \
  101. bytes([0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x05, 0x01, 0xFF])
  102. def handle(self, conn, addr):
  103. try:
  104. conn.settimeout(120)
  105. while True:
  106. frame = read_tpkt(conn)
  107. if frame is None:
  108. break
  109. payload = frame[4:]
  110. if payload[1] == 0xE0: # COTP Connect Request
  111. conn.sendall(CC)
  112. elif payload[1] == 0xF0: # COTP Data
  113. s7 = payload[3:]
  114. if len(s7) < 12:
  115. continue
  116. func = s7[10]
  117. seq = s7[4:6]
  118. if func == 0xF0: # S7 Setup Communication
  119. conn.sendall(tpkt(self.setup_resp(seq)))
  120. elif func == 0x04: # S7 Read Var
  121. conn.sendall(tpkt(self.read_var(seq, s7)))
  122. elif func == 0x05: # S7 Write Var
  123. conn.sendall(tpkt(self.write_var(seq, s7)))
  124. # else: ignore unknown requests
  125. except Exception:
  126. pass
  127. finally:
  128. conn.close()
  129. print("[plc] client %s:%d disconnected" % addr, flush=True)
  130. def cycle_loop(self):
  131. while True:
  132. time.sleep(self.cycle_ms / 1000.0)
  133. v = CYCLE_STATES[self.state_idx]
  134. with self.lock:
  135. self.area[0] = v
  136. print("[plc] %s (byte0=0x%02X)" %
  137. (STATE_NAMES[self.state_idx], v), flush=True)
  138. self.state_idx = (self.state_idx + 1) % len(CYCLE_STATES)
  139. def main():
  140. ap = argparse.ArgumentParser(description="Local S7comm PLC simulator")
  141. ap.add_argument("-p", "--port", type=int, default=102)
  142. ap.add_argument("-l", "--len", type=int, default=16)
  143. ap.add_argument("--cycle", action="store_true",
  144. help="self-rotate the 6 layout states every cycle")
  145. ap.add_argument("-t", "--cycle-ms", type=int, default=10000)
  146. args = ap.parse_args()
  147. server = PlcServer(args.len, args.cycle, args.cycle_ms)
  148. if args.cycle:
  149. threading.Thread(target=server.cycle_loop, daemon=True).start()
  150. srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  151. srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  152. srv.bind(("0.0.0.0", args.port))
  153. srv.listen(8)
  154. print("[plc] local PLC listening on 0.0.0.0:%d area_len=%d%s" %
  155. (args.port, args.len, " (cycling)" if args.cycle else ""), flush=True)
  156. while True:
  157. conn, addr = srv.accept()
  158. print("[plc] client %s:%d connected" % addr, flush=True)
  159. threading.Thread(target=server.handle, args=(conn, addr),
  160. daemon=True).start()
  161. if __name__ == "__main__":
  162. try:
  163. main()
  164. except KeyboardInterrupt:
  165. sys.exit(0)