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.

Files changed (75) hide show
  1. {pymscada-0.0.12 → pymscada-0.0.15}/PKG-INFO +28 -7
  2. {pymscada-0.0.12 → pymscada-0.0.15}/README.md +25 -6
  3. {pymscada-0.0.12 → pymscada-0.0.15}/pyproject.toml +3 -1
  4. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/__init__.py +6 -4
  5. pymscada-0.0.15/src/pymscada/demo/logixclient.yaml +50 -0
  6. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/modbusserver.yaml +2 -2
  7. pymscada-0.0.15/src/pymscada/demo/pymscada-io-logixclient.service +15 -0
  8. pymscada-0.0.15/src/pymscada/demo/pymscada-io-snmpclient.service +15 -0
  9. pymscada-0.0.15/src/pymscada/demo/snmpclient.yaml +73 -0
  10. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/tags.yaml +85 -1
  11. pymscada-0.0.15/src/pymscada/iodrivers/logix_client.py +80 -0
  12. pymscada-0.0.15/src/pymscada/iodrivers/logix_map.py +147 -0
  13. {pymscada-0.0.12/src/pymscada → pymscada-0.0.15/src/pymscada/iodrivers}/modbus_client.py +1 -1
  14. {pymscada-0.0.12/src/pymscada → pymscada-0.0.15/src/pymscada/iodrivers}/modbus_server.py +1 -1
  15. pymscada-0.0.15/src/pymscada/iodrivers/snmp_client.py +75 -0
  16. pymscada-0.0.15/src/pymscada/iodrivers/snmp_client2.py +53 -0
  17. pymscada-0.0.15/src/pymscada/iodrivers/snmp_map.py +71 -0
  18. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/main.py +14 -3
  19. pymscada-0.0.15/src/pymscada/tools/walk.py +55 -0
  20. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/validate.py +10 -7
  21. pymscada-0.0.15/tests/iodrivers/test_logix.py +111 -0
  22. pymscada-0.0.15/tests/test_assets/hist_tag_0_10_2.dat +0 -0
  23. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_validate.py +3 -0
  24. {pymscada-0.0.12 → pymscada-0.0.15}/LICENSE +0 -0
  25. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/__main__.py +0 -0
  26. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/bus_client.py +0 -0
  27. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/bus_server.py +0 -0
  28. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/checkout.py +0 -0
  29. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/config.py +0 -0
  30. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/console.py +0 -0
  31. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/README.md +0 -0
  32. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/__init__.py +0 -0
  33. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/bus.yaml +0 -0
  34. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/files.yaml +0 -0
  35. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/history.yaml +0 -0
  36. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/modbus_plc.py +0 -0
  37. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/modbusclient.yaml +0 -0
  38. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-bus.service +0 -0
  39. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-demo-modbus_plc.service +0 -0
  40. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-files.service +0 -0
  41. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-history.service +0 -0
  42. /pymscada-0.0.12/src/pymscada/demo/pymscada-modbusclient.service → /pymscada-0.0.15/src/pymscada/demo/pymscada-io-modbusclient.service +0 -0
  43. /pymscada-0.0.12/src/pymscada/demo/pymscada-modbusserver.service → /pymscada-0.0.15/src/pymscada/demo/pymscada-io-modbusserver.service +0 -0
  44. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/pymscada-wwwserver.service +0 -0
  45. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/simulate.yaml +0 -0
  46. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/demo/wwwserver.yaml +0 -0
  47. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/files.py +0 -0
  48. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/history.py +0 -0
  49. /pymscada-0.0.12/tests/test_assets/hist_tag_0_10_2.dat → /pymscada-0.0.15/src/pymscada/iodrivers/__init__.py +0 -0
  50. {pymscada-0.0.12/src/pymscada → pymscada-0.0.15/src/pymscada/iodrivers}/modbus_map.py +0 -0
  51. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/misc.py +0 -0
  52. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/pdf/__init__.py +0 -0
  53. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/pdf/one.pdf +0 -0
  54. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/pdf/two.pdf +0 -0
  55. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/periodic.py +0 -0
  56. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/protocol_constants.py +0 -0
  57. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/samplers.py +0 -0
  58. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/simulate.py +0 -0
  59. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/tag.py +0 -0
  60. {pymscada-0.0.12 → pymscada-0.0.15}/src/pymscada/www_server.py +0 -0
  61. {pymscada-0.0.12 → pymscada-0.0.15}/tests/__init__.py +0 -0
  62. {pymscada-0.0.12 → pymscada-0.0.15}/tests/bus_echo.py +0 -0
  63. {pymscada-0.0.12/tests → pymscada-0.0.15/tests/iodrivers}/test_modbus.py +0 -0
  64. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/busserver.yaml +0 -0
  65. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_0.dat +0 -0
  66. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_15.dat +0 -0
  67. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_26.dat +0 -0
  68. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_assets/hist_tag_0_50.dat +0 -0
  69. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_bus_server.py +0 -0
  70. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_config.py +0 -0
  71. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_misc.py +0 -0
  72. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_periodic.py +0 -0
  73. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_samplers.py +0 -0
  74. {pymscada-0.0.12 → pymscada-0.0.15}/tests/test_tag.py +0 -0
  75. {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.12
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 - an Angular single page web application
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 - an Angular single page web application
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.12"
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
- 'find_nodes', 'ramp',
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
@@ -1,5 +1,5 @@
1
- bus_ip:
2
- bus_port:
1
+ bus_ip: 192.168.1.100 # for the demo PLC make empty (None)
2
+ bus_port: 2345 # for the demo PLC make empty (None)
3
3
  rtus:
4
4
  - name: RTU
5
5
  ip: 0.0.0.0
@@ -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
 
@@ -3,7 +3,7 @@ import asyncio
3
3
  import logging
4
4
  from struct import pack, unpack_from
5
5
  from pymscada.bus_client import BusClient
6
- from pymscada.modbus_map import ModbusMaps
6
+ from pymscada.iodrivers.modbus_map import ModbusMaps
7
7
 
8
8
 
9
9
  class ModbusServerProtocol: