Guest User

Untitled

a guest
Apr 15th, 2026
153
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.53 KB | Source Code | 0 0
  1. """
  2. Omron PLC FINS Protocol - Read Data Memory (DM) Words
  3. ======================================================
  4. Reads 3 consecutive DM words from an Omron PLC using FINS over UDP.
  5.  
  6. FINS Frame Structure:
  7.  Header (10 bytes) + Command (2 bytes) + Address (4 bytes) + Count (2 bytes)
  8.  
  9. Usage:
  10.  python omron_fins_reader.py
  11.  python omron_fins_reader.py --host 192.168.1.10 --dm 100 --count 3
  12. """
  13.  
  14. import socket
  15. import struct
  16. import argparse
  17. import time
  18.  
  19.  
  20. # ---------------------------------------------------------------------------
  21. # FINS Constants
  22. # ---------------------------------------------------------------------------
  23. FINS_PORT          = 9600          # Default Omron FINS UDP port
  24. DEFAULT_TIMEOUT    = 3.0           # Socket timeout in seconds
  25.  
  26. # Memory area codes
  27. MEM_DM_WORD        = 0x82          # DM area, word access
  28. MEM_DM_BIT         = 0x02          # DM area, bit access  (not used here)
  29. MEM_CIO_WORD       = 0xB0          # CIO/Work area, word access
  30.  
  31. # FINS command codes
  32. CMD_MEMORY_READ    = (0x01, 0x01)  # MEMORY AREA READ
  33. CMD_MEMORY_WRITE   = (0x01, 0x02)  # MEMORY AREA WRITE
  34.  
  35.  
  36. # ---------------------------------------------------------------------------
  37. # FINS Frame Builder
  38. # ---------------------------------------------------------------------------
  39. def build_fins_read_frame(
  40.     dst_net: int,
  41.     dst_node: int,
  42.     dst_unit: int,
  43.     src_net: int,
  44.     src_node: int,
  45.     src_unit: int,
  46.     sid: int,
  47.     mem_area: int,
  48.     start_address: int,
  49.     start_bit: int,
  50.     word_count: int,
  51. ) -> bytes:
  52.     """
  53.    Build a FINS UDP read request frame.
  54.  
  55.    Frame layout (18 bytes total for a read command):
  56.      [0]     ICF  – Information Control Field  (0x80 = command, no response needed flag clear)
  57.      [1]     RSV  – Reserved                  (0x00)
  58.      [2]     GCT  – Gateway Count             (0x02)
  59.      [3]     DNA  – Destination Network      
  60.      [4]     DA1  – Destination Node          
  61.      [5]     DA2  – Destination Unit          
  62.      [6]     SNA  – Source Network            
  63.      [7]     SA1  – Source Node              
  64.      [8]     SA2  – Source Unit              
  65.      [9]     SID  – Service ID (counter)      
  66.      [10:12] MRC/SRC – Command code          
  67.      [12]    Memory area code                
  68.      [13:15] Start address (word, big-endian)
  69.      [15]    Start bit                        
  70.      [16:18] Word count (big-endian)          
  71.    """
  72.     header = struct.pack(
  73.         "BBBBBBBBBB",
  74.         0x80,       # ICF: command frame
  75.         0x00,       # RSV
  76.         0x02,       # GCT: gateway count (max hops)
  77.         dst_net,    # DNA
  78.         dst_node,   # DA1
  79.         dst_unit,   # DA2
  80.         src_net,    # SNA
  81.         src_node,   # SA1
  82.         src_unit,   # SA2
  83.         sid & 0xFF, # SID
  84.     )
  85.     command = struct.pack("BB", *CMD_MEMORY_READ)
  86.     address = struct.pack(
  87.         "BBHB",
  88.         mem_area,
  89.         (start_address >> 8) & 0xFF,
  90.         start_address & 0xFF,
  91.         start_bit,
  92.     )
  93.     # Re-pack properly: area(1) + addr_high(1) + addr_low(1) + bit(1)
  94.     address = bytes([
  95.         mem_area,
  96.         (start_address >> 8) & 0xFF,
  97.         start_address & 0xFF,
  98.         start_bit,
  99.     ])
  100.     count = struct.pack(">H", word_count)
  101.     return header + command + address + count
  102.  
  103.  
  104. # ---------------------------------------------------------------------------
  105. # FINS Response Parser
  106. # ---------------------------------------------------------------------------
  107. def parse_fins_response(data: bytes, expected_words: int) -> list[int]:
  108.     """
  109.    Parse a FINS MEMORY AREA READ response.
  110.  
  111.    Response layout:
  112.      [0:10]  FINS header (same structure as request)
  113.      [10:12] Command code echo (01 01)
  114.      [12:14] End code (00 00 = success)
  115.      [14:]   Data: word_count × 2 bytes, big-endian unsigned shorts
  116.    """
  117.     if len(data) < 14:
  118.         raise ValueError(f"Response too short: {len(data)} bytes")
  119.  
  120.     # End code
  121.     end_code = struct.unpack_from(">H", data, 12)[0]
  122.     if end_code != 0x0000:
  123.         main_code = (end_code >> 8) & 0xFF
  124.         sub_code  = end_code & 0xFF
  125.         raise RuntimeError(
  126.             f"FINS error – main: 0x{main_code:02X}, sub: 0x{sub_code:02X}  "
  127.             f"({fins_error_description(main_code, sub_code)})"
  128.         )
  129.  
  130.     payload = data[14:]
  131.     required_bytes = expected_words * 2
  132.     if len(payload) < required_bytes:
  133.         raise ValueError(
  134.             f"Payload too short: got {len(payload)} bytes, "
  135.             f"expected {required_bytes} for {expected_words} words"
  136.         )
  137.  
  138.     words = []
  139.     for i in range(expected_words):
  140.         word = struct.unpack_from(">H", payload, i * 2)[0]
  141.         words.append(word)
  142.     return words
  143.  
  144.  
  145. # ---------------------------------------------------------------------------
  146. # Error code lookup (subset of common codes)
  147. # ---------------------------------------------------------------------------
  148. def fins_error_description(main: int, sub: int) -> str:
  149.     errors = {
  150.         (0x01, 0x01): "Local node not in network",
  151.         (0x02, 0x01): "Destination node not in network",
  152.         (0x03, 0x01): "Communications controller error",
  153.         (0x04, 0x01): "Command not supported",
  154.         (0x05, 0x01): "Routing table error",
  155.         (0x10, 0x01): "Command too long",
  156.         (0x10, 0x02): "Command too short",
  157.         (0x11, 0x01): "Area type error (wrong memory area)",
  158.         (0x11, 0x02): "Access size error",
  159.         (0x11, 0x03): "Address range error",
  160.         (0x20, 0x02): "Protected – cannot write",
  161.         (0x21, 0x01): "CPU unit in wrong mode",
  162.         (0x22, 0x01): "Service already executing",
  163.         (0x23, 0x05): "Aborted",
  164.     }
  165.     return errors.get((main, sub), "Unknown error")
  166.  
  167.  
  168. # ---------------------------------------------------------------------------
  169. # Main reader class
  170. # ---------------------------------------------------------------------------
  171. class OmronFINSReader:
  172.     def __init__(
  173.         self,
  174.         plc_host: str,
  175.         plc_port: int = FINS_PORT,
  176.         plc_node: int = 0,        # DA1: 0 = auto-detect by IP
  177.         src_node: int = 1,        # SA1: this PC's node number (1–254)
  178.         timeout: float = DEFAULT_TIMEOUT,
  179.     ):
  180.         self.plc_host = plc_host
  181.         self.plc_port = plc_port
  182.         self.plc_node = plc_node
  183.         self.src_node = src_node
  184.         self.timeout  = timeout
  185.         self._sid     = 0
  186.         self._sock    = None
  187.  
  188.     def connect(self):
  189.         """Open UDP socket."""
  190.         self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  191.         self._sock.settimeout(self.timeout)
  192.         print(f"[FINS] UDP socket ready → {self.plc_host}:{self.plc_port}")
  193.  
  194.     def disconnect(self):
  195.         if self._sock:
  196.             self._sock.close()
  197.             self._sock = None
  198.             print("[FINS] Socket closed.")
  199.  
  200.     def _next_sid(self) -> int:
  201.         self._sid = (self._sid + 1) & 0xFF
  202.         return self._sid
  203.  
  204.     def read_dm_words(self, start_address: int, count: int) -> list[int]:
  205.         """
  206.        Read `count` words starting at DM[start_address].
  207.        Returns a list of unsigned 16-bit integers.
  208.        """
  209.         if self._sock is None:
  210.             raise RuntimeError("Not connected. Call connect() first.")
  211.  
  212.         # Resolve node from last octet of IP if not set explicitly
  213.         plc_node = self.plc_node or int(self.plc_host.split(".")[-1])
  214.  
  215.         frame = build_fins_read_frame(
  216.             dst_net=0,
  217.             dst_node=plc_node,
  218.             dst_unit=0x00,        # 0x00 = CPU unit
  219.             src_net=0,
  220.             src_node=self.src_node,
  221.             src_unit=0x00,
  222.             sid=self._next_sid(),
  223.             mem_area=MEM_DM_WORD,
  224.             start_address=start_address,
  225.             start_bit=0,
  226.             word_count=count,
  227.         )
  228.  
  229.         print(f"[FINS] TX ({len(frame)} bytes): {frame.hex(' ').upper()}")
  230.         self._sock.sendto(frame, (self.plc_host, self.plc_port))
  231.  
  232.         response, addr = self._sock.recvfrom(2048)
  233.         print(f"[FINS] RX ({len(response)} bytes) from {addr}: {response.hex(' ').upper()}")
  234.  
  235.         return parse_fins_response(response, count)
  236.  
  237.  
  238. # ---------------------------------------------------------------------------
  239. # Entry point
  240. # ---------------------------------------------------------------------------
  241. def main():
  242.     parser = argparse.ArgumentParser(
  243.         description="Read DM words from an Omron PLC via FINS/UDP"
  244.     )
  245.     parser.add_argument("--host",    default="192.168.1.10", help="PLC IP address")
  246.     parser.add_argument("--port",    default=FINS_PORT, type=int, help="FINS UDP port (default 9600)")
  247.     parser.add_argument("--dm",      default=100, type=int, help="DM start address (default 100)")
  248.     parser.add_argument("--count",   default=3,   type=int, help="Number of words to read (default 3)")
  249.     parser.add_argument("--src-node",default=1,   type=int, help="Source node number (default 1)")
  250.     parser.add_argument("--plc-node",default=0,   type=int, help="PLC node (0 = auto from IP)")
  251.     parser.add_argument("--repeat",  default=1,   type=int, help="Poll N times (0 = forever)")
  252.     parser.add_argument("--interval",default=1.0, type=float, help="Seconds between polls")
  253.     args = parser.parse_args()
  254.  
  255.     reader = OmronFINSReader(
  256.         plc_host=args.host,
  257.         plc_port=args.port,
  258.         plc_node=args.plc_node,
  259.         src_node=args.src_node,
  260.     )
  261.     reader.connect()
  262.  
  263.     iteration = 0
  264.     try:
  265.         while True:
  266.             iteration += 1
  267.             print(f"\n─── Poll #{iteration} ───────────────────────────────")
  268.             try:
  269.                 words = reader.read_dm_words(args.dm, args.count)
  270.                 print(f"\n  DM area read  (start=D{args.dm:05d}, count={args.count})")
  271.                 print(f"  {'Address':<12} {'Dec':>6}  {'Hex':>6}  {'Bin':>18}")
  272.                 print(f"  {'-'*46}")
  273.                 for i, w in enumerate(words):
  274.                     addr = args.dm + i
  275.                     print(f"  D{addr:05d}      {w:>6}  {w:#06x}  {w:016b}")
  276.             except RuntimeError as e:
  277.                 print(f"  [ERROR] {e}")
  278.             except socket.timeout:
  279.                 print("  [TIMEOUT] No response from PLC.")
  280.  
  281.             if args.repeat != 0 and iteration >= args.repeat:
  282.                 break
  283.             time.sleep(args.interval)
  284.  
  285.     except KeyboardInterrupt:
  286.         print("\n[INFO] Interrupted by user.")
  287.     finally:
  288.         reader.disconnect()
  289.  
  290.  
  291. if __name__ == "__main__":
  292.     main()
Tags: Fins python
Advertisement
Add Comment
Please, Sign In to add comment