pymscada 0.0.12__tar.gz → 0.0.15__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.
Potentially problematic release.
This version of pymscada might be problematic. Click here for more details.
- {pymscada-0.0.12 → pymscada-0.0.15}/PKG-INFO +28 -7
- {pymscada-0.0.12 → pymscada-0.0.15}/README.md +25 -6
- {pymscada-0.0.12 → pymscada-0.0.15}/pyproject.toml +3 -1
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/__init__.py +6 -4
- pymscada-0.0.15/src/pymscada/demo/logixclient.yaml +50 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/modbusserver.yaml +2 -2
- pymscada-0.0.15/src/pymscada/demo/pymscada-io-logixclient.service +15 -0
- pymscada-0.0.15/src/pymscada/demo/pymscada-io-snmpclient.service +15 -0
- pymscada-0.0.15/src/pymscada/demo/snmpclient.yaml +73 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/tags.yaml +85 -1
- pymscada-0.0.15/src/pymscada/iodrivers/logix_client.py +80 -0
- pymscada-0.0.15/src/pymscada/iodrivers/logix_map.py +147 -0
- {pymscada-0.0.12/src/pymscada → pymscada-0.0.15/src/pymscada/iodrivers}/modbus_client.py +1 -1
- {pymscada-0.0.12/src/pymscada → pymscada-0.0.15/src/pymscada/iodrivers}/modbus_server.py +1 -1
- pymscada-0.0.15/src/pymscada/iodrivers/snmp_client.py +75 -0
- pymscada-0.0.15/src/pymscada/iodrivers/snmp_client2.py +53 -0
- pymscada-0.0.15/src/pymscada/iodrivers/snmp_map.py +71 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/main.py +14 -3
- pymscada-0.0.15/src/pymscada/tools/walk.py +55 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/validate.py +10 -7
- pymscada-0.0.15/tests/iodrivers/test_logix.py +111 -0
- pymscada-0.0.15/tests/test_assets/hist_tag_0_10_2.dat +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_validate.py +3 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/LICENSE +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/__main__.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/bus_client.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/bus_server.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/checkout.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/config.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/console.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/README.md +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/__init__.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/bus.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/files.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/history.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/modbus_plc.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/modbusclient.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-bus.service +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-files.service +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-history.service +0 -0
- /pymscada-0.0.12/src/pymscada/demo/pymscada-modbusclient.service → /pymscada-0.0.15/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
- /pymscada-0.0.12/src/pymscada/demo/pymscada-modbusserver.service → /pymscada-0.0.15/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/simulate.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/wwwserver.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/files.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/history.py +0 -0
- /pymscada-0.0.12/tests/test_assets/hist_tag_0_10_2.dat → /pymscada-0.0.15/src/pymscada/iodrivers/__init__.py +0 -0
- {pymscada-0.0.12/src/pymscada → pymscada-0.0.15/src/pymscada/iodrivers}/modbus_map.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/misc.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/pdf/__init__.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/pdf/one.pdf +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/pdf/two.pdf +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/periodic.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/protocol_constants.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/samplers.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/simulate.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/tag.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/www_server.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/__init__.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/bus_echo.py +0 -0
- {pymscada-0.0.12/tests → pymscada-0.0.15/tests/iodrivers}/test_modbus.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/busserver.yaml +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_0.dat +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_15.dat +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_26.dat +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_50.dat +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_bus_server.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_config.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_misc.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_periodic.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_samplers.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_tag.py +0 -0
- {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_tag_history.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pymscada
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.15
|
|
4
4
|
Summary: Shared tag value SCADA with python backup and Angular UI
|
|
5
5
|
Author-Email: Jamie Walton <jamie@walton.net.nz>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -15,6 +15,8 @@ Requires-Dist: PyYAML>=6.0.1
|
|
|
15
15
|
Requires-Dist: aiohttp>=3.8.5
|
|
16
16
|
Requires-Dist: pymscada-html<=0.1.0
|
|
17
17
|
Requires-Dist: cerberus>=1.3.5
|
|
18
|
+
Requires-Dist: pycomm3>=1.2.14
|
|
19
|
+
Requires-Dist: pysnmplib>=5.0.24
|
|
18
20
|
Description-Content-Type: text/markdown
|
|
19
21
|
|
|
20
22
|
# pymscada
|
|
@@ -24,20 +26,39 @@ Description-Content-Type: text/markdown
|
|
|
24
26
|
|
|
25
27
|
## Python Mobile SCADA
|
|
26
28
|
|
|
29
|
+
This is an open source Python SCADA package intended for use over your
|
|
30
|
+
mobile phone. It has been developed to connect custom python scripts to
|
|
31
|
+
Modicon and Rockwell PLCs, and present a web based user interface, with
|
|
32
|
+
minimal coding. ```pymscada``` does expect you to be able to use python.
|
|
33
|
+
|
|
34
|
+
```pymscada``` shares values through a ```Tag``` for all data. Tag values
|
|
35
|
+
are updated by exception through a message bus. Changes are in any
|
|
36
|
+
direction with simple loops stopped with a simplistic _don't send updates
|
|
37
|
+
back to where they came from_ check. It's too simple to stop you defeating
|
|
38
|
+
it (but there are easier ways to break the code).
|
|
39
|
+
|
|
40
|
+
There are primary owner Tag's. A _request to set_ command is passed to the
|
|
41
|
+
author of the Tag, where it is updated. This allows database information
|
|
42
|
+
to pass through the Tag values as well. Tag values are typically float or
|
|
43
|
+
int however they may also be megabyte sized binary or dictionary values.
|
|
44
|
+
|
|
45
|
+
Example ```systemd``` service files, pymscada module config files and
|
|
46
|
+
custom script examples are included. The example scripts include polling
|
|
47
|
+
weather from tommorrow.io and a python script based Modbus TCP PLC.
|
|
48
|
+
|
|
49
|
+
## Introduction
|
|
50
|
+
|
|
27
51
|
This is a small SCADA package that will run on Linux (preferably) or
|
|
28
52
|
Windows. The server runs as several modules on the host, sharing
|
|
29
53
|
information through a message bus. A __subset__ of modules is:
|
|
30
54
|
|
|
31
55
|
- Bus server - shares tag values with by exception updates
|
|
32
56
|
- Modbus client - reads and writes to a PLC using Modbus/TCP
|
|
57
|
+
- Logix client - uses ```pycomm3``` to read / write to Rockwell PLCs
|
|
58
|
+
- SNMP client - polls SNMP OID values
|
|
33
59
|
- History - saves data changes, serves history to web pages
|
|
34
60
|
- Web server - serves web pages which connect with a web socket
|
|
35
|
-
- Web pages -
|
|
36
|
-
|
|
37
|
-
Web pages are responsive and defined procedurally from the
|
|
38
|
-
```wwwserver.yaml``` config file.
|
|
39
|
-
|
|
40
|
-
Trends use [uPlot](https://github.com/leeoniya/uPlot).
|
|
61
|
+
- Web pages - procedurally generated setpoint, indication and trends
|
|
41
62
|
|
|
42
63
|
## Objectives
|
|
43
64
|
|
|
@@ -5,20 +5,39 @@
|
|
|
5
5
|
|
|
6
6
|
## Python Mobile SCADA
|
|
7
7
|
|
|
8
|
+
This is an open source Python SCADA package intended for use over your
|
|
9
|
+
mobile phone. It has been developed to connect custom python scripts to
|
|
10
|
+
Modicon and Rockwell PLCs, and present a web based user interface, with
|
|
11
|
+
minimal coding. ```pymscada``` does expect you to be able to use python.
|
|
12
|
+
|
|
13
|
+
```pymscada``` shares values through a ```Tag``` for all data. Tag values
|
|
14
|
+
are updated by exception through a message bus. Changes are in any
|
|
15
|
+
direction with simple loops stopped with a simplistic _don't send updates
|
|
16
|
+
back to where they came from_ check. It's too simple to stop you defeating
|
|
17
|
+
it (but there are easier ways to break the code).
|
|
18
|
+
|
|
19
|
+
There are primary owner Tag's. A _request to set_ command is passed to the
|
|
20
|
+
author of the Tag, where it is updated. This allows database information
|
|
21
|
+
to pass through the Tag values as well. Tag values are typically float or
|
|
22
|
+
int however they may also be megabyte sized binary or dictionary values.
|
|
23
|
+
|
|
24
|
+
Example ```systemd``` service files, pymscada module config files and
|
|
25
|
+
custom script examples are included. The example scripts include polling
|
|
26
|
+
weather from tommorrow.io and a python script based Modbus TCP PLC.
|
|
27
|
+
|
|
28
|
+
## Introduction
|
|
29
|
+
|
|
8
30
|
This is a small SCADA package that will run on Linux (preferably) or
|
|
9
31
|
Windows. The server runs as several modules on the host, sharing
|
|
10
32
|
information through a message bus. A __subset__ of modules is:
|
|
11
33
|
|
|
12
34
|
- Bus server - shares tag values with by exception updates
|
|
13
35
|
- Modbus client - reads and writes to a PLC using Modbus/TCP
|
|
36
|
+
- Logix client - uses ```pycomm3``` to read / write to Rockwell PLCs
|
|
37
|
+
- SNMP client - polls SNMP OID values
|
|
14
38
|
- History - saves data changes, serves history to web pages
|
|
15
39
|
- Web server - serves web pages which connect with a web socket
|
|
16
|
-
- Web pages -
|
|
17
|
-
|
|
18
|
-
Web pages are responsive and defined procedurally from the
|
|
19
|
-
```wwwserver.yaml``` config file.
|
|
20
|
-
|
|
21
|
-
Trends use [uPlot](https://github.com/leeoniya/uPlot).
|
|
40
|
+
- Web pages - procedurally generated setpoint, indication and trends
|
|
22
41
|
|
|
23
42
|
## Objectives
|
|
24
43
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pymscada"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.15"
|
|
4
4
|
description = "Shared tag value SCADA with python backup and Angular UI"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Jamie Walton", email = "jamie@walton.net.nz" },
|
|
@@ -10,6 +10,8 @@ dependencies = [
|
|
|
10
10
|
"aiohttp>=3.8.5",
|
|
11
11
|
"pymscada-html<=0.1.0",
|
|
12
12
|
"cerberus>=1.3.5",
|
|
13
|
+
"pycomm3>=1.2.14",
|
|
14
|
+
"pysnmplib>=5.0.24",
|
|
13
15
|
]
|
|
14
16
|
requires-python = ">=3.9"
|
|
15
17
|
readme = "README.md"
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
from pymscada.bus_client import BusClient
|
|
3
3
|
from pymscada.bus_server import BusServer
|
|
4
4
|
from pymscada.config import Config
|
|
5
|
+
from pymscada.iodrivers.logix_client import LogixClient
|
|
6
|
+
from pymscada.iodrivers.modbus_client import ModbusClient
|
|
7
|
+
from pymscada.iodrivers.modbus_server import ModbusServer
|
|
5
8
|
from pymscada.misc import find_nodes, ramp
|
|
6
|
-
from pymscada.modbus_client import ModbusClient
|
|
7
|
-
from pymscada.modbus_server import ModbusServer
|
|
8
9
|
from pymscada.periodic import Periodic
|
|
9
10
|
from pymscada.tag import Tag
|
|
10
11
|
from pymscada.validate import validate
|
|
@@ -13,10 +14,11 @@ __all__ = [
|
|
|
13
14
|
'BusClient',
|
|
14
15
|
'BusServer',
|
|
15
16
|
'Config',
|
|
16
|
-
'
|
|
17
|
+
'LogixClient',
|
|
17
18
|
'ModbusClient',
|
|
18
19
|
'ModbusServer',
|
|
20
|
+
'find_nodes', 'ramp',
|
|
19
21
|
'Periodic',
|
|
20
22
|
'Tag',
|
|
21
|
-
'validate'
|
|
23
|
+
'validate',
|
|
22
24
|
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
bus_ip: 127.0.0.1
|
|
2
|
+
bus_port: 1324
|
|
3
|
+
rtus:
|
|
4
|
+
- name: Ani
|
|
5
|
+
ip: 172.26.7.196
|
|
6
|
+
rate: 0.2
|
|
7
|
+
read:
|
|
8
|
+
- {addr: Fout, type: 'REAL[]', start: 0, end: 99}
|
|
9
|
+
- {addr: Iout, type: 'DINT[]', start: 0, end: 99}
|
|
10
|
+
- {addr: OutVar, type: REAL}
|
|
11
|
+
writeok:
|
|
12
|
+
- {addr: Fin, type: 'REAL[]', start: 0, end: 99}
|
|
13
|
+
- {addr: Iin, type: 'REAL[]', start: 0, end: 99}
|
|
14
|
+
- {addr: InVar, type: REAL}
|
|
15
|
+
tags:
|
|
16
|
+
Ani_Fin_20:
|
|
17
|
+
type: float32
|
|
18
|
+
read: 'Ani:Fout[20]'
|
|
19
|
+
write: 'Ani:Fin[20]'
|
|
20
|
+
Ani_Fout_20:
|
|
21
|
+
type: float32
|
|
22
|
+
addr: 'Ani:Fout[20]'
|
|
23
|
+
Ani_Iin_20:
|
|
24
|
+
type: int32
|
|
25
|
+
read: 'Ani:Iout[20]'
|
|
26
|
+
write: 'Ani:Iin[20]'
|
|
27
|
+
Ani_Iout_20:
|
|
28
|
+
type: int32
|
|
29
|
+
addr: 'Ani:Iout[20]'
|
|
30
|
+
Ani_Iin_21_0:
|
|
31
|
+
type: bool
|
|
32
|
+
read: 'Ani:Iout[21].0'
|
|
33
|
+
write: 'Ani:Iin[21].0'
|
|
34
|
+
Ani_Iout_21_0:
|
|
35
|
+
type: bool
|
|
36
|
+
addr: 'Ani:Iout[21].0'
|
|
37
|
+
Ani_Iin_21_1:
|
|
38
|
+
type: bool
|
|
39
|
+
read: 'Ani:Iout[21].1'
|
|
40
|
+
write: 'Ani:Iin[21].1'
|
|
41
|
+
Ani_Iout_21_1:
|
|
42
|
+
type: bool
|
|
43
|
+
addr: 'Ani:Iout[21].1'
|
|
44
|
+
InVar:
|
|
45
|
+
type: float32
|
|
46
|
+
read: Ani:OutVar
|
|
47
|
+
write: Ani:InVar
|
|
48
|
+
OutVar:
|
|
49
|
+
type: float32
|
|
50
|
+
addr: Ani:OutVar
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - pycomm3 client
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ logixclient --config __DIR__/config/logixclient.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=mscada
|
|
12
|
+
Group=mscada
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - SNMP client
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ snmpclient --config __DIR__/config/snmpclient.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=mscada
|
|
12
|
+
Group=mscada
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
bus_ip: 127.0.0.1
|
|
2
|
+
bus_port: 1324
|
|
3
|
+
rtus:
|
|
4
|
+
- name: router
|
|
5
|
+
ip: 172.26.3.254
|
|
6
|
+
community: public
|
|
7
|
+
rate: 10
|
|
8
|
+
read:
|
|
9
|
+
- '1.3.6.1.2.1.31.1.1.1.6.1'
|
|
10
|
+
- '1.3.6.1.2.1.31.1.1.1.6.2'
|
|
11
|
+
- '1.3.6.1.2.1.31.1.1.1.6.3'
|
|
12
|
+
- '1.3.6.1.2.1.31.1.1.1.6.4'
|
|
13
|
+
- '1.3.6.1.2.1.31.1.1.1.6.5'
|
|
14
|
+
- '1.3.6.1.2.1.31.1.1.1.6.6'
|
|
15
|
+
- '1.3.6.1.2.1.31.1.1.1.6.7'
|
|
16
|
+
- '1.3.6.1.2.1.31.1.1.1.6.8'
|
|
17
|
+
- '1.3.6.1.2.1.31.1.1.1.10.1'
|
|
18
|
+
- '1.3.6.1.2.1.31.1.1.1.10.2'
|
|
19
|
+
- '1.3.6.1.2.1.31.1.1.1.10.3'
|
|
20
|
+
- '1.3.6.1.2.1.31.1.1.1.10.4'
|
|
21
|
+
- '1.3.6.1.2.1.31.1.1.1.10.5'
|
|
22
|
+
- '1.3.6.1.2.1.31.1.1.1.10.6'
|
|
23
|
+
- '1.3.6.1.2.1.31.1.1.1.10.7'
|
|
24
|
+
- '1.3.6.1.2.1.31.1.1.1.10.8'
|
|
25
|
+
tags:
|
|
26
|
+
Router_eth1_bytes_in:
|
|
27
|
+
type: int_roc
|
|
28
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.1'
|
|
29
|
+
Router_eth1_bytes_out:
|
|
30
|
+
type: int_roc
|
|
31
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.1'
|
|
32
|
+
Router_eth2_bytes_in:
|
|
33
|
+
type: int_roc
|
|
34
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.2'
|
|
35
|
+
Router_eth2_bytes_out:
|
|
36
|
+
type: int_roc
|
|
37
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.2'
|
|
38
|
+
Router_eth3_bytes_in:
|
|
39
|
+
type: int_roc
|
|
40
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.3'
|
|
41
|
+
Router_eth3_bytes_out:
|
|
42
|
+
type: int_roc
|
|
43
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.3'
|
|
44
|
+
Router_eth4_bytes_in:
|
|
45
|
+
type: int_roc
|
|
46
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.4'
|
|
47
|
+
Router_eth4_bytes_out:
|
|
48
|
+
type: int_roc
|
|
49
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.4'
|
|
50
|
+
Router_eth5_bytes_in:
|
|
51
|
+
type: int_roc
|
|
52
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.5'
|
|
53
|
+
Router_eth5_bytes_out:
|
|
54
|
+
type: int_roc
|
|
55
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.5'
|
|
56
|
+
Router_eth6_bytes_in:
|
|
57
|
+
type: int_roc
|
|
58
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.6'
|
|
59
|
+
Router_eth6_bytes_out:
|
|
60
|
+
type: int_roc
|
|
61
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.6'
|
|
62
|
+
Router_eth7_bytes_in:
|
|
63
|
+
type: int_roc
|
|
64
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.7'
|
|
65
|
+
Router_eth7_bytes_out:
|
|
66
|
+
type: int_roc
|
|
67
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.7'
|
|
68
|
+
Router_eth8_bytes_in:
|
|
69
|
+
type: int_roc
|
|
70
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.6.8'
|
|
71
|
+
Router_eth8_bytes_out:
|
|
72
|
+
type: int_roc
|
|
73
|
+
addr: 'router:1.3.6.1.2.1.31.1.1.1.10.8'
|
|
@@ -129,4 +129,88 @@ disk_use:
|
|
|
129
129
|
min: 0
|
|
130
130
|
max: 100
|
|
131
131
|
units: '%'
|
|
132
|
-
dp: 1
|
|
132
|
+
dp: 1
|
|
133
|
+
sim_IntSet: # for the demo PLC sim_ tags not required
|
|
134
|
+
desc: Simulation tag
|
|
135
|
+
type: int
|
|
136
|
+
sim_IntVal:
|
|
137
|
+
desc: Simulation tag
|
|
138
|
+
type: int
|
|
139
|
+
sim_FloatSet:
|
|
140
|
+
desc: Simulation tag
|
|
141
|
+
type: float
|
|
142
|
+
sim_FloatVal:
|
|
143
|
+
desc: Simulation tag
|
|
144
|
+
type: float
|
|
145
|
+
sim_MultiSet:
|
|
146
|
+
desc: Simulation tag
|
|
147
|
+
type: int
|
|
148
|
+
sim_MultiVal:
|
|
149
|
+
desc: Simulation tag
|
|
150
|
+
type: int
|
|
151
|
+
sim_TimeSet:
|
|
152
|
+
desc: Simulation tag
|
|
153
|
+
type: int
|
|
154
|
+
sim_TimeVal:
|
|
155
|
+
desc: Simulation tag
|
|
156
|
+
type: int
|
|
157
|
+
sim_DateSet:
|
|
158
|
+
desc: Simulation tag
|
|
159
|
+
type: int
|
|
160
|
+
sim_DateVal:
|
|
161
|
+
desc: Simulation tag
|
|
162
|
+
type: int
|
|
163
|
+
sim_DateTimeSet:
|
|
164
|
+
desc: Simulation tag
|
|
165
|
+
type: int
|
|
166
|
+
sim_DateTimeVal:
|
|
167
|
+
desc: Simulation tag
|
|
168
|
+
type: int
|
|
169
|
+
Router_eth1_bytes_in:
|
|
170
|
+
desc: eth1 bytes in
|
|
171
|
+
type: int
|
|
172
|
+
Router_eth1_bytes_out:
|
|
173
|
+
desc: eth1 bytes out
|
|
174
|
+
type: int
|
|
175
|
+
Router_eth2_bytes_in:
|
|
176
|
+
desc: eth2 bytes in
|
|
177
|
+
type: int
|
|
178
|
+
Router_eth2_bytes_out:
|
|
179
|
+
desc: eth2 bytes out
|
|
180
|
+
type: int
|
|
181
|
+
Router_eth3_bytes_in:
|
|
182
|
+
desc: eth3 bytes in
|
|
183
|
+
type: int
|
|
184
|
+
Router_eth3_bytes_out:
|
|
185
|
+
desc: eth3 bytes out
|
|
186
|
+
type: int
|
|
187
|
+
Router_eth4_bytes_in:
|
|
188
|
+
desc: eth4 bytes in
|
|
189
|
+
type: int
|
|
190
|
+
Router_eth4_bytes_out:
|
|
191
|
+
desc: eth4 bytes out
|
|
192
|
+
type: int
|
|
193
|
+
Router_eth5_bytes_in:
|
|
194
|
+
desc: eth5 bytes in
|
|
195
|
+
type: int
|
|
196
|
+
Router_eth5_bytes_out:
|
|
197
|
+
desc: eth5 bytes out
|
|
198
|
+
type: int
|
|
199
|
+
Router_eth6_bytes_in:
|
|
200
|
+
desc: eth6 bytes in
|
|
201
|
+
type: int
|
|
202
|
+
Router_eth6_bytes_out:
|
|
203
|
+
desc: eth6 bytes out
|
|
204
|
+
type: int
|
|
205
|
+
Router_eth7_bytes_in:
|
|
206
|
+
desc: eth7 bytes in
|
|
207
|
+
type: int
|
|
208
|
+
Router_eth7_bytes_out:
|
|
209
|
+
desc: eth7 bytes out
|
|
210
|
+
type: int
|
|
211
|
+
Router_eth8_bytes_in:
|
|
212
|
+
desc: eth8 bytes in
|
|
213
|
+
type: int
|
|
214
|
+
Router_eth8_bytes_out:
|
|
215
|
+
desc: eth8 bytes out
|
|
216
|
+
type: int
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Read and write to Logix PLC processor."""
|
|
2
|
+
import logging
|
|
3
|
+
from pycomm3 import LogixDriver
|
|
4
|
+
from pymscada.bus_client import BusClient
|
|
5
|
+
from pymscada.periodic import Periodic
|
|
6
|
+
from pymscada.iodrivers.logix_map import LogixMaps
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LogixClientConnector:
|
|
10
|
+
"""Poll Logix device, write on change in write range."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, name: str, ip:str, rate: float, read: list,
|
|
13
|
+
writeok: list, mapping: LogixMaps):
|
|
14
|
+
"""Set up polling client."""
|
|
15
|
+
self.plc_name = name
|
|
16
|
+
self.ip = ip
|
|
17
|
+
self.read_tags = []
|
|
18
|
+
for r in read:
|
|
19
|
+
tag = r['addr']
|
|
20
|
+
if r['type'].endswith('[]'):
|
|
21
|
+
count = r['end'] - r['start'] + 1
|
|
22
|
+
tag = f"{r['addr']}[{r['start']}]{{{count}}}"
|
|
23
|
+
self.read_tags.append(tag)
|
|
24
|
+
self.mapping = mapping
|
|
25
|
+
self.mapping.add_write_callback(name, writeok,
|
|
26
|
+
self.write_tag_update)
|
|
27
|
+
self.periodic = Periodic(self.poll, rate)
|
|
28
|
+
self.plc = LogixDriver(ip)
|
|
29
|
+
|
|
30
|
+
def write_tag_update(self, addr: str, value): # : int|float
|
|
31
|
+
"""Write out any tag updates."""
|
|
32
|
+
if not self.plc.connected and not self.plc.open():
|
|
33
|
+
logging.warning(f'write failed {self.plc_name} {addr} to {value}')
|
|
34
|
+
return
|
|
35
|
+
logging.info(f'writing {addr} {value}')
|
|
36
|
+
res = self.plc.write((addr, value))
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
async def poll(self):
|
|
40
|
+
"""Poll data, reopen connection if dead."""
|
|
41
|
+
if not self.plc.connected and not self.plc.open():
|
|
42
|
+
return
|
|
43
|
+
polled_tags = None
|
|
44
|
+
polled_tags = self.plc.read(*self.read_tags)
|
|
45
|
+
self.mapping.polled_data(self.plc_name, polled_tags)
|
|
46
|
+
|
|
47
|
+
async def start(self):
|
|
48
|
+
"""Start polling."""
|
|
49
|
+
await self.periodic.start()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LogixClient:
|
|
53
|
+
|
|
54
|
+
def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
|
|
55
|
+
rtus: dict = {}, tags: dict = {}) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Connect to bus on bus_ip:bus_port, connect to Logix PLCs.
|
|
58
|
+
|
|
59
|
+
Event loop must be running.
|
|
60
|
+
"""
|
|
61
|
+
self.busclient = None
|
|
62
|
+
if bus_ip is not None:
|
|
63
|
+
self.busclient = BusClient(bus_ip, bus_port)
|
|
64
|
+
self.mapping = LogixMaps(tags)
|
|
65
|
+
self.connections: list[LogixClientConnector] = []
|
|
66
|
+
for rtu in rtus:
|
|
67
|
+
connection = LogixClientConnector(**rtu, mapping=self.mapping)
|
|
68
|
+
self.connections.append(connection)
|
|
69
|
+
|
|
70
|
+
async def _poll(self):
|
|
71
|
+
"""For testing."""
|
|
72
|
+
for connection in self.connections:
|
|
73
|
+
await connection.poll()
|
|
74
|
+
|
|
75
|
+
async def start(self):
|
|
76
|
+
"""Start bus connection and PLC polling."""
|
|
77
|
+
if self.busclient is not None:
|
|
78
|
+
await self.busclient.start()
|
|
79
|
+
for connection in self.connections:
|
|
80
|
+
await connection.start()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Map between modbus table and Tag."""
|
|
2
|
+
import logging
|
|
3
|
+
from time import time
|
|
4
|
+
from pymscada.tag import Tag
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# data types for PLCs
|
|
8
|
+
DTYPES = {
|
|
9
|
+
'int32': [int, -2147483648, 2147483647],
|
|
10
|
+
'float32': [float, -3.40282346639e+38, 3.40282346639e+38],
|
|
11
|
+
'bool': [int, 0, 1]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def tag_split(plc_tag: str):
|
|
16
|
+
separator = plc_tag.find(':')
|
|
17
|
+
arr_start_loc = plc_tag.find('[')
|
|
18
|
+
arr_end_loc = plc_tag.find(']')
|
|
19
|
+
bit_loc = plc_tag.find('.')
|
|
20
|
+
plc = plc_tag[:separator]
|
|
21
|
+
if arr_start_loc == -1 and bit_loc == -1:
|
|
22
|
+
var = plc_tag[separator + 1:]
|
|
23
|
+
elm = None
|
|
24
|
+
bit = None
|
|
25
|
+
elif arr_start_loc == -1:
|
|
26
|
+
var = plc_tag[separator + 1:bit_loc]
|
|
27
|
+
elm = None
|
|
28
|
+
bit = int(plc_tag[bit_loc + 1:])
|
|
29
|
+
elif bit_loc == -1:
|
|
30
|
+
var = plc_tag[separator + 1:arr_start_loc]
|
|
31
|
+
elm = int(plc_tag[arr_start_loc + 1:arr_end_loc])
|
|
32
|
+
bit = None
|
|
33
|
+
else:
|
|
34
|
+
var = plc_tag[separator + 1:arr_start_loc]
|
|
35
|
+
elm = int(plc_tag[arr_start_loc + 1:arr_end_loc])
|
|
36
|
+
bit = int(plc_tag[bit_loc + 1:])
|
|
37
|
+
return plc, var, elm, bit
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LogixMap:
|
|
41
|
+
"""Do value updates for each tag."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, tagname: str, src_type: str, read_tag: str,
|
|
44
|
+
write_tag: str):
|
|
45
|
+
"""Initialise modbus map and Tag."""
|
|
46
|
+
dtype, dmin, dmax = DTYPES[src_type][0:3]
|
|
47
|
+
self.tag = Tag(tagname, dtype)
|
|
48
|
+
self.map_bus = id(self)
|
|
49
|
+
self.read_plc, self.read_var, self.read_elm, self.read_bit = \
|
|
50
|
+
tag_split(read_tag)
|
|
51
|
+
self.plc_read_tag = read_tag
|
|
52
|
+
self.write_plc, self.write_var, self.write_elm, self.write_bit = \
|
|
53
|
+
tag_split(write_tag)
|
|
54
|
+
self.plc_write_tag = write_tag
|
|
55
|
+
self.callback = None
|
|
56
|
+
if dmin is not None:
|
|
57
|
+
self.tag.value_min = dmin
|
|
58
|
+
if dmax is not None:
|
|
59
|
+
self.tag.value_max = dmax
|
|
60
|
+
|
|
61
|
+
def set_callback(self, callback):
|
|
62
|
+
"""Add tag callback interface."""
|
|
63
|
+
self.callback = callback
|
|
64
|
+
self.tag.add_callback(self.tag_value_changed, bus_id=self.map_bus)
|
|
65
|
+
|
|
66
|
+
def set_tag_value(self, value, time_us):
|
|
67
|
+
"""Pass update from IO driver to tag value."""
|
|
68
|
+
if self.read_bit is not None:
|
|
69
|
+
if value & 1 << self.read_bit:
|
|
70
|
+
value = 1
|
|
71
|
+
else:
|
|
72
|
+
value = 0
|
|
73
|
+
if self.tag.value != value:
|
|
74
|
+
self.tag.value = value, time_us, self.map_bus
|
|
75
|
+
|
|
76
|
+
def tag_value_changed(self, tag: Tag):
|
|
77
|
+
"""Pass update from tag value to IO driver."""
|
|
78
|
+
if self.write_elm is None and self.write_bit is None:
|
|
79
|
+
addr = self.write_var
|
|
80
|
+
elif self.write_elm is None:
|
|
81
|
+
addr = f'{self.write_var}.{self.write_bit}'
|
|
82
|
+
elif self.write_bit is None:
|
|
83
|
+
addr = f'{self.write_var}[{self.write_elm}]'
|
|
84
|
+
else:
|
|
85
|
+
addr = f'{self.write_var}[{self.write_elm}].{self.write_bit}'
|
|
86
|
+
self.callback(addr, tag.value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LogixMaps:
|
|
90
|
+
"""Link tags with protocol connector."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, tags: dict):
|
|
93
|
+
"""Collect maps based on a tag dictionary."""
|
|
94
|
+
# use the tagname to access the map.
|
|
95
|
+
self.tag_map: dict[str, LogixMap] = {}
|
|
96
|
+
# use the plc_name then variable name to access a list of maps.
|
|
97
|
+
self.read_var_map: dict[str, dict[str, list[LogixMap]]] = {}
|
|
98
|
+
for tagname, v in tags.items():
|
|
99
|
+
if 'addr' in v:
|
|
100
|
+
read_addr = v['addr']
|
|
101
|
+
write_addr = v['addr']
|
|
102
|
+
else:
|
|
103
|
+
read_addr = v['read']
|
|
104
|
+
write_addr = v['write']
|
|
105
|
+
map = LogixMap(tagname, v['type'], read_addr, write_addr)
|
|
106
|
+
if map.read_plc not in self.read_var_map:
|
|
107
|
+
self.read_var_map[map.read_plc] = {}
|
|
108
|
+
if map.read_var not in self.read_var_map[map.read_plc]:
|
|
109
|
+
# make a list so multiple bits can map to a word
|
|
110
|
+
self.read_var_map[map.read_plc][map.read_var] = []
|
|
111
|
+
self.read_var_map[map.read_plc][map.read_var].append(map)
|
|
112
|
+
self.tag_map[map.tag.name] = map
|
|
113
|
+
|
|
114
|
+
def add_write_callback(self, plcname, writeok, callback):
|
|
115
|
+
"""Connection advises device links."""
|
|
116
|
+
# Create a set of all possible valid addresses
|
|
117
|
+
write_set = set()
|
|
118
|
+
for w in writeok:
|
|
119
|
+
if '[' in w['type']:
|
|
120
|
+
for i in range(w['start'], w['end'] + 1):
|
|
121
|
+
write_set.add((w['addr'], i))
|
|
122
|
+
else:
|
|
123
|
+
write_set.add((w['addr'], None))
|
|
124
|
+
# where the mapped tag uses a valid address, add callback to
|
|
125
|
+
# the connection writer
|
|
126
|
+
for map in self.tag_map.values():
|
|
127
|
+
if map.write_plc == plcname and \
|
|
128
|
+
(map.write_var, map.write_elm) in write_set:
|
|
129
|
+
map.set_callback(callback)
|
|
130
|
+
|
|
131
|
+
def polled_data(self, plcname, polls):
|
|
132
|
+
"""Pass updates read from the PLC to the tags."""
|
|
133
|
+
time_us = int(time() * 1e6)
|
|
134
|
+
for poll in polls:
|
|
135
|
+
if poll.error is not None:
|
|
136
|
+
logging.error(poll.error)
|
|
137
|
+
arr_start_loc = poll.tag.find('[')
|
|
138
|
+
if arr_start_loc == -1:
|
|
139
|
+
for map in self.read_var_map[plcname][poll.tag]:
|
|
140
|
+
map.set_tag_value(poll.value, time_us)
|
|
141
|
+
else:
|
|
142
|
+
var = poll.tag[:arr_start_loc]
|
|
143
|
+
elm = int(poll.tag[arr_start_loc + 1: -1])
|
|
144
|
+
for map in self.read_var_map[plcname][var]:
|
|
145
|
+
elm_offset = map.read_elm - elm
|
|
146
|
+
if elm_offset > 0 and elm_offset < len(poll.value):
|
|
147
|
+
map.set_tag_value(poll.value[elm_offset], time_us)
|
|
@@ -4,7 +4,7 @@ from itertools import chain
|
|
|
4
4
|
import logging
|
|
5
5
|
from struct import pack, unpack_from
|
|
6
6
|
from pymscada.bus_client import BusClient
|
|
7
|
-
from pymscada.modbus_map import ModbusMaps
|
|
7
|
+
from pymscada.iodrivers.modbus_map import ModbusMaps
|
|
8
8
|
from pymscada.periodic import Periodic
|
|
9
9
|
|
|
10
10
|
|