In Slow-Dash projects, user Python module can be used for:
SlowTask is an extension of the user module, and it should be good for most simple cases. User module is provided for full flexibility beyond SlowTask.
slowdash_project:
name: ...
module:
file: FILE_PATH
parameters:
KEY1: VALUE1
...
data_suffix: SUFFIX
cgi_enabled: False
[TODO] implement SUFFIX
By default, user modules are not enabled if the server program is launched by CGI. To enable this, set the cgi_enabled
parameter True
. Be careful for all the side effects, including performance overhead and security issues. As multiple user modules can be loaded in parallel, splitting functions to a CGI-enabled module and disabled one might be a good strategy.
mymodule.py
at the user project directory will be loaded to slow-dash.mymodule.py
will be called for various context.### Called when this module is loaded. The params are the parameters in the configuration file.
def _initialize(params):
...
### Called during termination of slow-dash.
def _finalize():
...
### Called when web clients need a list of available channels.
# Return a list of channel struct, e.g., [ { "name": NAME1, "type": TYPE1 }, ... ]
def get_channels():
...
return []
### Called when web clients request data.
# If the channel is not known, return None
# else return a JSON object of the data, in the format described in the Data Model document.
def _get_data(channel):
...
return None
### Called when web clients send a command.
# If command is not recognized, return None
# elif command is executed successfully, return True
# else return False or { "status": "error", "message": ... }
def _process_command(doc):
...
return None
### Called periodically while the system is running
# If this function is defined, a dedicated thread is created for that.
# Do not forget to insert a sleep otherwise the system load becomes unnecessarily large.
def _loop():
...
time.sleep(0.1)
### Instead of loop(), a lower level implementation with run() and halt() can also be used.
# run() is called as a thread after initialize(), and halt() is called before finalize().
is_stop_requested = False
def _run():
global is_stop_requested
while not is_stop_requested:
....
time.sleep(0.1)
def _halt():
global is_stop_requested
is_stop_requested = True
[TODO] implement the full data-source interface
slowdash_project:
name: WorldClock
module:
file: worldclock.py
parameters:
timeoffset: -9
data_suffix: worldclock
# worldclock.py
import time, datetime
timeoffset = 0
def _initialize(params):
global timeoffset
timeoffset = params.get('timeoffset', 0)
def _get_channels():
return [
{'name': 'WorldClock', 'type': 'tree'},
]
def _get_data(channel):
if channel == 'WorldClock':
t = time.time()
dt = datetime.datetime.fromtimestamp(t)
return { 'tree': {
'UnixWorldClock': t,
'UTC': dt.astimezone(datetime.timezone.utc).isoformat(),
'Local': dt.astimezone().isoformat(),
'%+dh'%timeoffset: dt.astimezone(tz).isoformat()
}}
return None
# for testing
if __name__ == '__main__':
print(_get_data(get_channels()[0]['name']))
Running the slowdash
command without a port number option shows the query result to screen. The query string is given as the first argument.
Two queries are useful to test the module:
channel
: query for a channel listdata/CHANNEL
: query for data for the channel$ slowdash channels
[{ "name": "WorldClock", "type": "tree" }]
$ slowdash data/WorldClock
{ "WorldClock": { "start": 1678801863.0, "length": 3600.0, "t": 1678805463.0, "x": { "tree": {
"UnixTime": 1678805463.7652955,
"UTC": "2023-03-14T14:51:03.765296+00:00",
"Local": "2023-03-14T15:51:03.765296+01:00",
"-9h": "2023-03-14T05:51:03.765296-09:00"
}}}}
(the output above is reformatted for better readability)
To the example user data source above, add the following function:
def _process_command(doc):
global timeoffset
try:
if doc.get('set_time_offset', False):
timeoffset = int(doc.get('time_offset', None))
return True
except Exception as e:
return { "status": "error", "message": str(e) }
return False
Make a HTML form to send commands from Web browser:
<form>
Time Offset (hours): <input type="number" name="time_offset" value="0">
<input type="submit" name="set_time_offset" value="Set">
</form>
Save the file at the config
directory under the user project direcotry. Add a new HTML panel with HTML file WorldClock
.
When the Set
button is clicked, the form data is sent to the user module as a JSON document. On the terminal screen where the slowdash command is running, you can see a message like:
POST: /slowdash.cgi/control
23-03-14 16:37:46 INFO: DISPATCH: {'set_time_offset': True, 'time_offset': '3'}
A dedicated thread is created for each user module, and the module is loaded within the thread. Therefore, all the statements outside a function will be executed in this thread at the time of module loading, followed by _initialize()
.
If the _loop()
function is defined in a user module, the function is called periodically within the user module thread:
If the _run()
function is defined, a dedicated thread is created and the function will be started immediately after _initialize()
. When _run()
is used, a terminator function, _halt()
should also be defined in the user module to stop the thread. The _halt()
function is called just before _finalize()
. A typical construction of _run()
and _halt()
looks like:
is_stop_requested = False
def _run():
global is_stop_requested
while not is_stop_requested:
do_my_task()
time.sleep(0.1)
def _halt():
global is_stop_requested
is_stop_requested = True
If both _run()
and loop()
are defined, run()
is called first (after _initialize()
), followed by loop()
and finalize()
.
All the other callback functions, such as _process_command()
, _get_channels()
, and _get_data()
, are called from the main Slowdash thread (not the user module thread) and therefore these can be called concurrently with the user thread callbacks (_initialize()
, _loop()
, _run()
, etc.). It is okay to start another thread in user modules, as done in SlowTask which creates a dedicated thread for each _process_command()
call.
To print debug messages from user modules, use the logging module:
import logging
logger = logging.getLogger(name)
logger.addHandler(logging.StreamHandler(sys.stderr))
logger.setLevel(logging.INFO)
def _process_command(doc):
logger.info(doc)
...
To avoid using a number of “global” variables, consider making a class to handle user tasks and using the user module interface functions for simply forwarding the messages.
class MyTask:
....
my_task = MyTask()
def _loop():
my_task.do()
time.sleep(0.1)
def _process_command(doc):
return my_task.process_command(doc)
It is often convenient to have the user module executable standalone.
For contineous execution, signal might be used to stop the thread: