slowpy
, is provided to be imported by
the user task scripts, for:
from slowpy.control import ControlSystem
= ControlSystem()
ctrl
# make a control node for a SCIP command of "MEAS:V0" on a device at 182.168.1.43
= ctrl.ethernet(host='192.168.1.43', port=17674).scpi().command('MEAS:V0')
V0
while True:
# read a value from the control node, with a SCPI command "MEAS:V?"
= V0.get()
value ...
from slowpy.store import DataStore_PostgreSQL
= DataStore_PostgreSQL('postgresql://postgres:postgres@localhost:5432/SlowTestData', table="SlowData")
datastore
while True:
= ...
value ="ch00") datastore.append(value, tag
If you have a User Task Script like this:
def set_V0(value):
set(value) V0.
and write a SlowDash HTML panel like this:
<form>
<input name="value">
V0 value: <input type="submit" name="test.set_V0()" value="Set">
</form>
Then clicking the Set
button will call the function
set_V0()
with a parameter in the value
input
field.
For a control node V0
, and V1
, defining
_export()
function in the User Task Script will export
these node values, making them available in SlowDash GUI in the same way
as the values stored in database.
= ctrl.ethernet(host='192.168.1.43', port=17674).scpi()
device = device.command('MEAS:V0')
V0 = device.command('MEAS:V1')
V1 def _export():
return [
'V0', V0),
('V1', V1)
( ]
Only the “current” values are available in this way. If you need historical values, store the values in a database.
In slowdash/ExampleProjects/SlowTask
there is a slowdash
project to demonstrate some of the features described here.
$ cd slowdash/ExampleProjects/SlowTask
$ slowdash --port=18881
or
$ cd slowdash/ExampleProjects/SlowTask
$ docker compose up
SlowPy is a Python library (module) that provides functions like:
The SlowPy library is included in the SlowDash package, under
slowdash/lib/slowpy
. By running
source slowdash/bin/slowdash-bashrc
, as instructed in the
Installation section, this path will be included in the environmental
variable PYTHONPAH
, so that users can use the library
without modifying users system. It is also possible to install the
library in a usual way: you can do
pip install slowdas/lib/slowpy
to install SlowPy into your
Python environment. You might want to combine this with
venv
not to mess up your Python.
SlowPy provides an unified interface to connect external software
systems and hardware devices; everything will be mapped into a single
“ControlTree” where each node has set()
and
get()
. The tree represents logical structure of the system,
for example, a SCPI command of MEAS:V
to a voltmeter
connected to an ethernet would be addressed like
ControlSystem.ethernet(host, port).scpi().command('MEAS:V')
,
and set(value)
to this node will send a SCPI command of
MEAS:V?
to the voltmeter. The get()
method
makes a read access and returns a value.
Plugin modules can dynamically add branches to the control tree. For
example, Redis plugin adds the redis()
node and a number of
sub-branches for functions that Redis provides, such as hash, json and
time-series. Plugins are loaded to a node, not (necessarily) to the root
ControlSystem; for example, a plugin that uses ethernet is loaded to an
ethernet node, creating a sub-branch under the ethernet node, and the
plugin can make use of the function provided by the ethernet node such
as send()
(which is set()
of the node) and
receive()
(which is get()
).
Here is an example of using SlowPy Controls. In this example, we use a power supply device that accepts the SCPI commands through ethernet.
from slowpy.control import ControlSystem
= ControlSystem()
ctrl
# make a control node for a SCIP command of "MEAS:V0" on a device at 182.168.1.32
= ctrl.ethernet(host='192.168.1.43', port=17674).scpi().command('MEAS:V0', set_format='V0 {};*OPC?')
V0
# write a value to the control node: this will issue a SCPI command "V0 10;*OPC?"
set(10)
V0.
while True:
# read a value from the control node, with a SCPI command "MEAS:V"
= V0.get()
value ...
A common start would be importing the slowpy.control
,
and then creating an instance of the ControlSystem
class.
The ControlSystem
already includes the
Ethernet
plugin, but if it were not, the loading plugin
code would have been:
'Ethernet') ctrl.load_control_module(
This will search for a file named control_Ethernet.py
from search directories, load it, and inject the ethernet()
Python method to the node class that loaded the plugin (which is
ControlSystem in this case). The Ethernet plugin already includes
sub-branches for SCPI; for specific protocols not already included, a
plugin would be loaded onto the Ethernet node.
Each node constructor takes parameters. In this example, the ethernet
node, which sends and receives data to/from the ethernet, takes the
host/IP and port parameters, and a SCPI command node, which is bound to
a specific SCPI command, takes the SCPI command parameter with optional
set_format
which overrides the default SCPI command for
writing. (The default SCPI command to be written/read
.scpi().command(CMD)
are CMD
and
CMD?
, respectively. Often write operations should wait for
the command completion, and SlowPy expects a return value from each
command. A technique to do it, regardless to the actual command
behaviors of the device, is to append OPC?
to the command,
as done in the example.)
Once you get the control node object, you can call
node.set(value)
to write the value, and call
value=node.get()
to read from it. As a shortcut,
node(value)
is equivalent to node.set(value)
,
and value=node()
is equivalent to
value=node.get()
. Also control nodes define Python’s
__str__()
, __float__()
, etc., so
print(node)
and x = float(node)
will
implicitly call node.get()
.
If a control node has a threading method of run()
or
loop()
, calling start()
of the node will
create a dedicated thread and start it. This is useful for:
set()
and get()
queries, such as PID loop.The run()
function is called once, and then
loop()
is called repeatedly. In the loop()
function, sleep()
or similar must be inserted to control
the frequency.
The thread is stopped by calling the stop()
method of
the node, or by a global stop request (such as
ControlSystem.stop()
or by signals). The run()
function must watch the stop request (by
is_stop_requested()
) and terminate itself when a stop
request is made. loop()
will not be called after stop.
Naming convention: set()
, get()
, and
do_XXX()
are usual methods to do something. Methods with a
noun name return a sub-node.
ControlSystem.load_control_module('Ethernet')
cmd value
and waits for a
replycmd?
and waits for a reply, then returns
the replycmd value line_terminator
,
consumes echocmd line_terminator
, consumes echo, and
returns a reply until the next promptControlSystem.load_control_module('HTTP')
base_url
path
with value
as its contentbase_url
path
and return the reply contentvalue
is a Python object to be converted to
JSONControlSystem.load_control_module('Shell')
cmd value
,cmd
and returns the resultadc0 = ctrl.shell('read_adc', '--timeout=0').arg('--ch=0')
ControlSystem.load_control_module('HTTP').load_control_module('Slowdash')
pong
ControlSystem.load_control_module('Redis')
All the control nodes (derived from
slowpy.control.ControlNode
) have the following methods:
get()
condition_lambda
returns True
if the
lambda is not None
; if condition_lambda
is
None
, wait until has_data()
returns
True
. On timeout, wait()
returns
None
, and on stop-request, returns False
.
Otherwise, it returns True
.get()
which calls
parent_node.get()
set()
which calls
parent_node.set()
Nodes derived from ControlValueNode
have the following
methods:
set(value)
of the parent node.get()
set(0)
will stop the current ramping if it
is runningTrue
if rumping is in progress,
otherwise returns False
Nodes derived from ControlThreadNode
have the following
methods:
run()
and/or
loop()
, if the node has one of these methods (or both)Simple example of writing single values to a long form table:
import time
from slowpy.control import RandomWalkDevice
from slowpy.store import DataStore_PostgreSQL
= RandomWalkDevice(n=4)
device = DataStore_PostgreSQL('postgresql://postgres:postgres@localhost:5432/SlowTestData', table="Test")
datastore
while True:
for ch in range(4):
= device.read(ch)
value ='%02d'%ch)
datastore.append(value, tag
1) time.sleep(
Example of writing a dict of key-values:
while True:
= { 'ch%02d'%ch: device.read(ch) for ch in range(4) }
record
datastore.append(record)1) time.sleep(
For SQL databases, the “long format” with UNIX timestamps is used by
default. To use other table schemata, specify an user defined
TableFormat
:
class QuickTourTestDataFormat(LongTableFormat):
= '(datetime DATETIME, timestamp INTEGER, channel STRING, value REAL, PRIMARY KEY(timestamp, channel))'
schema_numeric = '(datetime DATETIME, timestamp INTEGER, channel STRING, value REAL, PRIMARY KEY(timestamp, channel))'
schema_text
def insert_numeric_data(self, cur, timestamp, channel, value):
f'INSERT INTO {self.table} VALUES(CURRENT_TIMESTAMP,%d,?,%f)' % (timestamp, value), (channel,))
cur.execute(
def insert_text_data(self, cur, timestamp, channel, value):
f'INSERT INTO {self.table} VALUES(CURRENT_TIMESTAMP,%d,?,?' % timestamp), (channel, value))
cur.execute(
= DataStore_SQLite('sqlite:///QuickTourTestData.db', table="testdata", table_format=QuickTourTestDataFormat()) datastore
SlowPy provides commonly used data objects such as histograms and graphs. These objects can be directly written to the database using the SlowPy Daabase Interface described above.
import slowpy as slp
= slp.Histogram(100, 0, 10)
hist
= ...
device = ...
datastore
while not ControlSystem.is_stop_requested():
= device.read(...
value
hist.fill(value)="test_hist") data_store.append(hist, tag
data_store.append(hist, tag=name)
will create a
time-series of histograms (one histogram for each time point). To keep
only the latest histogram, use
data_store.update(hist, tag=name)
instead.
import slowpy as slp
= slp.Graph(['channel', 'value'])
graph
while not ControlSystem.is_stop_requested():
for ch in range(n_ch):
= device.read(ch, ...
value
graph.fill(ch, value)="test_graph") data_store.append(graph, tag
data_store.append(graph, tag=name)
will create a
time-series of graphs (one graph for each time point). To keep only the
latest graph, use data_store.update(graph, tag=name)
instead.
import slowpy as slp
= slp.RateTrend(length=300, tick=10)
rate_trend
while not ControlSystem.is_stop_requested():
= device.read(...
value
rate_trend.fill(time.time())
'test_rate')) data_store.append(rate_trend.time_series(
If your device has some programming capability, such as Raspberry-Pi GPIO, an easy way to integrate it into a SlowDash system is to implement the SCPI interface on it. This approach is also useful to integrate other non-ethernet devices (such as USB) connected to a remote computer.
Here is an example to wrap a device with the ethernet-SCPI capability:
from slowpy.control import ScpiServer, ScpiAdapter, RandomWalkDevice
class RandomWalkScpiDevice(ScpiAdapter):
def __init__(self)
super().__init__(idn='RandomWalk')
self.device = RandomWalkDevice(n=2)
def do_command(self, cmd_path, params):
'''
parameters:
cmd_path: array of strings, SCPI command splitted by ':'
params: array of strings, SCPI command parameters splited by ','
return value: reply text (even if empty) or None if command is not recognized
'''
# Implemented Commands: "V0 Value" "V1 Value" "MEASure:V0?" "MEASure:V1?"
# Common SCPI commands, such as "*IDN?", are implemented in the parent ScpiAdapter class
if len(params) == 1 and cmd_path[0].startswith('V'):
try:
if cmd_path[0] == 'V0':
self.device.write(0, float(params[0]))
elif cmd_path[0] == 'V1':
self.device.write(1, float(params[0]))
else:
self.push_error(f'invalid command {cmd_path[0]}')
except:
self.push_error(f'bad parameter value: {params[0]}')
return ''
elif len(cmd_path) == 2 and cmd_path[0].startswith('MEAS'):
if cmd_path[1] == 'V0?':
return self.device.read(0)
elif cmd_path[1] == 'V1?':
return self.device.read(1)
else:
self.push_error(f'invalid command {cmd_path[0]}')
return ''
return None
= RandomWalkScpiDevice()
device = ScpiServer(device, port=17674)
server server.start()
Make this code start automatically on PC boot in your favorite way
(/etc/rc.local
, Docker, …).
if SlowPy Control Nodes are already available for the device, the nodes can be directly mapped to the SCPI interface:
from slowpy.control import ControlSystem, ScpiServer, ScpiAdapter
'DummyDevice')
ControlSystem.import_control_module(= ControlSystem().randomwalk_device()
device
= ScpiAdapter(idn='RandomWalk')
adapter
adapter.bind_nodes(['CONFIGure:WALK', device.walk().setpoint(limits=(0,None))),
('CONFIGure:DECAY', device.decay().setpoint(limits=(0,1))),
('V0', device.ch(0).setpoint()),
('V1', device.ch(1).setpoint()),
('MEASure:V0', device.ch(0).readonly()),
('MEASure:V1', device.ch(1).readonly()),
(
])
= ScpiServer(adapter, port=17674)
server server.start()
SlowTask is a user Python script placed under the SlowDash config
directory with a name like slowtask-XXX.py
. SlowDash GUI
can start/stop the script, call functions defined in the script, and
bind control variables in the script to GUI elements. The SlowPy library
was designed to be used in user task scripts, but this is not a
requirement.
SlowTask script files (such as slowtask-test.py
) are
placed under the project configuration directory. By default, tasks are
“listed” in the Task Manager panel of SlowDash web page, but they will
not be “loaded” until the start
button is clicked.
Auto-loading can be set by making an entry in the SlowDash configuration
file (SlowdashProject.yaml
):
slowdash_project:
...
task:
name: test
auto_load: true
parameters:
default_ramping_speed: 0.1
system:
our_security_is_perfect: false
The parameters are optional, and if given, these will be passed to
the _initialize(params)
function of the script (described
later).
Setting system/our_security_is_peferct
to
true
will enable editing of the Python scripts on the
SlowDash web page. While this is convenient, be aware what it means.
Python scripts can do anything, such as system("rm *")
.
Enable this feature only when the access is strictly controlled. Some
careful systems run processes only in Docker containers, where the
container system runs on a virtual machine (in case the container is
cracked) inside a firewall which accepts only VPN or SSH accesses from
listed addresses. On top of that, in order to prevent unintended
destruction by novice operators, it might be safer to run two instances
of SlowDash, one with full features and one with limited operations
(without or only with selected tasks); the slowdash
command
have an option to specify which configuration file to use.
Functions in SlowTask scripts can be called from SlowDash GUI. If a
script (of name test
) has a function like this
def destroy_apparatus():
#... do your work here
This can be called from SlowDash GUI in several ways.
From a HTML form panel in SlowPlot, clicking a
<button>
with a name of the task module and the
function will call the function:
<form>
<input type="submit" name="test.destroy_apparatus()" value="Finish">
</form>
Here the parenthesis at the and of the button name indicate that this button is bound to a SlowTask function.
SlowTask functions can take parameters:
def set_V0(voltage:float, ramping:float):
#... do your work here
and these parameter values are taken from the form input values:
<form>
<input type="number" name="voltage" value="0"><br>
Voltage: <input type="number" name="ramping" value="1"><br>
Ramping: <input type="submit" name="test.set_V0()" value="Set">
</form>
By using the type notation in the SlowTask function as shown above, the parameter values are converted to the specified types. If the value literal cannot be converted to the specified type, an error response will be returned to the browser and the SlowTask function will not be called.
The SlowTask function can be either the standard def
or
async def
.
It is possible to place multiple buttons in one form:
<form>
<input type="number" name="ramping" value="1" style="width:5em">/sec
Ramping: <p>
<input type="number" name="V0" value="0"><input type="submit" name="parallel test.set_V0()" value="Set"><br>
V0: <input type="number" name="V1" value="0"><input type="submit" name="parallel test.set_V1()" value="Set"><br>
V1: <input type="number" name="V2" value="0"><input type="submit" name="parallel test.set_V2()" value="Set"><br>
V2: <input type="number" name="V3" value="0"><input type="submit" name="parallel test.set_V3()" value="Set"><br>
V3: <p>
<input type="submit" name="test.set_all()" value="Set All">
<input type="submit" name="parallel test.stop()" value="Stop Ramping"><br>
</form>
def set_V0(V0:float, ramping:float):
#...
def set_V1(V1:float, ramping:float):
#...
#...
The input values on the Web form will be bound to the function
parameters based on the names (<input name="V0">
will
be bound to the V0
parameter).
In the example above, some functions have the parallel
qualifier: by default, if a previous function call is in execution, a
next action cannot be accepted to avoid multi-threading issues in the
user code. The parallel
qualifier indicates that this
function can run in parallel to others. Another common qualifier is
await
, which instructs the web browser to wait for
completion of the function execution before doing any other things
(therefore the browser will look frozen).
On a canvas panel, a button to call a task can be placed by:
{
...
"items": [
{
"type": "button",
"attr": { "x":400, "y": 630, "width": 130, "label": "Finish Experiment" },
"action": { "submit": { "name": "test.destroy_apparatus()" } }
},
...
For functions with parameters, a form can be used:
{
...
"items": [
{
"type": "valve",
"attr": { ... },
"metric": { "channel": "sccm.GasInlet", ... },
"action": { "form": "FlowControl" }
},
...
],
"forms": {
"FlowControl": {
"title": " Injection Flow",
"inputs": [
{ "name": "flow", "label": "Set-point (sccm)", "type": "number", "initial": 15, "step": 0.1 }
],
"buttons": [
{ "name": "ATDS.set_flow()", "label": "Apply Set-point" },
{ "name": "ATDS.stop_flow()", "label": "Close Valve" }
]
}
}
...
For more details, see the UI Panels section.
If a SlowTask script has functions of
_initialize(params)
, _finalize()
,
_run()
, and/or _loop()
, one dedicated thread
will be created and these function are called in a sequence:
function | description |
---|---|
_initialize(params) |
called once when the script is loaded. The params
values are given by the SlowDash configuration. |
_run() |
called once after _initialize() . If _run()
is defined, _halt() should be also defined to stop the
_run() function. _halt() will be called by
SlowDash when the user stops the system. |
_loop() |
called repeatedly after _initialize() , until the user
stops the system. To control the intervals, the function usually
contains time.sleep() or equivalent. |
_finalize() |
called once after _run() or _loop() |
These functions can be either the standard def
or
async def
.
If _export()
is defined in the SlowTask script, control
variables listed in its return value become accessible by other SlowDash
components (typically web browsers), in a very simlar way as the data
stored in data sources (typically databases), except that only the
“curent” values are available.
def _export():
return [
'V0', V0),
('V1', V1),
('V2', V2),
('V3', V3)
( ]
To export a variable that is not a control node, make a temporary control node variable to wrap it:
class RampingStatusNode(ControlNode):
def get(self):
return {
'columns': [ 'Channel', 'Value', 'Ramping' ],
'table': [
'Ch0', float(V0), 'Yes' if V0.ramping().status().get() else 'No' ],
[ 'Ch1', float(V1), 'Yes' if V1.ramping().status().get() else 'No' ],
[ 'Ch2', float(V2), 'Yes' if V2.ramping().status().get() else 'No' ],
[ 'Ch3', float(V3), 'Yes' if V3.ramping().status().get() else 'No' ]
[
]
}
def _export():
return [
'V0', V0),
('V1', V1),
('V2', V2),
('V3', V3),
('Status', StatusNode())
( ]
If an instance of ValueNode
(which typically holds a
value but not associated to any external device or object) is exported,
it can push the value to receivers (typically web browsers).
import asyncio
from slowpy.control import ValueNode
= ValueNode(initial_value=0)
x
async def _loop():
= V0.get()
x await x.deliver()
await asyncio.sleep(1)
The deliver()
method calls .get()
and
publish the value to receivers. As deliver()
is an async
function, it must be used in an async def
and the return
value must be await
ed.
The exported variables can be “bound” in browser. If a SlowTask
control variable is bound in SlowDash HTML, changes to the variable on
the browser calls the .set()
method of the SlowTask
variable.
<form>
<input type="range" min="0.1" max="9.9" step="0.1" sd-value="scope.fx" sd-live="true">
<span sd-value="scope.fx" style="font-size:150%"></span>
...</form>
# slowtask-scope.py
= ValueNode(initial_value=0)
fx
def _export():
return [ ('fx', fx) ]
...
In HTML, sd-live="true"
indicates that changes of the
value on the browser will trigger calling the .set()
method
of the bound variable.
An example can be found in
ExampleProjects/Streaming
.