pyfactoryio 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ from pyfactoryio.server import FactoryIOServer
2
+ from pyfactoryio.gui import FactoryIOGUI
pyfactoryio/gui.py ADDED
@@ -0,0 +1,97 @@
1
+ import tkinter as tk
2
+ from pyModbusTCP.server import ModbusServer
3
+
4
+
5
+ class FactoryIOGUI:
6
+ def __init__(self, master, server, di_names=None, do_names=None, ri_names=None, ro_names=None):
7
+ self.master = master
8
+ self.master.title("Factory I/O Modbus Server Interface")
9
+ self.master.geometry("800x600")
10
+ self.master.configure(padx=20, pady=20)
11
+ self.server = server
12
+
13
+ di_count = len(di_names) if di_names else 0
14
+ do_count = len(do_names) if do_names else 0
15
+ ri_count = len(ri_names) if ri_names else 0
16
+ ro_count = len(ro_names) if ro_names else 0
17
+
18
+ self.frame_di = tk.LabelFrame(master, text="Digital Inputs (Sensors)", padx=10, pady=10)
19
+ self.frame_do = tk.LabelFrame(master, text="Digital Outputs (Actuators)", padx=10, pady=10)
20
+ self.frame_ri = tk.LabelFrame(master, text="Register Inputs (Sensors)", padx=10, pady=10)
21
+ self.frame_ro = tk.LabelFrame(master, text="Register Outputs (Actuators)", padx=10, pady=10)
22
+
23
+ cols = sum(1 for c in [di_count, do_count, ri_count, ro_count] if c > 0)
24
+ col = 0
25
+ if di_count:
26
+ self.frame_di.grid(row=0, column=col, padx=10, pady=10, sticky="n")
27
+ col += 1
28
+ if do_count:
29
+ self.frame_do.grid(row=0, column=col, padx=10, pady=10, sticky="n")
30
+ col += 1
31
+ if ri_count:
32
+ self.frame_ri.grid(row=0, column=col, padx=10, pady=10, sticky="n")
33
+ col += 1
34
+ if ro_count:
35
+ self.frame_ro.grid(row=0, column=col, padx=10, pady=10, sticky="n")
36
+
37
+ self.di_labels = []
38
+ for i, name in enumerate(di_names or []):
39
+ lbl = tk.Label(self.frame_di, text=name, bg="gray", fg="white", width=20)
40
+ lbl.grid(row=i, column=0, pady=2)
41
+ self.di_labels.append(lbl)
42
+
43
+ self.do_vars = []
44
+ for i, name in enumerate(do_names or []):
45
+ var = tk.IntVar()
46
+ chk = tk.Checkbutton(self.frame_do, text=name, variable=var,
47
+ command=lambda i=i, v=var: self.write_digital_output(i, v))
48
+ chk.grid(row=i, column=0, pady=2, sticky="w")
49
+ self.do_vars.append(var)
50
+
51
+ self.ri_labels = []
52
+ for i, name in enumerate(ri_names or []):
53
+ lbl = tk.Label(self.frame_ri, text=f"{name}: 0", font=("Arial", 10, "bold"), width=20, anchor="w")
54
+ lbl.grid(row=i, column=0, pady=2)
55
+ self.ri_labels.append(lbl)
56
+
57
+ self.ro_entries = []
58
+ for i, name in enumerate(ro_names or []):
59
+ frame = tk.Frame(self.frame_ro)
60
+ frame.grid(row=i, column=0, pady=2)
61
+ tk.Label(frame, text=f"{name}: ").pack(side=tk.LEFT)
62
+ entry = tk.Entry(frame, width=10)
63
+ entry.insert(0, "0")
64
+ entry.pack(side=tk.LEFT)
65
+ entry.bind("<Return>", lambda event, i=i, e=entry: self.write_register_output(i, e))
66
+ self.ro_entries.append(entry)
67
+
68
+ self.update_gui_from_server()
69
+
70
+ def update_gui_from_server(self):
71
+ di_data = self.server.get_digital_inputs()
72
+ for i, lbl in enumerate(self.di_labels):
73
+ if i < len(di_data):
74
+ color = "green" if di_data[i] else "gray"
75
+ lbl.config(bg=color)
76
+
77
+ ri_data = self.server.get_register_inputs()
78
+ for i, lbl in enumerate(self.ri_labels):
79
+ if i < len(ri_data):
80
+ lbl.config(text=f"{self.ri_labels[i].cget('text').split(':')[0]}: {ri_data[i]}")
81
+
82
+ self.master.after(50, self.update_gui_from_server)
83
+
84
+ def write_digital_output(self, index, var):
85
+ val = bool(var.get())
86
+ self.server.set_digital_output(index, val)
87
+
88
+ def write_register_output(self, index, entry):
89
+ try:
90
+ val = int(entry.get())
91
+ self.server.set_register_output(index, val)
92
+ except ValueError:
93
+ print("Please enter a valid integer.")
94
+
95
+ def on_closing(self):
96
+ self.server.stop()
97
+ self.master.destroy()
pyfactoryio/server.py ADDED
@@ -0,0 +1,145 @@
1
+ from pyModbusTCP.server import ModbusServer
2
+
3
+
4
+ class FactoryIOServer:
5
+ def __init__(self, host="127.0.0.1", port=5020, di_names=None, do_names=None, ri_names=None, ro_names=None):
6
+ self.host = host
7
+ self.port = port
8
+ self.server = ModbusServer(host=host, port=port, no_block=True)
9
+ self.di_offset = 0
10
+ self.do_offset = 16
11
+ self.ri_offset = 0
12
+ self.ro_offset = 8
13
+
14
+ self.di_names = di_names or []
15
+ self.do_names = do_names or []
16
+ self.ri_names = ri_names or []
17
+ self.ro_names = ro_names or []
18
+
19
+ self._di_map = {name: i for i, name in enumerate(self.di_names)}
20
+ self._do_map = {name: i for i, name in enumerate(self.do_names)}
21
+ self._ri_map = {name: i for i, name in enumerate(self.ri_names)}
22
+ self._ro_map = {name: i for i, name in enumerate(self.ro_names)}
23
+
24
+ self._callbacks = {}
25
+ self._running = False
26
+
27
+ def on_change(self, name):
28
+ """Decorator to register a callback when a variable changes state."""
29
+ def decorator(func):
30
+ if name not in self._callbacks:
31
+ self._callbacks[name] = []
32
+ self._callbacks[name].append(func)
33
+ return func
34
+ return decorator
35
+
36
+ def run_loop(self, logic_func=None, hz=10):
37
+ """Runs a continuous control loop, executing logic_func and monitoring events."""
38
+ import time
39
+ if not self.server.is_run:
40
+ self.start()
41
+
42
+ self._running = True
43
+ delay = 1.0 / hz
44
+
45
+ # Initialize previous states for all mapped variables
46
+ prev_state = {}
47
+ all_names = self.di_names + self.ri_names + self.do_names + self.ro_names
48
+ for name in all_names:
49
+ prev_state[name] = getattr(self, name)
50
+
51
+ print(f"Starting control loop at {hz}Hz. Press Ctrl+C to stop.")
52
+ try:
53
+ while self._running:
54
+ # 1. Detect changes and fire callbacks
55
+ for name in all_names:
56
+ current_val = getattr(self, name)
57
+ if current_val != prev_state[name]:
58
+ prev_state[name] = current_val
59
+ for cb in self._callbacks.get(name, []):
60
+ cb(current_val)
61
+
62
+ # 2. Run user logic
63
+ if logic_func:
64
+ logic_func(self)
65
+
66
+ time.sleep(delay)
67
+ except KeyboardInterrupt:
68
+ print("Loop interrupted by user.")
69
+ finally:
70
+ self._running = False
71
+ self.stop()
72
+
73
+ def __getattr__(self, name):
74
+ if name in self.__dict__.get('_di_map', {}):
75
+ index = self._di_map[name]
76
+ data = self.server.data_bank.get_coils(self.di_offset + index, 1)
77
+ return bool(data[0]) if data else False
78
+
79
+ if name in self.__dict__.get('_ri_map', {}):
80
+ index = self._ri_map[name]
81
+ data = self.server.data_bank.get_holding_registers(self.ri_offset + index, 1)
82
+ if data:
83
+ val = data[0]
84
+ return val - 65536 if val > 32767 else val
85
+ return 0
86
+
87
+ if name in self.__dict__.get('_do_map', {}):
88
+ index = self._do_map[name]
89
+ data = self.server.data_bank.get_discrete_inputs(self.do_offset + index, 1)
90
+ return bool(data[0]) if data else False
91
+
92
+ if name in self.__dict__.get('_ro_map', {}):
93
+ index = self._ro_map[name]
94
+ data = self.server.data_bank.get_input_registers(self.ro_offset + index, 1)
95
+ if data:
96
+ val = data[0]
97
+ return val - 65536 if val > 32767 else val
98
+ return 0
99
+
100
+ raise AttributeError(f"'FactoryIOServer' object has no attribute '{name}'")
101
+
102
+ def __setattr__(self, name, value):
103
+ if hasattr(self, '_do_map') and name in self._do_map:
104
+ index = self._do_map[name]
105
+ self.set_digital_output(index, value)
106
+ return
107
+
108
+ if hasattr(self, '_ro_map') and name in self._ro_map:
109
+ index = self._ro_map[name]
110
+ self.set_register_output(index, value)
111
+ return
112
+
113
+ if hasattr(self, '_di_map') and name in self._di_map:
114
+ raise AttributeError(f"Cannot set digital input '{name}', it is read-only (written by Factory I/O).")
115
+
116
+ if hasattr(self, '_ri_map') and name in self._ri_map:
117
+ raise AttributeError(f"Cannot set register input '{name}', it is read-only (written by Factory I/O).")
118
+
119
+ super().__setattr__(name, value)
120
+
121
+ def start(self):
122
+ self.server.start()
123
+ print(f"Modbus Server Started on {self.host}:{self.port}")
124
+
125
+ def stop(self):
126
+ self.server.stop()
127
+
128
+ def get_digital_inputs(self):
129
+ data = self.server.data_bank.get_coils(self.di_offset, 16)
130
+ return list(data) if data else [False] * 16
131
+
132
+ def get_register_inputs(self):
133
+ data = self.server.data_bank.get_holding_registers(self.ri_offset, 8)
134
+ if data:
135
+ return [val - 65536 if val > 32767 else val for val in data]
136
+ return [0] * 8
137
+
138
+ def set_digital_output(self, index, value):
139
+ self.server.data_bank.set_discrete_inputs(self.do_offset + index, [bool(value)])
140
+
141
+ def set_register_output(self, index, value):
142
+ if not (-32768 <= value <= 32767):
143
+ raise ValueError("Value must be between -32768 and 32767")
144
+ unsigned_val = value & 0xFFFF
145
+ self.server.data_bank.set_input_registers(self.ro_offset + index, [unsigned_val])
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyfactoryio
3
+ Version: 0.1.0
4
+ Summary: Factory IO Modbus Interface
5
+ Author: Developer
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pyModbusTCP
12
+
13
+ # PyFactoryIO
14
+
15
+ PyFactoryIO is a Python library that provides a high-level, modular Modbus interface for interacting with [Factory I/O](https://factoryio.com/). It allows you to easily connect your Python control logic to Factory I/O simulations, abstracting away the low-level Modbus TCP communication and mapping digital/register inputs and outputs by name.
16
+
17
+ ## Features
18
+
19
+ - **Easy Configuration:** Map Factory I/O sensors and actuators using descriptive names instead of Modbus addresses.
20
+ - **Event-Driven & Loop-Based Execution:** Provides decorators to run callbacks on state changes, or a continuous control loop for cyclic execution.
21
+ - **Auto-Mapping:** Automatically handles Modbus coils, discrete inputs, holding registers, and input registers.
22
+ - **Lightweight:** Built on top of `pyModbusTCP`.
23
+
24
+ ## Installation
25
+
26
+ You can install `pyfactoryio` directly via pip:
27
+
28
+ ```bash
29
+ pip install pyfactoryio
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### 1. Configure Factory I/O
35
+
36
+ Ensure Factory I/O is configured to use the **Modbus TCP Server** driver. By default, PyFactoryIO listens on `127.0.0.1` port `5020`.
37
+
38
+ ### 2. Basic Example
39
+
40
+ Here is a simple example demonstrating how to read a sensor and trigger an actuator.
41
+
42
+ ```python
43
+ from pyfactoryio import FactoryIOServer
44
+
45
+ # Define your input and output names in the order they appear in Factory I/O
46
+ DI_NAMES = ["Sensor_1", "Start_Button"]
47
+ DO_NAMES = ["Conveyor", "Warning_Light"]
48
+
49
+ # Initialize the server
50
+ server = FactoryIOServer(
51
+ di_names=DI_NAMES,
52
+ do_names=DO_NAMES
53
+ )
54
+
55
+ # Example 1: Event-driven callback
56
+ @server.on_change("Start_Button")
57
+ def on_start_pressed(value):
58
+ if value:
59
+ print("Start button pressed!")
60
+ server.Conveyor = True
61
+ else:
62
+ print("Start button released!")
63
+ server.Conveyor = False
64
+
65
+ # Example 2: Continuous control loop
66
+ def my_logic(srv):
67
+ # This function is called continuously by run_loop
68
+ if srv.Sensor_1:
69
+ srv.Warning_Light = True
70
+ else:
71
+ srv.Warning_Light = False
72
+
73
+ if __name__ == "__main__":
74
+ # Start the event loop (e.g., at 10Hz)
75
+ server.run_loop(logic_func=my_logic, hz=10)
76
+ ```
77
+
78
+ ## Supported Variable Types
79
+
80
+ - **di_names:** Digital Inputs (Sensors, Buttons) - Read from Factory I/O via Coils.
81
+ - **do_names:** Digital Outputs (Conveyors, Lights) - Written to Factory I/O via Discrete Inputs.
82
+ - **ri_names:** Register Inputs (Analog Sensors) - Read from Factory I/O via Holding Registers.
83
+ - **ro_names:** Register Outputs (Analog Actuators) - Written to Factory I/O via Input Registers.
84
+
85
+ ## License
86
+
87
+ This project is licensed under the MIT License.
@@ -0,0 +1,7 @@
1
+ pyfactoryio/__init__.py,sha256=7bSXSo4WPN_Vc7cTZZZvTAGck6eyvmz7QgzCgg73u0w,88
2
+ pyfactoryio/gui.py,sha256=t6yNThBlyqkff5764EiZED_bSBSIJNunIh-NmneCHec,4000
3
+ pyfactoryio/server.py,sha256=CoyZi4WNkR2xMCJ19-5WW_BzY7Y9eB-jSZjpAgnqR-w,5678
4
+ pyfactoryio-0.1.0.dist-info/METADATA,sha256=SHgtpHJ-HKH7ohX1MdkBb033oYbJMaOwfgjCzARocp4,2915
5
+ pyfactoryio-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ pyfactoryio-0.1.0.dist-info/top_level.txt,sha256=wHw13rUek7LEac6eHGzkNIwjclrUIpKhT7NuFSSNE6w,12
7
+ pyfactoryio-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyfactoryio