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.
Files changed (30) hide show
  1. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/LICENSE +1 -1
  2. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/PKG-INFO +1 -1
  3. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controller_factory.py +4 -5
  4. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/arm_k210.py +5 -2
  5. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/emulator_contr.py +2 -3
  6. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/moxa.py +4 -15
  7. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/netping_relay.py +9 -10
  8. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/sigur.py +3 -6
  9. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/tests/test_moxa_controller.py +2 -1
  10. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/tests/test_wb_mr6lv_tcp.py +9 -17
  11. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/wb_mr6lv.py +80 -19
  12. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers_super.py +7 -3
  13. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/diagnostics_cli.py +14 -26
  14. gravity_controller_operator-2.4.0/gravity_controller_operator/exceptions.py +10 -0
  15. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/main.py +1 -1
  16. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/tests/main_tests.py +3 -4
  17. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/PKG-INFO +1 -1
  18. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/setup.py +2 -2
  19. gravity_controller_operator-2.3.0/gravity_controller_operator/exceptions.py +0 -8
  20. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/README.md +0 -0
  21. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/__init__.py +0 -0
  22. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/__init__.py +0 -0
  23. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/controllers/tests/__init__.py +0 -0
  24. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/docs/__init__.py +0 -0
  25. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator/tests/__init__.py +0 -0
  26. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/SOURCES.txt +0 -0
  27. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/dependency_links.txt +0 -0
  28. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/requires.txt +0 -0
  29. {gravity_controller_operator-2.3.0 → gravity_controller_operator-2.4.0}/gravity_controller_operator.egg-info/top_level.txt +0 -0
  30. {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.0
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.arm_k210 import ARMK210Controller
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
- from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
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
- auth=HTTPBasicAuth(self.username, self.password))
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 time
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)
@@ -1,6 +1,7 @@
1
1
  import pytest
2
- from gravity_controller_operator.main import ControllerOperator
2
+
3
3
  from gravity_controller_operator.controllers.moxa import MoxaE1214
4
+ from gravity_controller_operator.main import ControllerOperator
4
5
 
5
6
 
6
7
  @pytest.fixture(scope="module")
@@ -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
- for _ in range(attempts):
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
- self.starts_with, self.map_keys_amount, slave=self.slave_id),
52
- READ_ATTEMPTS)
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
- self.starts_with, self.map_keys_amount, slave=self.slave_id),
72
- READ_ATTEMPTS)
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
- spec_addr = {} # переназначение адресов (если физический != логическому)
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
- "--ip", help="IP шлюза WB-MGE v.3 (Modbus TCP вместо --device)")
20
- parser.add_argument(
21
- "--port", type=int, default=502,
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", choices=("rtu", "socket"), default="rtu",
26
- help="rtu — прозрачный режим шлюза (RTU-over-TCP, по умолчанию); "
27
- "socket — шлюз в режиме Modbus TCP",
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)}->" f"{int(curr)}")
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
- from gravity_controller_operator.main import ControllerOperator, \
3
- ControllerCreater
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.0
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 setup, find_packages
1
+ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="gravity_controller_operator",
5
- version="2.3.0",
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)