Shared tag value SCADA with python backup and Angular UI
Project description
pymscada
Docs
@Github
Python Mobile SCADA
This is an open source Python SCADA package intended for use over your
mobile phone. It has been developed to connect custom python scripts to
Modicon and Rockwell PLCs, and present a web based user interface, with
minimal coding. pymscada
does expect you to be able to use python.
pymscada
shares values through a Tag
for all data. Tag values
are updated by exception through a message bus. Changes are in any
direction with simple loops stopped with a simplistic don't send updates
back to where they came from check. It's too simple to stop you defeating
it (but there are easier ways to break the code).
There are primary owner Tag's. A request to set command is passed to the author of the Tag, where it is updated. This allows database information to pass through the Tag values as well. Tag values are typically float or int however they may also be megabyte sized binary or dictionary values.
Example systemd
service files, pymscada module config files and
custom script examples are included. The example scripts include polling
weather from tommorrow.io and a python script based Modbus TCP PLC.
Introduction
This is a small SCADA package that will run on Linux (preferably) or Windows. The server runs as several modules on the host, sharing information through a message bus. A subset of modules is:
- Bus server - shares tag values with by exception updates
- Modbus client - reads and writes to a PLC using Modbus/TCP
- Logix client - uses
pycomm3
to read / write to Rockwell PLCs - SNMP client - polls SNMP OID values
- History - saves data changes, serves history to web pages
- Web server - serves web pages which connect with a web socket
- Web pages - procedurally generated setpoint, indication and trends
Objectives
Traditional SCADA has a fixed 19:6, 1920x1080 or some equivalent layout. It's great on a big screen but not good on a phone. Hence Mobile SCADA with a responsive layout.
I wrote Mobile SCADA to provide a GUI to the other things I was trying to do, I wanted to leverage web browsers and eliminate a dedicated viewer.exe. Display on the client is fast, trends, as fast as I can make them.
Uptimes should be excellent. The best I have on an earlier version is over 5 years for about half of the script modules. This version is a complete rewrite, however the aim is the same.
All tag value updates are by exception. So an update from you setting a value to seeing the feedback should be FAST.
See also
- The angular project angmscada
- Python container for the compiled angular pages pymscada-html
- An add-on IO driver for Rockwell using pycomm3 pymscada-pycomm3
Licence
pymscada
is distributed under the GPLv3 license.
Example Use
This was all run on a Raspberry Pi 3B+ with a 16GB SDRAM card.
First
Checkout the example files. Start in an empty directory. Plan to keep in the directory you check out into as the config file path details are auto-generated for the location you check out in to.
mscada@raspberrypi:~/test $ pymscada checkout
making 'history' folder
making pdf dir
making config dir
Creating /home/mscada/test/config/modbusclient.yaml
Creating /home/mscada/test/config/pymscada-history.service
Creating /home/mscada/test/config/wwwserver.yaml
Creating /home/mscada/test/config/pymscada-demo-modbus_plc.service
Creating /home/mscada/test/config/files.yaml
Creating /home/mscada/test/config/pymscada-modbusserver.service
Creating /home/mscada/test/config/pymscada-wwwserver.service
Creating /home/mscada/test/config/simulate.yaml
Creating /home/mscada/test/config/tags.yaml
Creating /home/mscada/test/config/history.yaml
Creating /home/mscada/test/config/pymscada-files.service
Creating /home/mscada/test/config/bus.yaml
Creating /home/mscada/test/config/modbusserver.yaml
Creating /home/mscada/test/config/modbus_plc.py
Creating /home/mscada/test/config/pymscada-modbusclient.service
Creating /home/mscada/test/config/pymscada-bus.service
Creating /home/mscada/test/config/README.md
mscada@raspberrypi:~/test $
Objective
To show a trend of the temperature forecast with a custom pymscada bus client program. The end result should look like ...
Configuration
Bus
Defaults in bus.yaml
are fine.
Tags
Add some tags in tags.yaml
:
temperature:
desc: temperature
type: float
min: 0
max: 35
units: C
dp: 1
temperature_01:
desc: temperature_01
type: float
min: 0
max: 35
units: C
dp: 1
... etc.
History
Defaults in history.yaml
are fine.
Web Server
You will need to add a trend page to wwwserver.yaml
as:
- name: Temperature # Creates a Temperature page in the web client
parent: Weather # Add the Temperature page in a submenu under Weather
items:
- type: uplot # Identify the Angular component to use
ms:
desc: Temperature
age: 172800
legend_pos: left
time_pos: left
time_res: m
axes:
- scale: x
range: [-604800, 86400] # initial time range for the trend
- scale: 'C'
range: [0.0, 35.0]
dp: 1
series:
- tagname: temperature # pymscada Tag name
label: Current Temperature
scale: 'C'
color: black # standard html colour names
width: 2
dp: 1 # number of decimal places
... etc for additional series
Your custom pymscada Module
For this example I polled tomorrow.io
weather.py
from datetime import datetime
import time
from pymscada import BusClient, Periodic, Tag
URL = 'https://api.tomorrow.io/v4/timelines'
QUERY = {'location': '-43.527934570040124, 172.6415203551829',
'fields': ['temperature'],
'units': 'metric',
'timesteps': '1h',
'startTime': 'now',
'endTime': 'nowPlus24h',
'apikey': '<your key>'}
class PollWeather():
def __init__(self):
self.tags = {}
for tagname in ['temperature', 'temperature_01', 'temperature_04',
'temperature_12', 'temperature_24']:
# Create pymscada tags, tags are singletons by 'tagname'
self.tags[tagname] = Tag(tagname, float)
async def periodic(self):
now = int(time.time())
if now % 3600 != 120:
return
# Get the weather forecast from tomorrow.io
async with aiohttp.ClientSession() as session:
async with session.get(URL, params=QUERY) as resp:
response = await resp.json()
utc_now = None
for row in response['data']['timelines'][0]['intervals']:
convert = row['startTime'].replace('Z', '+0000')
utc = datetime.strptime(convert, '%Y-%m-%dT%H:%M:%S%z').timestamp()
if utc_now is None:
utc_now = utc
forecast = ''
else:
forecast = f'_{int((utc - utc_now) / 3600):02d}'
if forecast not in ['', '_01', '_04', '_12', '_24']:
continue
for k, v in row['values'].items():
ftag = k + forecast
value = float(v)
time_us = int(utc * 1000000)
if ftag in self.tags:
# Write the tag value. This is one of the following:
# - value # time_us, bus_id auto-generated
# - value, time_us # bus_id auto-generated
# - value, time_us, bus_id # Don't use this one
self.tags[ftag].value = value, time_us
async def main():
# Connect to the bus and poll the weather service.
bus = BusClient()
await bus.start()
weather = PollWeather() # demo function
periodic = Periodic(weather.periodic, 1.0) # part of pymscada
await periodic.start()
await asyncio.get_event_loop().create_future() # run forever
if __name__ == '__main__':
asyncio.run(main())
Run the modules
You can run the modules in one of: individual terminals, nohup ... &
or as a
systemd
service. I run as a service, the snips are abbreviated (no path) from
the exec line in the auto-generated service files.
Run the bus first! This needs to remain running all the time. It does not need to know the tagnames in advance so it can run forever for most tests. It will gather dead tagnames over time as you are experimenting, however this only requires a small amount of memory (unless you are setting tag values in the MB - which does work).
pymscada bus --config bus.yaml
pymscada wwwserver --config wwwserver.yaml --tags tags.yaml
pymscada history --config history.yaml --tags tags.yaml
python weather.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for pymscada-0.1.0a1-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 61e09969044e01a189a3e5386a9ddf474847820848219cc8c750d3e22b9413ab |
|
MD5 | 5516de9e0edebf4842a78114d6ab3940 |
|
BLAKE2b-256 | 2fe853fa5e3fa016ef749b87ef46f51dade1fc9f6ea19b23638945f015c7b619 |