Duet3D Logo Duet3D
    • Tags
    • Documentation
    • Order
    • Register
    • Login

    a python daemon

    Scheduled Pinned Locked Moved
    Plugins for DWC and DSF
    1
    2
    235
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • achrnundefined
      achrn
      last edited by achrn

      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

      achrnundefined 1 Reply Last reply Reply Quote 5
      • achrnundefined
        achrn @achrn
        last edited by achrn

        Updated for 3.6 (tested on 3.6.0rc1): PyDaemon.zip.txt (as usual you will need to take the .txt off the end of this file - I'm circumventing the forum restrictions).

        The only change is to renumber it to a 3.6 version and set the requisites to 3.6.

        Installation

        The 3.6.0RC1 package repository does not have 3.6.0 DSF (at time of writing), and the 3.5.1 DSF one does not work with 3.6.0, so you need to add DSF manually. The 3.6.0 RC1 version is on pypi: https://pypi.org/project/dsf-python/3.6.0rc1/ so sudo pip3 install --break-system-packages dsf-python==3.6.0rc1 should install it.

        python3-dsf-python (3.6.0~rc1-1) is in the repository now so can just sudo apt install python3-dsf-python (or apt upgrade) it.

        Otherwise it's install in the usual manner for plugins - drop the zip onto the 'Install Plugin' button in Plugins in DWC and click the Start button (after checking the code is not going to do you harm). Refer to the first post in this thread for discussion of what goes in your daemon.py file.

        1 Reply Last reply Reply Quote 2
        • First post
          Last post
        Unless otherwise noted, all forum content is licensed under CC-BY-SA