gravity-controller-operator 2.3.0__tar.gz → 2.4.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.3.0 → gravity_controller_operator-2.4.0}/LICENSE +1 -1
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/PKG-INFO +1 -1
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controller_factory.py +4 -5
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/arm_k210.py +5 -2
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/emulator_contr.py +2 -3
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/moxa.py +4 -15
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/netping_relay.py +9 -10
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/sigur.py +3 -6
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/tests/test_moxa_controller.py +2 -1
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/tests/test_wb_mr6lv_tcp.py +9 -17
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/wb_mr6lv.py +80 -19
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers_super.py +7 -3
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/diagnostics_cli.py +14 -26
- gravity_controller_operator-2.4.0/gravity_controller_operator/exceptions.py +10 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/main.py +1 -1
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/tests/main_tests.py +3 -4
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/PKG-INFO +1 -1
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/setup.py +2 -2
- gravity_controller_operator-2.3.0/gravity_controller_operator/exceptions.py +0 -8
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/README.md +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/__init__.py +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/__init__.py +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/tests/__init__.py +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/docs/__init__.py +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/tests/__init__.py +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/SOURCES.txt +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/dependency_links.txt +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/requires.txt +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/top_level.txt +0 -0
- {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/setup.cfg +0 -0
|
@@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
16
16
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
17
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
18
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
-
SOFTWARE.
|
|
19
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gravity_controller_operator
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)
|
|
5
5
|
Author: PunchyArchy
|
|
6
6
|
Author-email: ksmdrmvscthny@gmail.com
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
from gravity_controller_operator.controllers.arm_k210 import ARMK210Controller
|
|
1
2
|
from gravity_controller_operator.controllers.emulator_contr import EmulatorController
|
|
3
|
+
from gravity_controller_operator.controllers.moxa import MoxaE1214
|
|
2
4
|
from gravity_controller_operator.controllers.netping_relay import NetPing2Controller
|
|
3
|
-
from gravity_controller_operator.controllers.
|
|
5
|
+
from gravity_controller_operator.controllers.sigur import Sigur
|
|
4
6
|
from gravity_controller_operator.controllers.wb_mr6lv import WBMR6LV, WBMR6LVTCP
|
|
5
7
|
from gravity_controller_operator.exceptions import UnknownController
|
|
6
|
-
from gravity_controller_operator.controllers.moxa import MoxaE1214
|
|
7
|
-
from gravity_controller_operator.controllers.sigur import Sigur
|
|
8
|
-
|
|
9
8
|
|
|
10
9
|
AVAILABLE_CONTROLLERS = [
|
|
11
10
|
ARMK210Controller,
|
|
@@ -14,7 +13,7 @@ AVAILABLE_CONTROLLERS = [
|
|
|
14
13
|
NetPing2Controller,
|
|
15
14
|
EmulatorController,
|
|
16
15
|
Sigur,
|
|
17
|
-
MoxaE1214
|
|
16
|
+
MoxaE1214,
|
|
18
17
|
]
|
|
19
18
|
|
|
20
19
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
2
|
-
from pyModbusTCP.client import ModbusClient
|
|
3
1
|
import time
|
|
4
2
|
|
|
3
|
+
from pyModbusTCP.client import ModbusClient
|
|
4
|
+
|
|
5
|
+
from gravity_controller_operator.controllers_super import ControllerInterface, DIInterface, RelayInterface
|
|
6
|
+
|
|
7
|
+
|
|
5
8
|
class ARMK210ControllerDI(DIInterface):
|
|
6
9
|
map_keys_amount = 8
|
|
7
10
|
starts_with = 0
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from gravity_controller_operator.controllers_super import DIInterface,
|
|
2
|
-
RelayInterface, ControllerInterface
|
|
1
|
+
from gravity_controller_operator.controllers_super import ControllerInterface, DIInterface, RelayInterface
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
class EmulatorDI(DIInterface):
|
|
@@ -34,4 +33,4 @@ class EmulatorController:
|
|
|
34
33
|
def __init__(self, *args, **kwargs):
|
|
35
34
|
di = EmulatorDI()
|
|
36
35
|
relay = EmulatorRelay()
|
|
37
|
-
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
36
|
+
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import requests
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from gravity_controller_operator.controllers_super import ControllerInterface, DIInterface, RelayInterface
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class MoxaClient:
|
|
6
7
|
def __init__(self, ip: str):
|
|
7
8
|
self.base_url = f"http://{ip}/api/slot/0/io"
|
|
8
|
-
self.headers = {
|
|
9
|
-
"Content-Type": "application/json",
|
|
10
|
-
"Accept": "vdn.dac.v1"
|
|
11
|
-
}
|
|
9
|
+
self.headers = {"Content-Type": "application/json", "Accept": "vdn.dac.v1"}
|
|
12
10
|
|
|
13
11
|
def get_di(self):
|
|
14
12
|
url = f"{self.base_url}/di"
|
|
@@ -24,16 +22,7 @@ class MoxaClient:
|
|
|
24
22
|
|
|
25
23
|
def set_relay(self, channel: int, state: int):
|
|
26
24
|
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
|
-
}
|
|
25
|
+
payload = {"slot": "0", "io": {"relay": {str(channel): {"relayStatus": str(state)}}}}
|
|
37
26
|
headers = self.headers.copy()
|
|
38
27
|
headers["Content-Length"] = str(len(str(payload)))
|
|
39
28
|
r = requests.put(url, headers=headers, json=payload, timeout=2)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
2
|
-
from netping_contr import mixins
|
|
3
1
|
import requests
|
|
2
|
+
from netping_contr import mixins
|
|
4
3
|
from requests.auth import HTTPBasicAuth
|
|
5
4
|
|
|
5
|
+
from gravity_controller_operator.controllers_super import ControllerInterface, DIInterface, RelayInterface
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
class NetPingDevice(mixins.NetPingResponseParser):
|
|
8
9
|
def __init__(self, ip, port=80, username="visor", password="ping"):
|
|
@@ -16,23 +17,22 @@ class NetPingDevice(mixins.NetPingResponseParser):
|
|
|
16
17
|
return f"{self.schema}{self.ip}:{self.port}"
|
|
17
18
|
|
|
18
19
|
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))
|
|
20
|
+
return requests.get(url=f"{self.get_full_url()}/io.cgi?io", auth=HTTPBasicAuth(self.username, self.password))
|
|
22
21
|
|
|
23
22
|
def get_all_relay_states(self):
|
|
24
23
|
states = {}
|
|
25
24
|
for i in range(1, 5):
|
|
26
25
|
r = requests.get(
|
|
27
|
-
url=f"{self.get_full_url()}/relay.cgi?r{i}",
|
|
28
|
-
|
|
26
|
+
url=f"{self.get_full_url()}/relay.cgi?r{i}", auth=HTTPBasicAuth(self.username, self.password)
|
|
27
|
+
)
|
|
29
28
|
states[i] = self.parse_relay_state(r)
|
|
30
29
|
return states
|
|
31
30
|
|
|
32
31
|
def change_relay_status(self, relay_num, state):
|
|
33
32
|
return requests.get(
|
|
34
33
|
url=f"{self.get_full_url()}/relay.cgi?r{relay_num}={state}",
|
|
35
|
-
auth=HTTPBasicAuth(self.username, self.password)
|
|
34
|
+
auth=HTTPBasicAuth(self.username, self.password),
|
|
35
|
+
)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class NetPingDI(DIInterface):
|
|
@@ -68,8 +68,7 @@ class NetPingRelay(RelayInterface):
|
|
|
68
68
|
class NetPing2Controller:
|
|
69
69
|
model = "netping_relay"
|
|
70
70
|
|
|
71
|
-
def __init__(self, ip, port=80, username="visor", password="ping",
|
|
72
|
-
name="netping_relay2", *args, **kwargs):
|
|
71
|
+
def __init__(self, ip, port=80, username="visor", password="ping", name="netping_relay2", *args, **kwargs):
|
|
73
72
|
device = NetPingDevice(ip=ip, port=port, username=username, password=password)
|
|
74
73
|
di = NetPingDI(device)
|
|
75
74
|
relay = NetPingRelay(device)
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
from http.client import responses
|
|
3
|
-
|
|
4
|
-
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
1
|
+
from gravity_controller_operator.controllers_super import ControllerInterface, DIInterface, RelayInterface
|
|
5
2
|
|
|
6
3
|
|
|
7
4
|
class SigurBase:
|
|
8
5
|
def __init__(self):
|
|
9
6
|
pass
|
|
10
7
|
|
|
8
|
+
|
|
11
9
|
class SigurDI(DIInterface, SigurBase):
|
|
12
10
|
map_keys_amount = 5
|
|
13
11
|
starts_with = 3
|
|
@@ -45,8 +43,7 @@ class SigurRelay(RelayInterface, SigurBase):
|
|
|
45
43
|
class Sigur:
|
|
46
44
|
model = "sigur"
|
|
47
45
|
|
|
48
|
-
def __init__(self, sock, login="Administrator", password="",
|
|
49
|
-
name="Sigur", *args, **kwargs):
|
|
46
|
+
def __init__(self, sock, login="Administrator", password="", name="Sigur", *args, **kwargs):
|
|
50
47
|
di = SigurDI()
|
|
51
48
|
relay = SigurRelay()
|
|
52
49
|
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -4,26 +4,25 @@
|
|
|
4
4
|
что отдаёт реальный шлюз WB-MGE в прозрачном режиме (RTU-over-TCP), и то, чем
|
|
5
5
|
по умолчанию ходит драйвер (framer="rtu"). Железо/шлюз для теста не нужны.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import socket
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
|
|
11
12
|
import pytest
|
|
12
|
-
|
|
13
13
|
from pymodbus import Framer
|
|
14
|
-
from pymodbus.server import StartTcpServer, ServerStop
|
|
15
14
|
from pymodbus.datastore import (
|
|
16
15
|
ModbusSequentialDataBlock,
|
|
17
|
-
ModbusSlaveContext,
|
|
18
16
|
ModbusServerContext,
|
|
17
|
+
ModbusSlaveContext,
|
|
19
18
|
)
|
|
19
|
+
from pymodbus.server import ServerStop, StartTcpServer
|
|
20
20
|
|
|
21
21
|
from gravity_controller_operator.controller_factory import ControllerCreator
|
|
22
|
-
from gravity_controller_operator.controllers.wb_mr6lv import WBMR6LVTCP
|
|
23
22
|
from gravity_controller_operator.controllers.emulator_contr import EmulatorController
|
|
23
|
+
from gravity_controller_operator.controllers.wb_mr6lv import WBMR6LVTCP
|
|
24
24
|
from gravity_controller_operator.main import ControllerOperator
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
SLAVE_ID = 200 # адрес реле за шлюзом (как в сборке GravityBox)
|
|
28
27
|
|
|
29
28
|
|
|
@@ -48,8 +47,7 @@ def rtu_gateway():
|
|
|
48
47
|
context = ModbusServerContext(slaves={SLAVE_ID: store}, single=False)
|
|
49
48
|
port = _free_port()
|
|
50
49
|
threading.Thread(
|
|
51
|
-
target=lambda: StartTcpServer(
|
|
52
|
-
context=context, address=("127.0.0.1", port), framer=Framer.RTU),
|
|
50
|
+
target=lambda: StartTcpServer(context=context, address=("127.0.0.1", port), framer=Framer.RTU),
|
|
53
51
|
daemon=True,
|
|
54
52
|
).start()
|
|
55
53
|
time.sleep(1.0) # дать серверу подняться до первого запроса драйвера
|
|
@@ -59,17 +57,14 @@ def rtu_gateway():
|
|
|
59
57
|
|
|
60
58
|
def test_factory_returns_tcp_model(rtu_gateway):
|
|
61
59
|
_, port = rtu_gateway
|
|
62
|
-
inst = ControllerCreator.get_controller(
|
|
63
|
-
"wb_mr6lv_tcp", ip="127.0.0.1", port=port, slave_id=SLAVE_ID)
|
|
60
|
+
inst = ControllerCreator.get_controller("wb_mr6lv_tcp", ip="127.0.0.1", port=port, slave_id=SLAVE_ID)
|
|
64
61
|
assert isinstance(inst, WBMR6LVTCP)
|
|
65
62
|
|
|
66
63
|
|
|
67
64
|
def test_factory_emulator_flag_wins(rtu_gateway):
|
|
68
65
|
# emulator=true должен дать эмулятор независимо от имени модели
|
|
69
66
|
_, port = rtu_gateway
|
|
70
|
-
inst = ControllerCreator.get_controller(
|
|
71
|
-
"wb_mr6lv_tcp", emulator=True, ip="127.0.0.1", port=port,
|
|
72
|
-
slave_id=SLAVE_ID)
|
|
67
|
+
inst = ControllerCreator.get_controller("wb_mr6lv_tcp", emulator=True, ip="127.0.0.1", port=port, slave_id=SLAVE_ID)
|
|
73
68
|
assert isinstance(inst, EmulatorController)
|
|
74
69
|
|
|
75
70
|
|
|
@@ -97,9 +92,7 @@ def test_reads_relays_over_tcp(rtu_gateway):
|
|
|
97
92
|
def test_relay_toggle_over_tcp(rtu_gateway):
|
|
98
93
|
store, port = rtu_gateway
|
|
99
94
|
store.setValues(1, 0, [0] * 6)
|
|
100
|
-
operator = ControllerOperator(
|
|
101
|
-
WBMR6LVTCP(ip="127.0.0.1", port=port, slave_id=SLAVE_ID),
|
|
102
|
-
auto_update_points=False)
|
|
95
|
+
operator = ControllerOperator(WBMR6LVTCP(ip="127.0.0.1", port=port, slave_id=SLAVE_ID), auto_update_points=False)
|
|
103
96
|
# логический канал 1 → физическое реле 0 (spec_addr WBMR6LVRelay)
|
|
104
97
|
operator.change_relay_state(1, 1)
|
|
105
98
|
assert store.getValues(1, 0, 1)[0] == 1 # запись долетела до «железа»
|
|
@@ -114,8 +107,7 @@ def test_wrong_slave_id_degrades_not_hangs(rtu_gateway):
|
|
|
114
107
|
# чтение деградирует в {"error": ...}.
|
|
115
108
|
_, port = rtu_gateway
|
|
116
109
|
t0 = time.time()
|
|
117
|
-
inst = WBMR6LVTCP(ip="127.0.0.1", port=port, slave_id=SLAVE_ID + 1,
|
|
118
|
-
timeout=0.3)
|
|
110
|
+
inst = WBMR6LVTCP(ip="127.0.0.1", port=port, slave_id=SLAVE_ID + 1, timeout=0.3)
|
|
119
111
|
assert time.time() - t0 < 15 # не вечный цикл
|
|
120
112
|
phys = inst.interface.di_interface.get_phys_dict()
|
|
121
113
|
assert isinstance(phys, dict) and "error" in phys
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import time
|
|
2
3
|
|
|
3
|
-
from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
|
|
4
|
-
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
|
5
4
|
from pymodbus import Framer
|
|
5
|
+
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
|
6
6
|
from pymodbus.exceptions import ConnectionException, ModbusException
|
|
7
7
|
|
|
8
|
+
from gravity_controller_operator.controllers_super import ControllerInterface, DIInterface, RelayInterface
|
|
8
9
|
|
|
9
10
|
# Раньше DI/реле читались/писались безусловным `while not response or
|
|
10
11
|
# response.isError()` без паузы и без ограничения попыток. На неотвечающем
|
|
@@ -18,20 +19,69 @@ READ_ATTEMPTS = 4
|
|
|
18
19
|
WRITE_ATTEMPTS = 10
|
|
19
20
|
RETRY_SLEEP = 0.15
|
|
20
21
|
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Опрос DI/реле идёт ~3 раза в секунду, поэтому при лежащей шине наивное
|
|
25
|
+
# логирование залило бы журнал сотнями строк в минуту. Одинаковые сообщения
|
|
26
|
+
# схлопываем: пишем не чаще раза в LOG_THROTTLE_SEC и указываем, сколько
|
|
27
|
+
# таких же событий было подавлено.
|
|
28
|
+
LOG_THROTTLE_SEC = 30
|
|
29
|
+
_last_logged = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _log_throttled(level, key, message, *args):
|
|
33
|
+
now = time.monotonic()
|
|
34
|
+
last_ts, suppressed = _last_logged.get(key, (0.0, 0))
|
|
35
|
+
if now - last_ts < LOG_THROTTLE_SEC:
|
|
36
|
+
_last_logged[key] = (last_ts, suppressed + 1)
|
|
37
|
+
return
|
|
38
|
+
_last_logged[key] = (now, 0)
|
|
39
|
+
text = message % args if args else message
|
|
40
|
+
if suppressed:
|
|
41
|
+
text += " [и ещё {} таких же за последние {}с]".format(suppressed, LOG_THROTTLE_SEC)
|
|
42
|
+
logger.log(level, text)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _log_always(level, key, message, *args):
|
|
46
|
+
logger.log(level, message, *args)
|
|
47
|
+
|
|
21
48
|
|
|
22
|
-
def _request_with_retry(call, attempts):
|
|
49
|
+
def _request_with_retry(call, attempts, what="modbus-запрос", warn_on_retry=False, throttle=True):
|
|
23
50
|
"""pymodbus-запрос с ограниченным ретраем. Возвращает ответ при успехе либо
|
|
24
51
|
None, когда все попытки исчерпаны (в т.ч. при ConnectionException из-за
|
|
25
|
-
недоступного шлюза/шины). Наружу не бросает.
|
|
26
|
-
|
|
52
|
+
недоступного шлюза/шины). Наружу не бросает.
|
|
53
|
+
|
|
54
|
+
Молчать про исчерпанные попытки нельзя: команда на реле шлагбаума могла
|
|
55
|
+
просто не доехать до контроллера, а в журнале AR при этом оставалось
|
|
56
|
+
бодрое «Попытка открытия шлагбаума» — отличить «AR не послал» от «шина
|
|
57
|
+
не ответила» было невозможно. Пишем и деградацию (получилось, но не с
|
|
58
|
+
первой попытки), и полный отказ.
|
|
59
|
+
"""
|
|
60
|
+
log = _log_throttled if throttle else _log_always
|
|
61
|
+
last_error = None
|
|
62
|
+
for attempt in range(1, attempts + 1):
|
|
27
63
|
try:
|
|
28
64
|
resp = call()
|
|
29
|
-
except (ConnectionException, ModbusException):
|
|
65
|
+
except (ConnectionException, ModbusException) as e:
|
|
66
|
+
last_error = e
|
|
30
67
|
time.sleep(RETRY_SLEEP)
|
|
31
68
|
continue
|
|
32
69
|
if resp and not resp.isError():
|
|
70
|
+
if attempt > 1 and warn_on_retry:
|
|
71
|
+
log(
|
|
72
|
+
logging.WARNING,
|
|
73
|
+
("retry", what),
|
|
74
|
+
"%s прошёл только с %s-й попытки (шина деградирует, последняя ошибка: %s)",
|
|
75
|
+
what,
|
|
76
|
+
attempt,
|
|
77
|
+
last_error,
|
|
78
|
+
)
|
|
33
79
|
return resp
|
|
80
|
+
last_error = resp
|
|
34
81
|
time.sleep(RETRY_SLEEP)
|
|
82
|
+
log(
|
|
83
|
+
logging.ERROR, ("fail", what), "%s НЕ выполнен за %s попыток (последняя ошибка: %s)", what, attempts, last_error
|
|
84
|
+
)
|
|
35
85
|
return None
|
|
36
86
|
|
|
37
87
|
|
|
@@ -47,10 +97,12 @@ class WBMR6LVDI(DIInterface):
|
|
|
47
97
|
|
|
48
98
|
def get_phys_dict(self):
|
|
49
99
|
response = _request_with_retry(
|
|
50
|
-
lambda: self.client.read_discrete_inputs(
|
|
51
|
-
|
|
52
|
-
|
|
100
|
+
lambda: self.client.read_discrete_inputs(self.starts_with, self.map_keys_amount, slave=self.slave_id),
|
|
101
|
+
READ_ATTEMPTS,
|
|
102
|
+
what="Опрос DI (slave %s)" % self.slave_id,
|
|
103
|
+
)
|
|
53
104
|
if response is None:
|
|
105
|
+
# Состояния DI остаются прежними — фотоэлементы «замирают».
|
|
54
106
|
return {"error": "read_discrete_inputs failed"}
|
|
55
107
|
return {i: bit for i, bit in enumerate(response.bits)}
|
|
56
108
|
|
|
@@ -67,25 +119,35 @@ class WBMR6LVRelay(RelayInterface):
|
|
|
67
119
|
|
|
68
120
|
def get_phys_dict(self):
|
|
69
121
|
response = _request_with_retry(
|
|
70
|
-
lambda: self.client.read_coils(
|
|
71
|
-
|
|
72
|
-
|
|
122
|
+
lambda: self.client.read_coils(self.starts_with, self.map_keys_amount, slave=self.slave_id),
|
|
123
|
+
READ_ATTEMPTS,
|
|
124
|
+
what="Опрос реле (slave %s)" % self.slave_id,
|
|
125
|
+
)
|
|
73
126
|
if response is None:
|
|
74
127
|
return {"error": "read_coils failed"}
|
|
75
128
|
return {i: bit for i, bit in enumerate(response.bits)}
|
|
76
129
|
|
|
77
130
|
def change_phys_relay_state(self, addr, state: bool):
|
|
131
|
+
# Команд на реле единицы за заезд, поэтому здесь важен каждый случай:
|
|
132
|
+
# warn_on_retry — чтобы видеть деградацию шины заранее, throttle=False
|
|
133
|
+
# — чтобы не проглотить второй неудавшийся «открыть» подряд, когда
|
|
134
|
+
# оператор жмёт кнопку повторно. Опрос DI, наоборот, идёт трижды в
|
|
135
|
+
# секунду и логируется приглушённо.
|
|
78
136
|
return _request_with_retry(
|
|
79
137
|
lambda: self.client.write_coil(addr, state, slave=self.slave_id),
|
|
80
|
-
WRITE_ATTEMPTS
|
|
138
|
+
WRITE_ATTEMPTS,
|
|
139
|
+
what="Запись реле coil=%s -> %s (slave %s)" % (addr, int(state), self.slave_id),
|
|
140
|
+
warn_on_retry=True,
|
|
141
|
+
throttle=False,
|
|
142
|
+
)
|
|
81
143
|
|
|
82
144
|
|
|
83
145
|
class WBMR6LV:
|
|
84
146
|
"""WB-MR6LV/I по Modbus RTU (RS-485 через USB-адаптер, /dev/tty*)."""
|
|
147
|
+
|
|
85
148
|
model = "wb_mr6lv"
|
|
86
149
|
|
|
87
|
-
def __init__(self, device, slave_id, baudrate=9600, stopbits=2, bytesize=8,
|
|
88
|
-
name="WBMR6LV", *args, **kwargs):
|
|
150
|
+
def __init__(self, device, slave_id, baudrate=9600, stopbits=2, bytesize=8, name="WBMR6LV", *args, **kwargs):
|
|
89
151
|
client = ModbusSerialClient(
|
|
90
152
|
device,
|
|
91
153
|
framer=Framer.RTU,
|
|
@@ -116,13 +178,12 @@ class WBMR6LVTCP:
|
|
|
116
178
|
``slave_id`` — RTU-адрес самого реле за шлюзом (напр. 192/200), а не адрес
|
|
117
179
|
шлюза. ``port`` — TCP-порт шлюза: 502 → RS-485-1, 503 → RS-485-2.
|
|
118
180
|
"""
|
|
181
|
+
|
|
119
182
|
model = "wb_mr6lv_tcp"
|
|
120
183
|
|
|
121
|
-
def __init__(self, ip, slave_id, port=502, timeout=3, framer="rtu",
|
|
122
|
-
name="WBMR6LVTCP", *args, **kwargs):
|
|
184
|
+
def __init__(self, ip, slave_id, port=502, timeout=3, framer="rtu", name="WBMR6LVTCP", *args, **kwargs):
|
|
123
185
|
fr = Framer.SOCKET if str(framer).lower() in ("socket", "tcp") else Framer.RTU
|
|
124
|
-
client = ModbusTcpClient(
|
|
125
|
-
host=ip, port=int(port), framer=fr, timeout=timeout)
|
|
186
|
+
client = ModbusTcpClient(host=ip, port=int(port), framer=fr, timeout=timeout)
|
|
126
187
|
di = WBMR6LVDI(client, slave_id)
|
|
127
188
|
relay = WBMR6LVRelay(client, slave_id)
|
|
128
189
|
self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
1
|
import datetime
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class SoftStateMixin:
|
|
@@ -7,7 +7,8 @@ class SoftStateMixin:
|
|
|
7
7
|
Хранит программное состояние входов/выходов.
|
|
8
8
|
Позволяет обновлять состояния и получать их по ключу.
|
|
9
9
|
"""
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
spec_addr = {} # переназначение адресов (если физический != логическому)
|
|
11
12
|
map_keys_amount = 0
|
|
12
13
|
starts_with = 0
|
|
13
14
|
|
|
@@ -41,6 +42,7 @@ class BasePhysInterface(ABC):
|
|
|
41
42
|
Абстрактный интерфейс работы с железом.
|
|
42
43
|
Потомки реализуют конкретную работу с DI или Relay.
|
|
43
44
|
"""
|
|
45
|
+
|
|
44
46
|
@abstractmethod
|
|
45
47
|
def get_phys_dict(self) -> dict:
|
|
46
48
|
"""
|
|
@@ -57,6 +59,7 @@ class RelayPhysInterface(BasePhysInterface):
|
|
|
57
59
|
"""
|
|
58
60
|
pass
|
|
59
61
|
|
|
62
|
+
|
|
60
63
|
class DIInterface(SoftStateMixin, BasePhysInterface):
|
|
61
64
|
def __init__(self):
|
|
62
65
|
super().__init__()
|
|
@@ -104,6 +107,7 @@ class ControllerInterface:
|
|
|
104
107
|
"""
|
|
105
108
|
Комбинирует доступ к DI и Relay интерфейсам.
|
|
106
109
|
"""
|
|
110
|
+
|
|
107
111
|
def __init__(self, di_interface=None, relay_interface=None):
|
|
108
112
|
self.di_interface = di_interface
|
|
109
113
|
self.relay_interface = relay_interface
|
|
@@ -117,5 +121,5 @@ class ControllerInterface:
|
|
|
117
121
|
def get_all_states(self):
|
|
118
122
|
return {
|
|
119
123
|
"di": self.di_interface.get_state() if self.di_interface else {},
|
|
120
|
-
"relays": self.relay_interface.get_state() if self.relay_interface else {}
|
|
124
|
+
"relays": self.relay_interface.get_state() if self.relay_interface else {},
|
|
121
125
|
}
|
|
@@ -6,25 +6,24 @@ import time
|
|
|
6
6
|
from gravity_controller_operator.controller_factory import ControllerCreator
|
|
7
7
|
from gravity_controller_operator.main import ControllerOperator
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
RELAY_CHANNELS = list(range(1, 7))
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
def parse_args():
|
|
14
|
-
parser = argparse.ArgumentParser(
|
|
15
|
-
description="Диагностика контроллера: дискретные входы и реле."
|
|
16
|
-
)
|
|
13
|
+
parser = argparse.ArgumentParser(description="Диагностика контроллера: дискретные входы и реле.")
|
|
17
14
|
parser.add_argument("--device", help="Путь к /dev/tty (Modbus RTU)")
|
|
15
|
+
parser.add_argument("--ip", help="IP шлюза WB-MGE v.3 (Modbus TCP вместо --device)")
|
|
18
16
|
parser.add_argument(
|
|
19
|
-
"--
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
"--port",
|
|
18
|
+
type=int,
|
|
19
|
+
default=502,
|
|
22
20
|
help="TCP-порт шлюза: 502 → RS-485-1, 503 → RS-485-2",
|
|
23
21
|
)
|
|
24
22
|
parser.add_argument(
|
|
25
|
-
"--framer",
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
"--framer",
|
|
24
|
+
choices=("rtu", "socket"),
|
|
25
|
+
default="rtu",
|
|
26
|
+
help="rtu — прозрачный режим шлюза (RTU-over-TCP, по умолчанию); socket — шлюз в режиме Modbus TCP",
|
|
28
27
|
)
|
|
29
28
|
parser.add_argument("--slave-id", required=True, type=int, help="Modbus slave id")
|
|
30
29
|
parser.add_argument(
|
|
@@ -102,7 +101,7 @@ def describe_phys_changes(before, after):
|
|
|
102
101
|
if curr is None:
|
|
103
102
|
continue
|
|
104
103
|
if prev != curr:
|
|
105
|
-
changes.append(f"{addr}:{int(prev)}->
|
|
104
|
+
changes.append(f"{addr}:{int(prev)}->{int(curr)}")
|
|
106
105
|
return changes
|
|
107
106
|
|
|
108
107
|
|
|
@@ -134,8 +133,6 @@ def get_di_mapping(operator):
|
|
|
134
133
|
return sorted(mapping_by_phys.items(), key=lambda item: item[0])
|
|
135
134
|
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
136
|
def get_phys_channels(di_interface):
|
|
140
137
|
values = di_interface.get_phys_dict()
|
|
141
138
|
keys = sorted(values.keys())
|
|
@@ -160,9 +157,7 @@ def run_di_test(operator, timeout):
|
|
|
160
157
|
baseline_phys = di_interface.get_phys_dict()
|
|
161
158
|
baseline_logical = snapshot_logical_states(di_interface)
|
|
162
159
|
if not wait_for_state(operator, logical_ch, False, timeout):
|
|
163
|
-
action = prompt_retry(
|
|
164
|
-
f"{label}: не удалось увидеть неактивный уровень. (r)etry/(s)kip/(q)uit: "
|
|
165
|
-
)
|
|
160
|
+
action = prompt_retry(f"{label}: не удалось увидеть неактивный уровень. (r)etry/(s)kip/(q)uit: ")
|
|
166
161
|
if action == "quit":
|
|
167
162
|
return False
|
|
168
163
|
if action == "skip":
|
|
@@ -179,22 +174,15 @@ def run_di_test(operator, timeout):
|
|
|
179
174
|
operator.update_points()
|
|
180
175
|
logical_now = snapshot_logical_states(di_interface)
|
|
181
176
|
logical_changes = [
|
|
182
|
-
ch
|
|
183
|
-
for ch, prev in baseline_logical.items()
|
|
184
|
-
if prev is False and logical_now.get(ch) is True
|
|
177
|
+
ch for ch, prev in baseline_logical.items() if prev is False and logical_now.get(ch) is True
|
|
185
178
|
]
|
|
186
179
|
if len(logical_changes) > 1:
|
|
187
180
|
print(f"{label}: WARNING multiple logical changes: {logical_changes}")
|
|
188
181
|
if expected_phys is not None and changed_addr != expected_phys:
|
|
189
|
-
print(
|
|
190
|
-
f"{label}: WARNING mismatch expected phys DI{expected_phys} "
|
|
191
|
-
f"but saw DI{changed_addr}"
|
|
192
|
-
)
|
|
182
|
+
print(f"{label}: WARNING mismatch expected phys DI{expected_phys} but saw DI{changed_addr}")
|
|
193
183
|
|
|
194
184
|
if not wait_for_state(operator, logical_ch, True, timeout):
|
|
195
|
-
action = prompt_retry(
|
|
196
|
-
f"{label}: сигнал не зафиксирован. (r)etry/(s)kip/(q)uit: "
|
|
197
|
-
)
|
|
185
|
+
action = prompt_retry(f"{label}: сигнал не зафиксирован. (r)etry/(s)kip/(q)uit: ")
|
|
198
186
|
if action == "quit":
|
|
199
187
|
return False
|
|
200
188
|
if action == "skip":
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class UnknownController(Exception):
|
|
2
|
+
# Исключение, возникающее при неизвестном имени терминала
|
|
3
|
+
def __init__(self, contr_name=None, contr_list=[]):
|
|
4
|
+
text = (
|
|
5
|
+
f"Контроллер {contr_name} не обнаружен! Создайте класс с контроллером "
|
|
6
|
+
"в директории controllers, укажите его модель через атрибут "
|
|
7
|
+
"model, затем добавьте этот класс в список "
|
|
8
|
+
f"AVAILABLE_CONTROLLERS {tuple(contr_list)}"
|
|
9
|
+
)
|
|
10
|
+
super().__init__(text)
|
|
@@ -24,7 +24,7 @@ class ControllerOperator:
|
|
|
24
24
|
# — иначе контроллер «замерзает» до перезапуска AR. Пауза и
|
|
25
25
|
# продолжаем: клиент переподключится на следующей итерации.
|
|
26
26
|
time.sleep(0.5)
|
|
27
|
-
#time.sleep(self.update_cooldown)
|
|
27
|
+
# time.sleep(self.update_cooldown)
|
|
28
28
|
|
|
29
29
|
def update_points(self):
|
|
30
30
|
with self.mutex:
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import unittest
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
from gravity_controller_operator.main import ControllerCreater, ControllerOperator
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class TestCase(unittest.TestCase):
|
|
7
7
|
def test_operator(self):
|
|
8
|
-
inst = ControllerCreater.get_controller(
|
|
9
|
-
"wb_mr6lv12",False)
|
|
8
|
+
inst = ControllerCreater.get_controller("wb_mr6lv12", False)
|
|
10
9
|
co = ControllerOperator(inst)
|
|
11
10
|
print(co.get_points())
|
|
12
11
|
# inst = ControllerOperator("localhost", 8234, "arm_k210",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gravity_controller_operator
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)
|
|
5
5
|
Author: PunchyArchy
|
|
6
6
|
Author-email: ksmdrmvscthny@gmail.com
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from setuptools import
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="gravity_controller_operator",
|
|
5
|
-
version="2.
|
|
5
|
+
version="2.4.0",
|
|
6
6
|
description="SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)",
|
|
7
7
|
long_description=open("README.md", encoding="utf-8").read(),
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -1,8 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|