gravity-controller-operator 2.0.0__tar.gz
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.
- gravity_controller_operator-2.0.0/LICENSE +19 -0
- gravity_controller_operator-2.0.0/PKG-INFO +115 -0
- gravity_controller_operator-2.0.0/README.md +101 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/__init__.py +0 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controller_factory.py +26 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/__init__.py +0 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/arm_k210.py +47 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/emulator_contr.py +37 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/moxa.py +80 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/netping_relay.py +76 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/sigur.py +75 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/tests/__init__.py +0 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/tests/test_moxa_controller.py +31 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/wb_mr6lv.py +63 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/controllers_super.py +105 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/docs/__init__.py +0 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/exceptions.py +8 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/main.py +47 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/tests/__init__.py +0 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator/tests/main_tests.py +19 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/PKG-INFO +115 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/SOURCES.txt +25 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/dependency_links.txt +1 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/requires.txt +2 -0
- gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/top_level.txt +1 -0
- gravity_controller_operator-2.0.0/setup.cfg +4 -0
- gravity_controller_operator-2.0.0/setup.py +21 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gravity_controller_operator
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)
|
|
5
|
+
Author: PunchyArchy
|
|
6
|
+
Author-email: ksmdrmvscthny@gmail.com
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pyModbusTCP
|
|
13
|
+
Requires-Dist: pymodbus==3.6.9
|
|
14
|
+
|
|
15
|
+
# Gravity Controller Operator
|
|
16
|
+
|
|
17
|
+
Модуль для унифицированной работы с промышленными контроллерами (реле, дискретные входы и т.д.). Позволяет взаимодействовать с различными устройствами через единый интерфейс.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🚀 Поддерживаемые контроллеры
|
|
22
|
+
|
|
23
|
+
- **EmulatorController** — заглушка для тестов
|
|
24
|
+
- **ARMK210Controller** — Modbus TCP
|
|
25
|
+
- **WBMR6LV** — Modbus RTU
|
|
26
|
+
- **NetPing2Controller** — HTTP API
|
|
27
|
+
- **Sigur** — доступ через TCP (СКУД)
|
|
28
|
+
- **MoxaE1214** — REST API (vdn.dac.v1)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🧩 Архитектура
|
|
33
|
+
|
|
34
|
+
### 🧠 Интерфейсы:
|
|
35
|
+
|
|
36
|
+
- `DIInterface`: чтение дискретных входов
|
|
37
|
+
- `RelayInterface`: чтение/управление релейными выходами
|
|
38
|
+
- `ControllerInterface`: объединяет оба
|
|
39
|
+
|
|
40
|
+
Все интерфейсы наследуют `SoftStateMixin`, в котором реализовано хранение состояния точек (`state`, `changed`, `addr`).
|
|
41
|
+
|
|
42
|
+
### 🛠 Контроллер
|
|
43
|
+
Каждый контроллер реализует:
|
|
44
|
+
```python
|
|
45
|
+
self.interface = ControllerInterface(di_interface=..., relay_interface=...)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 🧪 Обёртки:
|
|
49
|
+
|
|
50
|
+
- `ControllerOperator` — отвечает за автоматическое обновление, mutex-безопасность и удобный API
|
|
51
|
+
- `ControllerCreator` — фабрика создания контроллеров по имени модели (см. `controller_factory.py`)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🧪 Как использовать
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from gravity_controller_operator.controllers.moxa import MoxaE1214
|
|
59
|
+
from gravity_controller_operator.controller_operator import ControllerOperator
|
|
60
|
+
|
|
61
|
+
moxa = MoxaE1214("192.168.60.103")
|
|
62
|
+
operator = ControllerOperator(moxa)
|
|
63
|
+
|
|
64
|
+
print(operator.get_points())
|
|
65
|
+
operator.change_relay_state(0, 1) # включить реле 0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🔬 Тестирование
|
|
71
|
+
|
|
72
|
+
Установка:
|
|
73
|
+
```bash
|
|
74
|
+
pip install pytest
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Запуск:
|
|
78
|
+
```bash
|
|
79
|
+
pytest gravity_controller_operator/tests/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 📦 Добавление нового контроллера
|
|
85
|
+
|
|
86
|
+
1. Реализуй `YourControllerDI` и `YourControllerRelay`, наследуя `DIInterface`, `RelayInterface`
|
|
87
|
+
2. Создай `YourController`, в котором:
|
|
88
|
+
```python
|
|
89
|
+
self.interface = ControllerInterface(di_interface=..., relay_interface=...)
|
|
90
|
+
```
|
|
91
|
+
3. Добавь контроллер в `AVAILABLE_CONTROLLERS` в `controller_factory.py`
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 📁 Структура
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
controllers_super.py # Базовые интерфейсы
|
|
99
|
+
controllers/ # Реализации контроллеров
|
|
100
|
+
controller_operator.py # Обёртка ControllerOperator ✅
|
|
101
|
+
controller_factory.py # AVAILABLE_CONTROLLERS + фабрика ✅
|
|
102
|
+
exceptions.py # Кастомные исключения ✅
|
|
103
|
+
tests/ # Тесты
|
|
104
|
+
README.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🤝 Контакты
|
|
110
|
+
|
|
111
|
+
- 📧 Email: ksmdrmvstchny@gmail.com
|
|
112
|
+
- 💬 Telegram: [@PunchyArchy](https://t.me/PunchyArchy)
|
|
113
|
+
- 📞 Phone: +7 996 294-75-95
|
|
114
|
+
|
|
115
|
+
Проект поддерживается Артуром (PunchyArchy, Qodex)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Gravity Controller Operator
|
|
2
|
+
|
|
3
|
+
Модуль для унифицированной работы с промышленными контроллерами (реле, дискретные входы и т.д.). Позволяет взаимодействовать с различными устройствами через единый интерфейс.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Поддерживаемые контроллеры
|
|
8
|
+
|
|
9
|
+
- **EmulatorController** — заглушка для тестов
|
|
10
|
+
- **ARMK210Controller** — Modbus TCP
|
|
11
|
+
- **WBMR6LV** — Modbus RTU
|
|
12
|
+
- **NetPing2Controller** — HTTP API
|
|
13
|
+
- **Sigur** — доступ через TCP (СКУД)
|
|
14
|
+
- **MoxaE1214** — REST API (vdn.dac.v1)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🧩 Архитектура
|
|
19
|
+
|
|
20
|
+
### 🧠 Интерфейсы:
|
|
21
|
+
|
|
22
|
+
- `DIInterface`: чтение дискретных входов
|
|
23
|
+
- `RelayInterface`: чтение/управление релейными выходами
|
|
24
|
+
- `ControllerInterface`: объединяет оба
|
|
25
|
+
|
|
26
|
+
Все интерфейсы наследуют `SoftStateMixin`, в котором реализовано хранение состояния точек (`state`, `changed`, `addr`).
|
|
27
|
+
|
|
28
|
+
### 🛠 Контроллер
|
|
29
|
+
Каждый контроллер реализует:
|
|
30
|
+
```python
|
|
31
|
+
self.interface = ControllerInterface(di_interface=..., relay_interface=...)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 🧪 Обёртки:
|
|
35
|
+
|
|
36
|
+
- `ControllerOperator` — отвечает за автоматическое обновление, mutex-безопасность и удобный API
|
|
37
|
+
- `ControllerCreator` — фабрика создания контроллеров по имени модели (см. `controller_factory.py`)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🧪 Как использовать
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from gravity_controller_operator.controllers.moxa import MoxaE1214
|
|
45
|
+
from gravity_controller_operator.controller_operator import ControllerOperator
|
|
46
|
+
|
|
47
|
+
moxa = MoxaE1214("192.168.60.103")
|
|
48
|
+
operator = ControllerOperator(moxa)
|
|
49
|
+
|
|
50
|
+
print(operator.get_points())
|
|
51
|
+
operator.change_relay_state(0, 1) # включить реле 0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🔬 Тестирование
|
|
57
|
+
|
|
58
|
+
Установка:
|
|
59
|
+
```bash
|
|
60
|
+
pip install pytest
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Запуск:
|
|
64
|
+
```bash
|
|
65
|
+
pytest gravity_controller_operator/tests/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 📦 Добавление нового контроллера
|
|
71
|
+
|
|
72
|
+
1. Реализуй `YourControllerDI` и `YourControllerRelay`, наследуя `DIInterface`, `RelayInterface`
|
|
73
|
+
2. Создай `YourController`, в котором:
|
|
74
|
+
```python
|
|
75
|
+
self.interface = ControllerInterface(di_interface=..., relay_interface=...)
|
|
76
|
+
```
|
|
77
|
+
3. Добавь контроллер в `AVAILABLE_CONTROLLERS` в `controller_factory.py`
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 📁 Структура
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
controllers_super.py # Базовые интерфейсы
|
|
85
|
+
controllers/ # Реализации контроллеров
|
|
86
|
+
controller_operator.py # Обёртка ControllerOperator ✅
|
|
87
|
+
controller_factory.py # AVAILABLE_CONTROLLERS + фабрика ✅
|
|
88
|
+
exceptions.py # Кастомные исключения ✅
|
|
89
|
+
tests/ # Тесты
|
|
90
|
+
README.md
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🤝 Контакты
|
|
96
|
+
|
|
97
|
+
- 📧 Email: ksmdrmvstchny@gmail.com
|
|
98
|
+
- 💬 Telegram: [@PunchyArchy](https://t.me/PunchyArchy)
|
|
99
|
+
- 📞 Phone: +7 996 294-75-95
|
|
100
|
+
|
|
101
|
+
Проект поддерживается Артуром (PunchyArchy, Qodex)
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers.emulator_contr import EmulatorController
|
|
2
|
+
from gravity_controller_operator.controllers.netping_relay import NetPing2Controller
|
|
3
|
+
from gravity_controller_operator.controllers.arm_k210 import ARMK210Controller
|
|
4
|
+
from gravity_controller_operator.controllers.wb_mr6lv import WBMR6LV
|
|
5
|
+
from gravity_controller_operator.exceptions import UnknownController
|
|
6
|
+
from gravity_controller_operator.controllers.sigur import Sigur
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
AVAILABLE_CONTROLLERS = [
|
|
10
|
+
ARMK210Controller,
|
|
11
|
+
WBMR6LV,
|
|
12
|
+
NetPing2Controller,
|
|
13
|
+
EmulatorController,
|
|
14
|
+
Sigur
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ControllerCreator:
|
|
19
|
+
@staticmethod
|
|
20
|
+
def get_controller(model, emulator=False, *args, **kwargs):
|
|
21
|
+
for contr in AVAILABLE_CONTROLLERS:
|
|
22
|
+
if emulator:
|
|
23
|
+
return EmulatorController(*args, **kwargs)
|
|
24
|
+
if contr.model.lower() == model.lower():
|
|
25
|
+
return contr(*args, **kwargs)
|
|
26
|
+
raise UnknownController(model, [contr.model for contr in AVAILABLE_CONTROLLERS])
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
2
|
+
from pyModbusTCP.client import ModbusClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ARMK210ControllerDI(DIInterface):
|
|
6
|
+
map_keys_amount = 8
|
|
7
|
+
starts_with = 0
|
|
8
|
+
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
self.client = client
|
|
11
|
+
super().__init__()
|
|
12
|
+
|
|
13
|
+
def get_phys_dict(self):
|
|
14
|
+
response = self.client.read_input_registers(self.starts_with, self.map_keys_amount)
|
|
15
|
+
while not response:
|
|
16
|
+
response = self.client.read_input_registers(self.starts_with, self.map_keys_amount)
|
|
17
|
+
return {i: val for i, val in enumerate(response)}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ARMK210ControllerRelay(RelayInterface):
|
|
21
|
+
map_keys_amount = 8
|
|
22
|
+
starts_with = 0
|
|
23
|
+
|
|
24
|
+
def __init__(self, client):
|
|
25
|
+
self.client = client
|
|
26
|
+
super().__init__()
|
|
27
|
+
|
|
28
|
+
def get_phys_dict(self):
|
|
29
|
+
response = self.client.read_holding_registers(self.starts_with, self.map_keys_amount)
|
|
30
|
+
while not response:
|
|
31
|
+
response = self.client.read_holding_registers(self.starts_with, self.map_keys_amount)
|
|
32
|
+
return {i: val for i, val in enumerate(response)}
|
|
33
|
+
|
|
34
|
+
def change_phys_relay_state(self, addr, state: bool):
|
|
35
|
+
result = self.client.write_single_coil(addr, state)
|
|
36
|
+
while not result:
|
|
37
|
+
result = self.client.write_single_coil(addr, state)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ARMK210Controller:
|
|
41
|
+
model = "arm_k210"
|
|
42
|
+
|
|
43
|
+
def __init__(self, ip: str, port: int = 8234, name="ARM_K210_Controller", *args, **kwargs):
|
|
44
|
+
client = ModbusClient(host=ip, port=port)
|
|
45
|
+
di = ARMK210ControllerDI(client)
|
|
46
|
+
relay = ARMK210ControllerRelay(client)
|
|
47
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers_super import DIInterface, \
|
|
2
|
+
RelayInterface, ControllerInterface
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EmulatorDI(DIInterface):
|
|
6
|
+
map_keys_amount = 4
|
|
7
|
+
starts_with = 1
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
def get_phys_dict(self):
|
|
13
|
+
return {1: 0, 2: 0, 3: 0, 4: 0}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EmulatorRelay(RelayInterface):
|
|
17
|
+
map_keys_amount = 4
|
|
18
|
+
starts_with = 1
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__()
|
|
22
|
+
|
|
23
|
+
def get_phys_dict(self):
|
|
24
|
+
return {1: 0, 2: 0, 3: 0, 4: 0}
|
|
25
|
+
|
|
26
|
+
def change_phys_relay_state(self, addr, state: bool):
|
|
27
|
+
# Ничего не делает, просто имитация
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EmulatorController:
|
|
32
|
+
model = "emulator_controller"
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
di = EmulatorDI()
|
|
36
|
+
relay = EmulatorRelay()
|
|
37
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MoxaClient:
|
|
6
|
+
def __init__(self, ip: str):
|
|
7
|
+
self.base_url = f"http://{ip}/api/slot/0/io"
|
|
8
|
+
self.headers = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
"Accept": "vdn.dac.v1"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def get_di(self):
|
|
14
|
+
url = f"{self.base_url}/di"
|
|
15
|
+
r = requests.get(url, headers=self.headers, timeout=2)
|
|
16
|
+
r.raise_for_status()
|
|
17
|
+
return r.json()["io"]["di"]
|
|
18
|
+
|
|
19
|
+
def get_relays(self):
|
|
20
|
+
url = f"{self.base_url}/relay"
|
|
21
|
+
r = requests.get(url, headers=self.headers, timeout=2)
|
|
22
|
+
r.raise_for_status()
|
|
23
|
+
return r.json()["io"]["relay"]
|
|
24
|
+
|
|
25
|
+
def set_relay(self, channel: int, state: int):
|
|
26
|
+
url = f"{self.base_url}/relay/{channel}/relayStatus"
|
|
27
|
+
payload = {
|
|
28
|
+
"slot": "0",
|
|
29
|
+
"io": {
|
|
30
|
+
"relay": {
|
|
31
|
+
str(channel): {
|
|
32
|
+
"relayStatus": str(state)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
headers = self.headers.copy()
|
|
38
|
+
headers["Content-Length"] = str(len(str(payload)))
|
|
39
|
+
r = requests.put(url, headers=headers, json=payload, timeout=2)
|
|
40
|
+
r.raise_for_status()
|
|
41
|
+
return r.status_code == 200
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MoxaDI(DIInterface):
|
|
45
|
+
map_keys_amount = 16
|
|
46
|
+
starts_with = 0
|
|
47
|
+
|
|
48
|
+
def __init__(self, client):
|
|
49
|
+
self.client = client
|
|
50
|
+
super().__init__()
|
|
51
|
+
|
|
52
|
+
def get_phys_dict(self):
|
|
53
|
+
data = self.client.get_di()
|
|
54
|
+
return {int(item["diIndex"]): item["diStatus"] for item in data}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MoxaRelay(RelayInterface):
|
|
58
|
+
map_keys_amount = 4
|
|
59
|
+
starts_with = 0
|
|
60
|
+
|
|
61
|
+
def __init__(self, client):
|
|
62
|
+
self.client = client
|
|
63
|
+
super().__init__()
|
|
64
|
+
|
|
65
|
+
def get_phys_dict(self):
|
|
66
|
+
data = self.client.get_relays()
|
|
67
|
+
return {int(item["relayIndex"]): int(item["relayStatus"]) for item in data}
|
|
68
|
+
|
|
69
|
+
def change_phys_relay_state(self, addr, state: bool):
|
|
70
|
+
return self.client.set_relay(addr, int(state))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MoxaE1214:
|
|
74
|
+
model = "moxa_e1214"
|
|
75
|
+
|
|
76
|
+
def __init__(self, ip):
|
|
77
|
+
client = MoxaClient(ip)
|
|
78
|
+
di = MoxaDI(client)
|
|
79
|
+
relay = MoxaRelay(client)
|
|
80
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
2
|
+
from netping_contr import mixins
|
|
3
|
+
import requests
|
|
4
|
+
from requests.auth import HTTPBasicAuth
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NetPingDevice(mixins.NetPingResponseParser):
|
|
8
|
+
def __init__(self, ip, port=80, username="visor", password="ping"):
|
|
9
|
+
self.ip = ip
|
|
10
|
+
self.port = port
|
|
11
|
+
self.username = username
|
|
12
|
+
self.password = password
|
|
13
|
+
self.schema = "http://"
|
|
14
|
+
|
|
15
|
+
def get_full_url(self):
|
|
16
|
+
return f"{self.schema}{self.ip}:{self.port}"
|
|
17
|
+
|
|
18
|
+
def get_all_di_status(self):
|
|
19
|
+
return requests.get(
|
|
20
|
+
url=f"{self.get_full_url()}/io.cgi?io",
|
|
21
|
+
auth=HTTPBasicAuth(self.username, self.password))
|
|
22
|
+
|
|
23
|
+
def get_all_relay_states(self):
|
|
24
|
+
states = {}
|
|
25
|
+
for i in range(1, 5):
|
|
26
|
+
r = requests.get(
|
|
27
|
+
url=f"{self.get_full_url()}/relay.cgi?r{i}",
|
|
28
|
+
auth=HTTPBasicAuth(self.username, self.password))
|
|
29
|
+
states[i] = self.parse_relay_state(r)
|
|
30
|
+
return states
|
|
31
|
+
|
|
32
|
+
def change_relay_status(self, relay_num, state):
|
|
33
|
+
return requests.get(
|
|
34
|
+
url=f"{self.get_full_url()}/relay.cgi?r{relay_num}={state}",
|
|
35
|
+
auth=HTTPBasicAuth(self.username, self.password))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NetPingDI(DIInterface):
|
|
39
|
+
map_keys_amount = 4
|
|
40
|
+
starts_with = 1
|
|
41
|
+
|
|
42
|
+
def __init__(self, controller):
|
|
43
|
+
self.controller = controller
|
|
44
|
+
super().__init__()
|
|
45
|
+
|
|
46
|
+
def get_phys_dict(self):
|
|
47
|
+
raw = self.controller.get_all_di_status()
|
|
48
|
+
return self.controller.parse_all_lines_request(raw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NetPingRelay(RelayInterface):
|
|
52
|
+
map_keys_amount = 4
|
|
53
|
+
starts_with = 1
|
|
54
|
+
|
|
55
|
+
def __init__(self, controller):
|
|
56
|
+
self.controller = controller
|
|
57
|
+
super().__init__()
|
|
58
|
+
|
|
59
|
+
def get_phys_dict(self):
|
|
60
|
+
return self.controller.get_all_relay_states()
|
|
61
|
+
|
|
62
|
+
def change_phys_relay_state(self, addr, state: bool):
|
|
63
|
+
result = self.controller.change_relay_status(addr, state)
|
|
64
|
+
while "error" in result:
|
|
65
|
+
result = self.controller.change_relay_status(addr, state)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NetPing2Controller:
|
|
69
|
+
model = "netping_relay"
|
|
70
|
+
|
|
71
|
+
def __init__(self, ip, port=80, username="visor", password="ping",
|
|
72
|
+
name="netping_relay2", *args, **kwargs):
|
|
73
|
+
device = NetPingDevice(ip=ip, port=port, username=username, password=password)
|
|
74
|
+
di = NetPingDI(device)
|
|
75
|
+
relay = NetPingRelay(device)
|
|
76
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
2
|
+
from _thread import allocate_lock
|
|
3
|
+
from socket import socket
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SigurBase:
|
|
7
|
+
def __init__(self, sock):
|
|
8
|
+
self.sock = sock
|
|
9
|
+
self.mutex = allocate_lock()
|
|
10
|
+
self.login()
|
|
11
|
+
|
|
12
|
+
def login(self, login="Administrator", password=""):
|
|
13
|
+
return self._send_command(f'"LOGIN" 1.8 "{login}" "{password}"')
|
|
14
|
+
|
|
15
|
+
def _send_command(self, command, get_response=True):
|
|
16
|
+
self.mutex.acquire()
|
|
17
|
+
command = f'{command}\r\n'
|
|
18
|
+
self.sock.send(command.encode("utf-8"))
|
|
19
|
+
if get_response:
|
|
20
|
+
response = self.sock.recv(1024)
|
|
21
|
+
self.mutex.release()
|
|
22
|
+
return response
|
|
23
|
+
self.mutex.release()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SigurDI(DIInterface, SigurBase):
|
|
27
|
+
map_keys_amount = 5
|
|
28
|
+
starts_with = 3
|
|
29
|
+
|
|
30
|
+
def __init__(self, sock):
|
|
31
|
+
SigurBase.__init__(self, sock)
|
|
32
|
+
DIInterface.__init__(self)
|
|
33
|
+
|
|
34
|
+
def get_phys_dict(self):
|
|
35
|
+
result = {}
|
|
36
|
+
for point in range(self.starts_with, self.starts_with + self.map_keys_amount):
|
|
37
|
+
raw = self._send_command(f'"GETAPINFO" {point}')
|
|
38
|
+
response = raw.decode()
|
|
39
|
+
status = response.split(" ")[-2]
|
|
40
|
+
result[point] = 0 if status in ("ONLINE_UNLOCKED", "ONLINE_NORMAL") else 1
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SigurRelay(RelayInterface, SigurBase):
|
|
45
|
+
map_keys_amount = 3
|
|
46
|
+
starts_with = 1
|
|
47
|
+
|
|
48
|
+
def __init__(self, sock):
|
|
49
|
+
SigurBase.__init__(self, sock)
|
|
50
|
+
RelayInterface.__init__(self)
|
|
51
|
+
|
|
52
|
+
def get_phys_dict(self):
|
|
53
|
+
result = {}
|
|
54
|
+
for point in range(self.starts_with, self.starts_with + self.map_keys_amount):
|
|
55
|
+
raw = self._send_command(f'"GETAPINFO" {point}')
|
|
56
|
+
response = raw.decode()
|
|
57
|
+
status = response.split(" ")[-2]
|
|
58
|
+
result[point] = 0 if status in ("ONLINE_UNLOCKED", "ONLINE_NORMAL") else 1
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def change_phys_relay_state(self, addr, state: bool):
|
|
62
|
+
# Имитация действия, при необходимости можно добавить настоящую команду
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Sigur:
|
|
67
|
+
model = "sigur"
|
|
68
|
+
|
|
69
|
+
def __init__(self, ip, port=3312, login="Administrator", password="",
|
|
70
|
+
name="Sigur", *args, **kwargs):
|
|
71
|
+
sock = socket()
|
|
72
|
+
sock.connect((ip, port))
|
|
73
|
+
di = SigurDI(sock)
|
|
74
|
+
relay = SigurRelay(sock)
|
|
75
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from gravity_controller_operator.main import ControllerOperator
|
|
3
|
+
from gravity_controller_operator.controllers.moxa import MoxaE1214
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture(scope="module")
|
|
7
|
+
def moxa_operator():
|
|
8
|
+
controller = MoxaE1214("192.168.60.103")
|
|
9
|
+
return ControllerOperator(controller, auto_update_points=False)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_di_states(moxa_operator):
|
|
13
|
+
di = moxa_operator.get_points()["di"]
|
|
14
|
+
assert isinstance(di, dict)
|
|
15
|
+
assert all(isinstance(v["state"], (int, type(None))) for v in di.values())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_relay_states(moxa_operator):
|
|
19
|
+
relays = moxa_operator.get_points()["relays"]
|
|
20
|
+
assert isinstance(relays, dict)
|
|
21
|
+
assert all(isinstance(v["state"], (int, type(None))) for v in relays.values())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_relay_toggle(moxa_operator):
|
|
25
|
+
moxa_operator.change_relay_state(0, 0)
|
|
26
|
+
moxa_operator.update_points()
|
|
27
|
+
assert moxa_operator.get_points()["relays"][0]["state"] == 0
|
|
28
|
+
|
|
29
|
+
moxa_operator.change_relay_state(0, 1)
|
|
30
|
+
moxa_operator.update_points()
|
|
31
|
+
assert moxa_operator.get_points()["relays"][0]["state"] == 1
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
2
|
+
from pymodbus.client import ModbusSerialClient
|
|
3
|
+
from pymodbus import Framer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WBMR6LVDI(DIInterface):
|
|
7
|
+
map_keys_amount = 8
|
|
8
|
+
starts_with = 0
|
|
9
|
+
spec_addr = {0: 7, 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5}
|
|
10
|
+
|
|
11
|
+
def __init__(self, client, slave_id):
|
|
12
|
+
self.client = client
|
|
13
|
+
self.slave_id = slave_id
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
def get_phys_dict(self):
|
|
17
|
+
response = self.client.read_discrete_inputs(
|
|
18
|
+
self.starts_with, self.map_keys_amount, slave=self.slave_id)
|
|
19
|
+
while not response or response.isError():
|
|
20
|
+
response = self.client.read_discrete_inputs(
|
|
21
|
+
self.starts_with, self.map_keys_amount, slave=self.slave_id)
|
|
22
|
+
return {i: bit for i, bit in enumerate(response.bits)}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WBMR6LVRelay(RelayInterface):
|
|
26
|
+
map_keys_amount = 6
|
|
27
|
+
starts_with = 0
|
|
28
|
+
spec_addr = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5}
|
|
29
|
+
|
|
30
|
+
def __init__(self, client, slave_id):
|
|
31
|
+
self.client = client
|
|
32
|
+
self.slave_id = slave_id
|
|
33
|
+
super().__init__()
|
|
34
|
+
|
|
35
|
+
def get_phys_dict(self):
|
|
36
|
+
response = self.client.read_coils(
|
|
37
|
+
self.starts_with, self.map_keys_amount, slave=self.slave_id)
|
|
38
|
+
while not response or response.isError():
|
|
39
|
+
response = self.client.read_coils(
|
|
40
|
+
self.starts_with, self.map_keys_amount, slave=self.slave_id)
|
|
41
|
+
return {i: bit for i, bit in enumerate(response.bits)}
|
|
42
|
+
|
|
43
|
+
def change_phys_relay_state(self, addr, state: bool):
|
|
44
|
+
result = self.client.write_coil(addr, state, slave=self.slave_id)
|
|
45
|
+
while not result or result.isError():
|
|
46
|
+
result = self.client.write_coil(addr, state, slave=self.slave_id)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WBMR6LV:
|
|
50
|
+
model = "wb_mr6lv"
|
|
51
|
+
|
|
52
|
+
def __init__(self, device, slave_id, baudrate=9600, stopbits=2, bytesize=8,
|
|
53
|
+
name="WBMR6LV", *args, **kwargs):
|
|
54
|
+
client = ModbusSerialClient(
|
|
55
|
+
device,
|
|
56
|
+
framer=Framer.RTU,
|
|
57
|
+
baudrate=baudrate,
|
|
58
|
+
stopbits=stopbits,
|
|
59
|
+
bytesize=bytesize,
|
|
60
|
+
)
|
|
61
|
+
di = WBMR6LVDI(client, slave_id)
|
|
62
|
+
relay = WBMR6LVRelay(client, slave_id)
|
|
63
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SoftStateMixin:
|
|
6
|
+
"""
|
|
7
|
+
Хранит программное состояние входов/выходов.
|
|
8
|
+
Позволяет обновлять состояния и получать их по ключу.
|
|
9
|
+
"""
|
|
10
|
+
spec_addr = {} # переназначение адресов (если физический != логическому)
|
|
11
|
+
map_keys_amount = 0
|
|
12
|
+
starts_with = 0
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.state = self._init_state()
|
|
16
|
+
|
|
17
|
+
def _init_state(self):
|
|
18
|
+
points = {}
|
|
19
|
+
for i in range(self.map_keys_amount):
|
|
20
|
+
logical_ch = self.starts_with + i
|
|
21
|
+
addr = self.spec_addr.get(logical_ch, logical_ch)
|
|
22
|
+
points[logical_ch] = {"state": None, "changed": None, "addr": addr}
|
|
23
|
+
return points
|
|
24
|
+
|
|
25
|
+
def update_state(self, addr, value, mark_time=True):
|
|
26
|
+
now = datetime.datetime.now() if mark_time else None
|
|
27
|
+
for logical_ch, info in self.state.items():
|
|
28
|
+
if info["addr"] == addr:
|
|
29
|
+
info["state"] = value
|
|
30
|
+
info["changed"] = now
|
|
31
|
+
|
|
32
|
+
def get_state(self):
|
|
33
|
+
return self.state
|
|
34
|
+
|
|
35
|
+
def get_point(self, num):
|
|
36
|
+
return self.state.get(num, {"error": f"channel {num} not found"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BasePhysInterface(ABC):
|
|
40
|
+
"""
|
|
41
|
+
Абстрактный интерфейс работы с железом.
|
|
42
|
+
Потомки реализуют конкретную работу с DI или Relay.
|
|
43
|
+
"""
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_phys_dict(self) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Вернуть словарь {канал: значение} с физического устройства.
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RelayPhysInterface(BasePhysInterface):
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def change_phys_relay_state(self, addr: int, state: bool):
|
|
55
|
+
"""
|
|
56
|
+
Изменить состояние реле на физическом устройстве.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
class DIInterface(SoftStateMixin, BasePhysInterface):
|
|
61
|
+
def __init__(self):
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.update_from_device()
|
|
64
|
+
|
|
65
|
+
def update_from_device(self):
|
|
66
|
+
values = self.get_phys_dict()
|
|
67
|
+
for addr, value in values.items():
|
|
68
|
+
self.update_state(addr, value, mark_time=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RelayInterface(SoftStateMixin, RelayPhysInterface):
|
|
72
|
+
def __init__(self):
|
|
73
|
+
super().__init__()
|
|
74
|
+
self.update_from_device()
|
|
75
|
+
|
|
76
|
+
def update_from_device(self):
|
|
77
|
+
values = self.get_phys_dict()
|
|
78
|
+
for addr, value in values.items():
|
|
79
|
+
self.update_state(addr, value, mark_time=False)
|
|
80
|
+
|
|
81
|
+
def change_relay_state(self, logical_ch: int, state: bool):
|
|
82
|
+
self.update_state(logical_ch, state)
|
|
83
|
+
addr = self.state[logical_ch]["addr"]
|
|
84
|
+
return self.change_phys_relay_state(addr, state)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ControllerInterface:
|
|
88
|
+
"""
|
|
89
|
+
Комбинирует доступ к DI и Relay интерфейсам.
|
|
90
|
+
"""
|
|
91
|
+
def __init__(self, di_interface=None, relay_interface=None):
|
|
92
|
+
self.di_interface = di_interface
|
|
93
|
+
self.relay_interface = relay_interface
|
|
94
|
+
|
|
95
|
+
def update_all(self):
|
|
96
|
+
if self.di_interface:
|
|
97
|
+
self.di_interface.update_from_device()
|
|
98
|
+
if self.relay_interface:
|
|
99
|
+
self.relay_interface.update_from_device()
|
|
100
|
+
|
|
101
|
+
def get_all_states(self):
|
|
102
|
+
return {
|
|
103
|
+
"di": self.di_interface.get_state() if self.di_interface else {},
|
|
104
|
+
"relays": self.relay_interface.get_state() if self.relay_interface else {}
|
|
105
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class UnknownController(Exception):
|
|
2
|
+
# Исключение, возникающее при неизвестном имени терминала
|
|
3
|
+
def __init__(self, contr_name=None, contr_list=[]):
|
|
4
|
+
text = f'Контроллер {contr_name} не обнаружен! Создайте класс с контроллером ' \
|
|
5
|
+
'в директории controllers, укажите его модель через атрибут ' \
|
|
6
|
+
'model, затем добавьте этот класс в список ' \
|
|
7
|
+
f'AVAILABLE_CONTROLLERS {tuple(contr_list)}'
|
|
8
|
+
super().__init__(text)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from threading import Lock
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ControllerOperator:
|
|
7
|
+
def __init__(self, controller, auto_update_points=True, update_cooldown=0.25):
|
|
8
|
+
self.controller = controller
|
|
9
|
+
self.interface = controller.interface
|
|
10
|
+
self.mutex = Lock()
|
|
11
|
+
self.update_cooldown = update_cooldown
|
|
12
|
+
self.auto_update_points_enabled = auto_update_points
|
|
13
|
+
|
|
14
|
+
if auto_update_points:
|
|
15
|
+
threading.Thread(target=self._auto_update_loop, daemon=True).start()
|
|
16
|
+
|
|
17
|
+
def _auto_update_loop(self):
|
|
18
|
+
while self.auto_update_points_enabled:
|
|
19
|
+
self.update_points()
|
|
20
|
+
time.sleep(self.update_cooldown)
|
|
21
|
+
|
|
22
|
+
def update_points(self):
|
|
23
|
+
with self.mutex:
|
|
24
|
+
self.interface.update_all()
|
|
25
|
+
|
|
26
|
+
def get_points(self):
|
|
27
|
+
with self.mutex:
|
|
28
|
+
return self.interface.get_all_states()
|
|
29
|
+
|
|
30
|
+
def change_relay_state(self, ch: int, value: int):
|
|
31
|
+
with self.mutex:
|
|
32
|
+
return self.interface.relay_interface.change_relay_state(ch, value)
|
|
33
|
+
|
|
34
|
+
def get_point(self, typ, ch):
|
|
35
|
+
if typ == "di":
|
|
36
|
+
return self.interface.di_interface.get_point(ch)
|
|
37
|
+
elif typ == "relays":
|
|
38
|
+
return self.interface.relay_interface.get_point(ch)
|
|
39
|
+
|
|
40
|
+
def get_di_state(self, ch):
|
|
41
|
+
return self.get_point("di", ch)
|
|
42
|
+
|
|
43
|
+
def get_relay_state(self, ch):
|
|
44
|
+
return self.get_point("relays", ch)
|
|
45
|
+
|
|
46
|
+
def get_model(self):
|
|
47
|
+
return self.controller.model
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from gravity_controller_operator.main import ControllerOperator, \
|
|
3
|
+
ControllerCreater
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestCase(unittest.TestCase):
|
|
7
|
+
def test_operator(self):
|
|
8
|
+
inst = ControllerCreater.get_controller(
|
|
9
|
+
"wb_mr6lv12",False)
|
|
10
|
+
co = ControllerOperator(inst)
|
|
11
|
+
print(co.get_points())
|
|
12
|
+
# inst = ControllerOperator("localhost", 8234, "arm_k210",
|
|
13
|
+
# auto_update_points=True)
|
|
14
|
+
# while True:
|
|
15
|
+
# print(inst.get_points())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
unittest.main()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: gravity_controller_operator
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)
|
|
5
|
+
Author: PunchyArchy
|
|
6
|
+
Author-email: ksmdrmvscthny@gmail.com
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pyModbusTCP
|
|
13
|
+
Requires-Dist: pymodbus==3.6.9
|
|
14
|
+
|
|
15
|
+
# Gravity Controller Operator
|
|
16
|
+
|
|
17
|
+
Модуль для унифицированной работы с промышленными контроллерами (реле, дискретные входы и т.д.). Позволяет взаимодействовать с различными устройствами через единый интерфейс.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🚀 Поддерживаемые контроллеры
|
|
22
|
+
|
|
23
|
+
- **EmulatorController** — заглушка для тестов
|
|
24
|
+
- **ARMK210Controller** — Modbus TCP
|
|
25
|
+
- **WBMR6LV** — Modbus RTU
|
|
26
|
+
- **NetPing2Controller** — HTTP API
|
|
27
|
+
- **Sigur** — доступ через TCP (СКУД)
|
|
28
|
+
- **MoxaE1214** — REST API (vdn.dac.v1)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🧩 Архитектура
|
|
33
|
+
|
|
34
|
+
### 🧠 Интерфейсы:
|
|
35
|
+
|
|
36
|
+
- `DIInterface`: чтение дискретных входов
|
|
37
|
+
- `RelayInterface`: чтение/управление релейными выходами
|
|
38
|
+
- `ControllerInterface`: объединяет оба
|
|
39
|
+
|
|
40
|
+
Все интерфейсы наследуют `SoftStateMixin`, в котором реализовано хранение состояния точек (`state`, `changed`, `addr`).
|
|
41
|
+
|
|
42
|
+
### 🛠 Контроллер
|
|
43
|
+
Каждый контроллер реализует:
|
|
44
|
+
```python
|
|
45
|
+
self.interface = ControllerInterface(di_interface=..., relay_interface=...)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 🧪 Обёртки:
|
|
49
|
+
|
|
50
|
+
- `ControllerOperator` — отвечает за автоматическое обновление, mutex-безопасность и удобный API
|
|
51
|
+
- `ControllerCreator` — фабрика создания контроллеров по имени модели (см. `controller_factory.py`)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🧪 Как использовать
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from gravity_controller_operator.controllers.moxa import MoxaE1214
|
|
59
|
+
from gravity_controller_operator.controller_operator import ControllerOperator
|
|
60
|
+
|
|
61
|
+
moxa = MoxaE1214("192.168.60.103")
|
|
62
|
+
operator = ControllerOperator(moxa)
|
|
63
|
+
|
|
64
|
+
print(operator.get_points())
|
|
65
|
+
operator.change_relay_state(0, 1) # включить реле 0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🔬 Тестирование
|
|
71
|
+
|
|
72
|
+
Установка:
|
|
73
|
+
```bash
|
|
74
|
+
pip install pytest
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Запуск:
|
|
78
|
+
```bash
|
|
79
|
+
pytest gravity_controller_operator/tests/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 📦 Добавление нового контроллера
|
|
85
|
+
|
|
86
|
+
1. Реализуй `YourControllerDI` и `YourControllerRelay`, наследуя `DIInterface`, `RelayInterface`
|
|
87
|
+
2. Создай `YourController`, в котором:
|
|
88
|
+
```python
|
|
89
|
+
self.interface = ControllerInterface(di_interface=..., relay_interface=...)
|
|
90
|
+
```
|
|
91
|
+
3. Добавь контроллер в `AVAILABLE_CONTROLLERS` в `controller_factory.py`
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 📁 Структура
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
controllers_super.py # Базовые интерфейсы
|
|
99
|
+
controllers/ # Реализации контроллеров
|
|
100
|
+
controller_operator.py # Обёртка ControllerOperator ✅
|
|
101
|
+
controller_factory.py # AVAILABLE_CONTROLLERS + фабрика ✅
|
|
102
|
+
exceptions.py # Кастомные исключения ✅
|
|
103
|
+
tests/ # Тесты
|
|
104
|
+
README.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🤝 Контакты
|
|
110
|
+
|
|
111
|
+
- 📧 Email: ksmdrmvstchny@gmail.com
|
|
112
|
+
- 💬 Telegram: [@PunchyArchy](https://t.me/PunchyArchy)
|
|
113
|
+
- 📞 Phone: +7 996 294-75-95
|
|
114
|
+
|
|
115
|
+
Проект поддерживается Артуром (PunchyArchy, Qodex)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
gravity_controller_operator/__init__.py
|
|
5
|
+
gravity_controller_operator/controller_factory.py
|
|
6
|
+
gravity_controller_operator/controllers_super.py
|
|
7
|
+
gravity_controller_operator/exceptions.py
|
|
8
|
+
gravity_controller_operator/main.py
|
|
9
|
+
gravity_controller_operator.egg-info/PKG-INFO
|
|
10
|
+
gravity_controller_operator.egg-info/SOURCES.txt
|
|
11
|
+
gravity_controller_operator.egg-info/dependency_links.txt
|
|
12
|
+
gravity_controller_operator.egg-info/requires.txt
|
|
13
|
+
gravity_controller_operator.egg-info/top_level.txt
|
|
14
|
+
gravity_controller_operator/controllers/__init__.py
|
|
15
|
+
gravity_controller_operator/controllers/arm_k210.py
|
|
16
|
+
gravity_controller_operator/controllers/emulator_contr.py
|
|
17
|
+
gravity_controller_operator/controllers/moxa.py
|
|
18
|
+
gravity_controller_operator/controllers/netping_relay.py
|
|
19
|
+
gravity_controller_operator/controllers/sigur.py
|
|
20
|
+
gravity_controller_operator/controllers/wb_mr6lv.py
|
|
21
|
+
gravity_controller_operator/controllers/tests/__init__.py
|
|
22
|
+
gravity_controller_operator/controllers/tests/test_moxa_controller.py
|
|
23
|
+
gravity_controller_operator/docs/__init__.py
|
|
24
|
+
gravity_controller_operator/tests/__init__.py
|
|
25
|
+
gravity_controller_operator/tests/main_tests.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gravity_controller_operator
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="gravity_controller_operator",
|
|
5
|
+
version="2.0.0",
|
|
6
|
+
description="SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)",
|
|
7
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="PunchyArchy",
|
|
10
|
+
author_email="ksmdrmvscthny@gmail.com",
|
|
11
|
+
packages=find_packages(),
|
|
12
|
+
python_requires=">=3.7",
|
|
13
|
+
install_requires=[
|
|
14
|
+
"pyModbusTCP",
|
|
15
|
+
"pymodbus==3.6.9",
|
|
16
|
+
],
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
],
|
|
21
|
+
)
|