pymscada 0.1.11__py3-none-any.whl → 0.1.11b3__py3-none-any.whl
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.
- pymscada/bus_client.py +23 -22
- pymscada/bus_server.py +28 -25
- pymscada/checkout.py +6 -10
- pymscada/console.py +11 -14
- pymscada/demo/openweather.yaml +25 -0
- pymscada/demo/ping.yaml +0 -3
- pymscada/demo/tags.yaml +2 -148
- pymscada/demo/wwwserver.yaml +6 -461
- pymscada/iodrivers/openweather.py +126 -0
- pymscada/main.py +41 -295
- pymscada/module_config.py +217 -0
- pymscada/protocol_constants.py +29 -39
- pymscada/validate.py +29 -26
- pymscada/www_server.py +4 -4
- {pymscada-0.1.11.dist-info → pymscada-0.1.11b3.dist-info}/METADATA +3 -3
- {pymscada-0.1.11.dist-info → pymscada-0.1.11b3.dist-info}/RECORD +19 -16
- {pymscada-0.1.11.dist-info → pymscada-0.1.11b3.dist-info}/WHEEL +1 -1
- {pymscada-0.1.11.dist-info → pymscada-0.1.11b3.dist-info}/entry_points.txt +0 -0
- {pymscada-0.1.11.dist-info → pymscada-0.1.11b3.dist-info}/licenses/LICENSE +0 -0
pymscada/main.py
CHANGED
|
@@ -1,311 +1,57 @@
|
|
|
1
1
|
"""Main server entry point."""
|
|
2
2
|
import argparse
|
|
3
3
|
import asyncio
|
|
4
|
-
from importlib.metadata import version
|
|
5
4
|
import logging
|
|
6
5
|
import sys
|
|
7
|
-
from
|
|
8
|
-
from pymscada.
|
|
9
|
-
from pymscada.config import Config
|
|
10
|
-
from pymscada.console import Console
|
|
11
|
-
from pymscada.files import Files
|
|
12
|
-
from pymscada.history import History
|
|
13
|
-
from pymscada.opnotes import OpNotes
|
|
14
|
-
from pymscada.www_server import WwwServer
|
|
15
|
-
from pymscada.iodrivers.accuweather import AccuWeatherClient
|
|
16
|
-
from pymscada.iodrivers.logix_client import LogixClient
|
|
17
|
-
from pymscada.iodrivers.modbus_client import ModbusClient
|
|
18
|
-
from pymscada.iodrivers.modbus_server import ModbusServer
|
|
19
|
-
from pymscada.iodrivers.ping_client import PingClient
|
|
20
|
-
from pymscada.iodrivers.snmp_client import SnmpClient
|
|
21
|
-
from pymscada.validate import validate
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Module():
|
|
25
|
-
"""Default Module."""
|
|
26
|
-
|
|
27
|
-
name = None
|
|
28
|
-
help = None
|
|
29
|
-
epilog = None
|
|
30
|
-
module = None
|
|
31
|
-
config = True
|
|
32
|
-
tags = True
|
|
33
|
-
sub = None
|
|
34
|
-
await_future = True
|
|
35
|
-
|
|
36
|
-
def __init__(self, subparser: argparse._SubParsersAction):
|
|
37
|
-
"""Add arguments common to all subparsers."""
|
|
38
|
-
self.sub = subparser.add_parser(
|
|
39
|
-
self.name, help=self.help, epilog=self.epilog)
|
|
40
|
-
self.sub.set_defaults(app=self)
|
|
41
|
-
if self.config:
|
|
42
|
-
self.sub.add_argument(
|
|
43
|
-
'--config', metavar='file', default=f'{self.name}.yaml',
|
|
44
|
-
help=f"Config file, default is '{self.name}.yaml'")
|
|
45
|
-
if self.tags:
|
|
46
|
-
self.sub.add_argument(
|
|
47
|
-
'--tags', metavar='file', default='tags.yaml',
|
|
48
|
-
help="Tags file, default is 'tags.yaml'")
|
|
49
|
-
self.sub.add_argument('--verbose', action='store_true',
|
|
50
|
-
help="Set level to logging.INFO")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class _Bus(Module):
|
|
54
|
-
"""Bus Server."""
|
|
55
|
-
|
|
56
|
-
name = 'bus'
|
|
57
|
-
help = 'run the message bus'
|
|
58
|
-
tags = False
|
|
59
|
-
|
|
60
|
-
def run_once(self, options):
|
|
61
|
-
"""Create the module."""
|
|
62
|
-
config = Config(options.config)
|
|
63
|
-
self.module = BusServer(**config)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class _WwwServer(Module):
|
|
67
|
-
"""WWW Server Module."""
|
|
68
|
-
|
|
69
|
-
name = 'wwwserver'
|
|
70
|
-
help = 'serve web pages'
|
|
71
|
-
|
|
72
|
-
def run_once(self, options):
|
|
73
|
-
"""Create the module."""
|
|
74
|
-
config = Config(options.config)
|
|
75
|
-
tag_info = dict(Config(options.tags))
|
|
76
|
-
self.module = WwwServer(tag_info=tag_info, **config)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class _History(Module):
|
|
80
|
-
"""History Module."""
|
|
81
|
-
|
|
82
|
-
name = 'history'
|
|
83
|
-
help = 'collect and serve history'
|
|
84
|
-
|
|
85
|
-
def run_once(self, options):
|
|
86
|
-
"""Create the module."""
|
|
87
|
-
config = Config(options.config)
|
|
88
|
-
tag_info = dict(Config(options.tags))
|
|
89
|
-
self.module = History(tag_info=tag_info, **config)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
class _Files(Module):
|
|
93
|
-
"""Files Module."""
|
|
94
|
-
|
|
95
|
-
name = 'files'
|
|
96
|
-
help = 'receive and send files'
|
|
97
|
-
tags = False
|
|
98
|
-
|
|
99
|
-
def run_once(self, options):
|
|
100
|
-
"""Create the module."""
|
|
101
|
-
config = Config(options.config)
|
|
102
|
-
self.module = Files(**config)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class _OpNotes(Module):
|
|
106
|
-
"""Operator Notes Module."""
|
|
107
|
-
|
|
108
|
-
name = 'opnotes'
|
|
109
|
-
help = 'present and manage operator notes'
|
|
110
|
-
tags = False
|
|
111
|
-
|
|
112
|
-
def run_once(self, options):
|
|
113
|
-
"""Create the module."""
|
|
114
|
-
config = Config(options.config)
|
|
115
|
-
self.module = OpNotes(**config)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class _Console(Module):
|
|
119
|
-
"""Bus Module."""
|
|
120
|
-
|
|
121
|
-
name = 'console'
|
|
122
|
-
help = 'interactive bus console'
|
|
123
|
-
config = False
|
|
124
|
-
await_future = False
|
|
125
|
-
|
|
126
|
-
def __init__(self, subparser: argparse._SubParsersAction):
|
|
127
|
-
super().__init__(subparser)
|
|
128
|
-
self.sub.add_argument(
|
|
129
|
-
'-p', '--port', action='store', type=int, default=1324,
|
|
130
|
-
help='connect to port (default: 1324)')
|
|
131
|
-
self.sub.add_argument(
|
|
132
|
-
'-i', '--ip', action='store', default='localhost',
|
|
133
|
-
help='connect to ip address (default: locahost)')
|
|
134
|
-
|
|
135
|
-
def run_once(self, options):
|
|
136
|
-
"""Create the module."""
|
|
137
|
-
tag_info = dict(Config(options.tags))
|
|
138
|
-
self.module = Console(options.ip, options.port, tag_info)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class _checkout(Module):
|
|
142
|
-
"""Bus Module."""
|
|
143
|
-
|
|
144
|
-
name = 'checkout'
|
|
145
|
-
help = 'create example config files'
|
|
146
|
-
epilog = """
|
|
147
|
-
To add to systemd `f="pymscada-bus" && cp config/$f.service
|
|
148
|
-
/lib/systemd/system && systemctl enable $f && systemctl start
|
|
149
|
-
$f`"""
|
|
150
|
-
config = False
|
|
151
|
-
tags = False
|
|
152
|
-
|
|
153
|
-
def __init__(self, subparser: argparse._SubParsersAction):
|
|
154
|
-
super().__init__(subparser)
|
|
155
|
-
self.sub.add_argument(
|
|
156
|
-
'--overwrite', action='store_true', default=False,
|
|
157
|
-
help='checkout may overwrite files, CARE!')
|
|
158
|
-
self.sub.add_argument(
|
|
159
|
-
'--diff', action='store_true', default=False,
|
|
160
|
-
help='compare default with existing')
|
|
161
|
-
|
|
162
|
-
def run_once(self, options):
|
|
163
|
-
"""Create the module."""
|
|
164
|
-
checkout(overwrite=options.overwrite, diff=options.diff)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class _validate(Module):
|
|
168
|
-
"""Bus Module."""
|
|
169
|
-
|
|
170
|
-
name = 'validate'
|
|
171
|
-
help = 'validate config files'
|
|
172
|
-
config = False
|
|
173
|
-
tags = False
|
|
174
|
-
|
|
175
|
-
def __init__(self, subparser: argparse._SubParsersAction):
|
|
176
|
-
super().__init__(subparser)
|
|
177
|
-
self.sub.add_argument(
|
|
178
|
-
'--path', metavar='file',
|
|
179
|
-
help='default is current working directory')
|
|
180
|
-
|
|
181
|
-
def run_once(self, options):
|
|
182
|
-
"""Create the module."""
|
|
183
|
-
r, e, p = validate(options.path)
|
|
184
|
-
if r:
|
|
185
|
-
print(f'Config files in {p} valid.')
|
|
186
|
-
else:
|
|
187
|
-
print(e)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
class _AccuWeatherClient(Module):
|
|
191
|
-
"""Bus Module."""
|
|
192
|
-
|
|
193
|
-
name = 'accuweatherclient'
|
|
194
|
-
help = 'poll weather information'
|
|
195
|
-
|
|
196
|
-
def run_once(self, options):
|
|
197
|
-
"""Create the module."""
|
|
198
|
-
config = Config(options.config)
|
|
199
|
-
self.module = AccuWeatherClient(**config)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class _LogixClient(Module):
|
|
203
|
-
"""Bus Module."""
|
|
204
|
-
|
|
205
|
-
name = 'logixclient'
|
|
206
|
-
help = 'poll/write to logix devices'
|
|
207
|
-
|
|
208
|
-
def run_once(self, options):
|
|
209
|
-
"""Create the module."""
|
|
210
|
-
config = Config(options.config)
|
|
211
|
-
self.module = LogixClient(**config)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
class _ModbusServer(Module):
|
|
215
|
-
"""Bus Module."""
|
|
216
|
-
|
|
217
|
-
name = 'modbusserver'
|
|
218
|
-
help = 'receive modbus messages'
|
|
219
|
-
epilog = """
|
|
220
|
-
Needs `setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/python3.nn` to
|
|
221
|
-
bind to port 502."""
|
|
222
|
-
tags = False
|
|
223
|
-
|
|
224
|
-
def run_once(self, options):
|
|
225
|
-
"""Create the module."""
|
|
226
|
-
config = Config(options.config)
|
|
227
|
-
self.module = ModbusServer(**config)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
class _ModbusClient(Module):
|
|
231
|
-
"""Bus Module."""
|
|
232
|
-
|
|
233
|
-
name = 'modbusclient'
|
|
234
|
-
help = 'poll/write to modbus devices'
|
|
235
|
-
tags = False
|
|
236
|
-
|
|
237
|
-
def run_once(self, options):
|
|
238
|
-
"""Create the module."""
|
|
239
|
-
config = Config(options.config)
|
|
240
|
-
self.module = ModbusClient(**config)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
class _PingClient(Module):
|
|
244
|
-
"""Bus Module."""
|
|
245
|
-
|
|
246
|
-
name = 'ping'
|
|
247
|
-
help = 'ping a list of addresses, return time'
|
|
248
|
-
epilog = """
|
|
249
|
-
Needs `setcap CAP_NET_RAW+ep /usr/bin/python3.nn` to open SOCK_RAW
|
|
250
|
-
"""
|
|
251
|
-
tags = False
|
|
252
|
-
|
|
253
|
-
def run_once(self, options):
|
|
254
|
-
"""Create the module."""
|
|
255
|
-
if sys.platform.startswith("win"):
|
|
256
|
-
asyncio.set_event_loop_policy(
|
|
257
|
-
asyncio.WindowsSelectorEventLoopPolicy())
|
|
258
|
-
config = Config(options.config)
|
|
259
|
-
self.module = PingClient(**config)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
class _SnmpClient(Module):
|
|
263
|
-
"""Bus Module."""
|
|
264
|
-
|
|
265
|
-
name = 'snmpclient'
|
|
266
|
-
help = 'poll snmp oids'
|
|
267
|
-
tags = False
|
|
268
|
-
|
|
269
|
-
def run_once(self, options):
|
|
270
|
-
"""Create the module."""
|
|
271
|
-
config = Config(options.config)
|
|
272
|
-
self.module = SnmpClient(**config)
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from pymscada.module_config import ModuleFactory
|
|
273
8
|
|
|
274
9
|
|
|
275
|
-
def args(
|
|
10
|
+
def args():
|
|
276
11
|
"""Read commandline arguments."""
|
|
277
12
|
parser = argparse.ArgumentParser(
|
|
278
13
|
prog='pymscada',
|
|
279
14
|
description='Connect IO, logic, applications, and webpage UI',
|
|
280
|
-
epilog=f'Python Mobile SCADA {
|
|
15
|
+
epilog=f'Python Mobile SCADA {version("pymscada")}'
|
|
281
16
|
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
_Files(s)
|
|
287
|
-
_OpNotes(s)
|
|
288
|
-
_Console(s)
|
|
289
|
-
_checkout(s)
|
|
290
|
-
_validate(s)
|
|
291
|
-
_AccuWeatherClient(s)
|
|
292
|
-
_LogixClient(s)
|
|
293
|
-
_ModbusServer(s)
|
|
294
|
-
_ModbusClient(s)
|
|
295
|
-
_PingClient(s)
|
|
296
|
-
_SnmpClient(s)
|
|
17
|
+
factory = ModuleFactory()
|
|
18
|
+
subparsers = parser.add_subparsers(title='module', dest='module_name')
|
|
19
|
+
for _, module_def in factory.modules.items():
|
|
20
|
+
factory.add_module_parser(subparsers, module_def)
|
|
297
21
|
return parser.parse_args()
|
|
298
22
|
|
|
299
23
|
|
|
300
24
|
async def run():
|
|
301
|
-
"""Run
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
25
|
+
"""Run the selected module."""
|
|
26
|
+
options = args()
|
|
27
|
+
if not options.module_name:
|
|
28
|
+
print("Error: Please specify a module to run")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
root_logger = logging.getLogger()
|
|
31
|
+
handler = logging.StreamHandler()
|
|
32
|
+
formatter = logging.Formatter('%(levelname)s:pymscada: %(message)s')
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
root_logger.handlers.clear() # Remove any existing handlers
|
|
35
|
+
root_logger.addHandler(handler)
|
|
36
|
+
root_logger.setLevel(logging.INFO)
|
|
37
|
+
logging.info(f'Python Mobile SCADA {version("pymscada")} starting '
|
|
38
|
+
f'{options.module_name}')
|
|
39
|
+
if not options.verbose:
|
|
40
|
+
root_logger.setLevel(logging.WARNING)
|
|
41
|
+
factory = ModuleFactory()
|
|
42
|
+
module = factory.create_module(options.module_name, options)
|
|
43
|
+
if module is not None:
|
|
44
|
+
if hasattr(module, 'start'):
|
|
45
|
+
await module.start()
|
|
46
|
+
if options.module_name in factory.modules:
|
|
47
|
+
module_def = factory.modules[options.module_name]
|
|
48
|
+
if module_def.await_future:
|
|
49
|
+
await asyncio.get_event_loop().create_future()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
"""Entry point."""
|
|
54
|
+
asyncio.run(run())
|
|
55
|
+
|
|
56
|
+
if __name__ == '__main__':
|
|
57
|
+
main()
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Module configuration and factory system."""
|
|
2
|
+
from typing import Any, Optional, Type
|
|
3
|
+
import argparse
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
import logging
|
|
6
|
+
from pymscada.config import Config
|
|
7
|
+
from pymscada.console import Console
|
|
8
|
+
|
|
9
|
+
class ModuleArgument:
|
|
10
|
+
def __init__(self, args: tuple[str, ...], kwargs: dict[str, Any]):
|
|
11
|
+
self.args = args
|
|
12
|
+
self.kwargs = kwargs
|
|
13
|
+
|
|
14
|
+
class ModuleDefinition:
|
|
15
|
+
"""Defines a module's configuration and behavior."""
|
|
16
|
+
def __init__(self, name: str, help: str, module_class: Type[Any], *,
|
|
17
|
+
config: bool = True, tags: bool = True,
|
|
18
|
+
epilog: Optional[str] = None,
|
|
19
|
+
extra_args: list[ModuleArgument] = None,
|
|
20
|
+
await_future: bool = True):
|
|
21
|
+
self.name = name
|
|
22
|
+
self.help = help
|
|
23
|
+
self.module_class = module_class
|
|
24
|
+
self.config = config
|
|
25
|
+
self.tags = tags
|
|
26
|
+
self.epilog = epilog
|
|
27
|
+
self.extra_args = extra_args
|
|
28
|
+
self.await_future = await_future
|
|
29
|
+
|
|
30
|
+
def create_module_registry():
|
|
31
|
+
"""Create the central module registry with lazy imports."""
|
|
32
|
+
return [
|
|
33
|
+
ModuleDefinition(
|
|
34
|
+
name='bus',
|
|
35
|
+
help='run the message bus',
|
|
36
|
+
module_class='pymscada.bus_server:BusServer',
|
|
37
|
+
tags=False
|
|
38
|
+
),
|
|
39
|
+
ModuleDefinition(
|
|
40
|
+
name='wwwserver',
|
|
41
|
+
help='serve web pages',
|
|
42
|
+
module_class='pymscada.www_server:WwwServer'
|
|
43
|
+
),
|
|
44
|
+
ModuleDefinition(
|
|
45
|
+
name='history',
|
|
46
|
+
help='save tag changes to database',
|
|
47
|
+
module_class='pymscada.history:History'
|
|
48
|
+
),
|
|
49
|
+
ModuleDefinition(
|
|
50
|
+
name='files',
|
|
51
|
+
help='serve files',
|
|
52
|
+
module_class='pymscada.files:Files',
|
|
53
|
+
tags=False
|
|
54
|
+
),
|
|
55
|
+
ModuleDefinition(
|
|
56
|
+
name='opnotes',
|
|
57
|
+
help='operator notes',
|
|
58
|
+
module_class='pymscada.opnotes:OpNotes',
|
|
59
|
+
tags=False
|
|
60
|
+
),
|
|
61
|
+
ModuleDefinition(
|
|
62
|
+
name='validate',
|
|
63
|
+
help='validate config files',
|
|
64
|
+
module_class='pymscada.validate:validate',
|
|
65
|
+
config=False,
|
|
66
|
+
tags=False,
|
|
67
|
+
extra_args=[
|
|
68
|
+
ModuleArgument(
|
|
69
|
+
('--path',),
|
|
70
|
+
{'metavar': 'file', 'help': 'default is current working directory'}
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
),
|
|
74
|
+
ModuleDefinition(
|
|
75
|
+
name='checkout',
|
|
76
|
+
help='create example config files',
|
|
77
|
+
module_class='pymscada.checkout:checkout',
|
|
78
|
+
config=False,
|
|
79
|
+
tags=False,
|
|
80
|
+
epilog="""To add to systemd `f="pymscada-bus" && cp config/$f.service
|
|
81
|
+
/lib/systemd/system && systemctl enable $f && systemctl start $f`""",
|
|
82
|
+
extra_args=[
|
|
83
|
+
ModuleArgument(
|
|
84
|
+
('--overwrite',),
|
|
85
|
+
{'action': 'store_true', 'default': False,
|
|
86
|
+
'help': 'checkout may overwrite files, CARE!'}
|
|
87
|
+
),
|
|
88
|
+
ModuleArgument(
|
|
89
|
+
('--diff',),
|
|
90
|
+
{'action': 'store_true', 'default': False,
|
|
91
|
+
'help': 'compare default with existing'}
|
|
92
|
+
)
|
|
93
|
+
]
|
|
94
|
+
),
|
|
95
|
+
ModuleDefinition(
|
|
96
|
+
name='accuweatherclient',
|
|
97
|
+
help='poll weather information',
|
|
98
|
+
module_class='pymscada.iodrivers.accuweather:AccuWeatherClient',
|
|
99
|
+
tags=False
|
|
100
|
+
),
|
|
101
|
+
ModuleDefinition(
|
|
102
|
+
name='logixclient',
|
|
103
|
+
help='poll/write to logix devices',
|
|
104
|
+
module_class='pymscada.iodrivers.logix_client:LogixClient',
|
|
105
|
+
tags=False
|
|
106
|
+
),
|
|
107
|
+
ModuleDefinition(
|
|
108
|
+
name='modbusclient',
|
|
109
|
+
help='poll/write modbus devices',
|
|
110
|
+
module_class='pymscada.iodrivers.modbus_client:ModbusClient',
|
|
111
|
+
tags=False
|
|
112
|
+
),
|
|
113
|
+
ModuleDefinition(
|
|
114
|
+
name='modbusserver',
|
|
115
|
+
help='serve modbus devices',
|
|
116
|
+
module_class='pymscada.iodrivers.modbus_server:ModbusServer',
|
|
117
|
+
tags=False
|
|
118
|
+
),
|
|
119
|
+
ModuleDefinition(
|
|
120
|
+
name='openweatherclient',
|
|
121
|
+
help='poll OpenWeather current and forecast data',
|
|
122
|
+
module_class='pymscada.iodrivers.openweather:OpenWeatherClient',
|
|
123
|
+
tags=False
|
|
124
|
+
),
|
|
125
|
+
ModuleDefinition(
|
|
126
|
+
name='pingclient',
|
|
127
|
+
help='ping network devices',
|
|
128
|
+
module_class='pymscada.iodrivers.ping_client:PingClient',
|
|
129
|
+
tags=False
|
|
130
|
+
),
|
|
131
|
+
ModuleDefinition(
|
|
132
|
+
name='snmpclient',
|
|
133
|
+
help='poll SNMP devices',
|
|
134
|
+
module_class='pymscada.iodrivers.snmp_client:SnmpClient',
|
|
135
|
+
tags=False
|
|
136
|
+
),
|
|
137
|
+
ModuleDefinition(
|
|
138
|
+
name='console',
|
|
139
|
+
help='interactive bus console',
|
|
140
|
+
module_class='pymscada.console:Console',
|
|
141
|
+
config=False,
|
|
142
|
+
await_future=False,
|
|
143
|
+
extra_args=[
|
|
144
|
+
ModuleArgument(
|
|
145
|
+
('-p', '--port'),
|
|
146
|
+
{'action': 'store', 'type': int, 'default': 1324,
|
|
147
|
+
'help': 'connect to port (default: 1324)'}
|
|
148
|
+
),
|
|
149
|
+
ModuleArgument(
|
|
150
|
+
('-i', '--ip'),
|
|
151
|
+
{'action': 'store', 'default': 'localhost',
|
|
152
|
+
'help': 'connect to ip address (default: localhost)'}
|
|
153
|
+
)
|
|
154
|
+
]
|
|
155
|
+
),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
class ModuleFactory:
|
|
159
|
+
"""Creates and manages module instances."""
|
|
160
|
+
|
|
161
|
+
def __init__(self):
|
|
162
|
+
self.modules = {m.name: m for m in create_module_registry()}
|
|
163
|
+
|
|
164
|
+
def add_module_parser(self, subparser: argparse._SubParsersAction,
|
|
165
|
+
module_def: ModuleDefinition) -> argparse.ArgumentParser:
|
|
166
|
+
"""Add a parser for a module with its arguments."""
|
|
167
|
+
parser = subparser.add_parser(
|
|
168
|
+
module_def.name,
|
|
169
|
+
help=module_def.help,
|
|
170
|
+
epilog=module_def.epilog
|
|
171
|
+
)
|
|
172
|
+
if module_def.config:
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
'--config',
|
|
175
|
+
metavar='file',
|
|
176
|
+
default=f'{module_def.name}.yaml',
|
|
177
|
+
help=f"Config file, default is '{module_def.name}.yaml'"
|
|
178
|
+
)
|
|
179
|
+
if module_def.tags:
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
'--tags',
|
|
182
|
+
metavar='file',
|
|
183
|
+
default='tags.yaml',
|
|
184
|
+
help="Tags file, default is 'tags.yaml'"
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
'--verbose',
|
|
188
|
+
action='store_true',
|
|
189
|
+
help="Set level to logging.INFO"
|
|
190
|
+
)
|
|
191
|
+
if module_def.extra_args:
|
|
192
|
+
for arg in module_def.extra_args:
|
|
193
|
+
parser.add_argument(*arg.args, **arg.kwargs)
|
|
194
|
+
return parser
|
|
195
|
+
|
|
196
|
+
def create_module(self, module_name: str, options: argparse.Namespace):
|
|
197
|
+
"""Create a module instance based on configuration and options."""
|
|
198
|
+
module_def = self.modules[module_name]
|
|
199
|
+
logging.info(f'Python Mobile SCADA {version("pymscada")} '
|
|
200
|
+
f'starting {module_def.name}')
|
|
201
|
+
# Import the module class only when needed
|
|
202
|
+
if isinstance(module_def.module_class, str):
|
|
203
|
+
module_path, class_name = module_def.module_class.split(':')
|
|
204
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
205
|
+
actual_class = getattr(module, class_name)
|
|
206
|
+
else:
|
|
207
|
+
actual_class = module_def.module_class
|
|
208
|
+
|
|
209
|
+
kwargs = {}
|
|
210
|
+
if module_def.config:
|
|
211
|
+
kwargs.update(Config(options.config))
|
|
212
|
+
if module_def.tags:
|
|
213
|
+
kwargs['tag_info'] = dict(Config(options.tags))
|
|
214
|
+
if module_name == 'console':
|
|
215
|
+
return Console(options.ip, options.port,
|
|
216
|
+
kwargs.get('tag_info',{}))
|
|
217
|
+
return actual_class(**kwargs)
|
pymscada/protocol_constants.py
CHANGED
|
@@ -16,17 +16,17 @@ rarely.
|
|
|
16
16
|
- data size of 8-bit char
|
|
17
17
|
|
|
18
18
|
command
|
|
19
|
-
-
|
|
19
|
+
- CMD.ID data is tagname
|
|
20
20
|
- reply: CMD_ID with tag_id and data as tagname
|
|
21
|
-
-
|
|
21
|
+
- CMD.SET id, data is typed or json packed
|
|
22
22
|
- no reply
|
|
23
|
-
-
|
|
23
|
+
- CMD.UNSUB id
|
|
24
24
|
- no reply
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
25
|
+
- CMD.GET id
|
|
26
|
+
- CMD.RTA id, data is request to author
|
|
27
|
+
- CMD.SUB id
|
|
28
28
|
- reply: SET id and value, value may be None
|
|
29
|
-
-
|
|
29
|
+
- CMD.LIST
|
|
30
30
|
- size == 0x00
|
|
31
31
|
- tags with values newer than time_us
|
|
32
32
|
- size > 0x00
|
|
@@ -34,43 +34,33 @@ command
|
|
|
34
34
|
- text$ matches start of tagname
|
|
35
35
|
- text matches anywhere in tagname
|
|
36
36
|
- reply: LIST data as space separated tagnames
|
|
37
|
-
-
|
|
37
|
+
- CMD.LOG data to logging.warning
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
# Tuning constants
|
|
41
41
|
MAX_LEN = 65535 - 14 # TODO fix server(?) when 3
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
CMD_ID = 1 # query / inform tag ID - data is tagname bytes string
|
|
45
|
-
CMD_SET = 2 # set a tag
|
|
46
|
-
CMD_GET = 3 # get a tag
|
|
47
|
-
CMD_RTA = 4 # request to author
|
|
48
|
-
CMD_SUB = 5 # subscribe to a tag
|
|
49
|
-
CMD_UNSUB = 6 # unsubscribe from a tag
|
|
50
|
-
CMD_LIST = 7 # bus list tags
|
|
51
|
-
CMD_ERR = 8 # action failed
|
|
52
|
-
CMD_LOG = 9 # bus print a logging message
|
|
43
|
+
from enum import IntEnum
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
1
|
|
56
|
-
2
|
|
57
|
-
3
|
|
58
|
-
4
|
|
59
|
-
5
|
|
60
|
-
6
|
|
61
|
-
7
|
|
62
|
-
8
|
|
63
|
-
9
|
|
64
|
-
}
|
|
45
|
+
class COMMAND(IntEnum):
|
|
46
|
+
ID = 1 # query / inform tag ID - data is tagname bytes string
|
|
47
|
+
SET = 2 # set a tag
|
|
48
|
+
GET = 3 # get a tag
|
|
49
|
+
RTA = 4 # request to author
|
|
50
|
+
SUB = 5 # subscribe to a tag
|
|
51
|
+
UNSUB = 6 # unsubscribe from a tag
|
|
52
|
+
LIST = 7 # bus list tags
|
|
53
|
+
ERR = 8 # action failed
|
|
54
|
+
LOG = 9 # bus print a logging message
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
@staticmethod
|
|
57
|
+
def text(cmd) -> str:
|
|
58
|
+
"""Return command text description for enum or int."""
|
|
59
|
+
return COMMAND(cmd).name
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
TYPES = [TYPE_INT, TYPE_FLOAT, TYPE_STR, TYPE_BYTES, TYPE_JSON]
|
|
61
|
+
class TYPE(IntEnum):
|
|
62
|
+
INT = 1 # 64 bit signed integer
|
|
63
|
+
FLOAT = 2 # 64 bit IEEE float
|
|
64
|
+
STR = 3 # string
|
|
65
|
+
BYTES = 4 # raw bytes
|
|
66
|
+
JSON = 5 # JSON encoded data
|