a python daemon
-
This is a Duet Software Framework (DSF) plugin that duplicates similar behaviour to 'daemon.g', except that it runs python code (python 3) on an attached Raspberry Pi when the printer is running SBC mode. The python code has access to a recently-updated copy of the machine object model, and can set the interval to the next time the code runs.
A python3 DSF routine runs every 'loop interval' seconds, looks for a specified file in a specified location (by default daemon.py in the printer sys folder, which is at /opt/dsf/sd/sys/ on the Pi - these can be changed but note that the filename must end in '.py'). If the file exists, it runs the function 'loop()' (if it exists) from within that file each interval. If the file has changed (or appeared) since the last check, the function 'setup()' (if it exists) from the file is run first immediately before running loop().
That is, the daemon.py file functions are somewhat like the functions in an arduino sketch - setup() is run once, and then loop() is run repeatedly. However, in this case, loop() is run only periodically, as defined by the 'loop interval'.
Note that the functions run 'on the dot', so if the interval is set to 60 (i.e. 60 seconds, i.e. 1 minute) loop() runs every minute on the minute, whether the function takes milliseconds to complete, or 59 seconds to complete. However, it runs on the next occurrence of the time interval, so if the interval is set to three seconds, but the function takes four seconds to run, it will actually be run every six seconds: starts at ...0, finishes at ...4, the next multiple of three is ...6 so it runs then and finishes at ..10, the next multiple is ..12 so it runs then and finishes at ..16 and runs again at ..18, etc.
Both functions are called with a dict-of-dicts-of-... parameter that contains the entire object model at the current time (though it can be up to about 2 seconds out-of-date). Thus, within the python functions, object model elements can be accessed with e.g.
print(object_model['heat']['heaters'][1]['active'])
(assuming the parameter has been named 'object_model' in the function definition).Both functions can change the current loop interval setting by returning a value that can be interpreted as numeric and greater than zero. It can be fractional, but values less than about a second are not recommended.
The whole daemon.py file is (re-)imported each time the routine notices it having changed, so whatever is outside any functions will run once at import (before setup() is run) so can include imports and global variable definitions.
Thus, for example, daemon.py could be:
# example daemon.py file # # setup() is called once, with dict-of-dicts of object model as sole parameter # if this function returns a value that can be interpreted as a number > zero # the loop interval will be set to that value (seconds) def setup(om): global tlog tlog=open('/opt/dsf/sd/sys/tempslog.csv','w') print (om['boards'][0]['uniqueId'], file=tlog) return(30) # loop() is called periodically, with dict-of-dicts of object model as sole parameter # # at each next occurrence of a multiple of the interval since the epoch, # the object model dict is updated and then this function is run # # if this function returns a value that can be interpreted as a number > zero # the loop interval will be changed to that value def loop(om): global tlog print(om['state']['time'], end='', file=tlog) for h in om['heat']['heaters']: print (','+str(h['current']), end='', file=tlog) print(file=tlog, flush=True) if om['heat']['heaters'][1]['active'] > 0: return(3) else: return(30)
This will create a 'tempslog.csv' file in the printer sys folder, and log the time and the temperature of each heater into it in CSV format. If heater 1 is set to a target value greater than zero, the log will update every three seconds, otherwise it will update every 30 seconds. If you make any change to daemon.py (including e.g. simply
touch daemon.py
at a command prompt on the Pi) the plugin will notice a change to the file, and re-run setup(), creating a new tempslog.csv and obliterating the previous one. You can also simply rename daemon.py to something else to stop it running, and name it back to daemon.py when you want the logging to run, or stop and start the plugin.(If you didn't want 'obliterating the previous one', it would be a case of changing the file open command in setup() to
tlog=open('/opt/dsf/sd/sys/tempslog.csv','a')
so you append to the .csv file rather than write to it.)Multiple loop functions can be defined, with names in the format loop<nn> where <nn> is an arbtrary integer, e.g.
def loop13(om):
. These will run every nth loop (e.g. every 13th loop). At each interval the bare loop() function runs first, and then each relevant loopn() function is run in the order that they are found in the daemon.py file. For example, to add a line of headers repeated every 20 lines of data, you could add to the example above:def loop20(om): global tlog print ('"time h0 h1 h2"', file=tlog)
Although loop() runs before any loopn() function, you can circumvent that by defining loop() as just
pass
and then defining e.g. a loop20() earlier in the daemon.py file than a loop1(). On most increments loop() will go first and do nothing then loop1() will run, but on every twentieth increment loop() will run and do nothing then loop20() and finally loop1() will run.As a further variant on this, you can name a loopn() function beyond the number, so two different functions that both run every 20 intervals could be named loop20a() and loop20b() (or loop20tweedledum() and loop20tweedldee(), or whatever).
All these additional loopn() functions also need to take a single parameter (which will be the object model) and can change the interval timer by returning a numeric value.
As implied above, the daemon.py file can be removed and replaced or changed at will and the plugin should handle it gracefully. It will fail and the plugin will stop running however if while editing the file it's saved in an intermediate non-python-grammatical state at the moment the plugin tries to run it.
To see messages relating to the plugin run
journalctl -u duetpluginservice -f
at a command prompt on the Pi.The logged messages can be greatly increased by setting 'verbose' and 'vTS' to True in the top of file /opt/dsf/plugins/PyDaemon/dsf/pydaemon.py on the Pi. 'vTS' adds a microsecond-resolution timestamp to each logged comment.
It's not very extensively tested.
To install:
You need dsf-python installed. At a command prompt on the Pi:
sudo pip3 install --break-system-packages --pre dsf-python
. It will moan about how you should be using a virtual environment instead, but should work. I haven't done any testing with virtual environments. Note the '--pre' is because dsf-python is currently pre-release for 3.5 firmware.Otherwise I don't think it needs any very non-standard modules - it uses json, time, sys, pathlib, importlib, re and inspect, which are probably all in by default (but I can't swear to that). If something complains you might need to install the relevant module.
You should read the source to ensure it's not doing anything nefarious. It's about 200 lines of pure python (and about 100 lines of comments).
To install the plugin drop the zip onto the 'Install Plugin' button in Plugins / external plugins in DWC and click the Start button. Then (or previously) create your daemon.py file in the printer sys folder (on the Pi: /opt/dsf/sd/sys)
As usual, I warrant nothing. It's not even fit for any purpose.
You'll need to remove the .txt, this is just me circumventing the forum systems: download: PyDaemon.zip.txt