Home

Awesome

serialOM.py

A RepRapFirmware ObjectModel serial access tool for Python3 and microPython

serialOM implements a fetch and update cycle using M409 commands to query the ObjectModel on the controller, the responses are gathered and merged into a local Dictionary structure. That can be accessed to drive displays, loggers and more.

Requirements:

Either: CPython 3.7+

Or: microPython 1.21+

A Duet/RRF controller to talk to,

Overview:

serialOM() takes a 'serial' object at init, and a dictionary with the OM keys to gather for each machine mode.

It returns a serialOM object, with the model property populated with the requested keys (and the 'state' key)

A bare bones example of serialOM can be as simple as:

from serialOM import serialOM
from serial import Serial

rrf = Serial('/dev/ttyACM0',57600)
OM  = serialOM(rrf, {'FFF':[],'CNC':[],'Laser':[]}, quiet=True)
print('state: ' + OM.model['state']['status']
     + ', up: ' + str(OM.model['state']['upTime']) + 's')

This will quietly connect and display the current machine state and uptime. Try setting quiet=False in the OM = serialOM() init arguments to see a lot more detail of the connecction progress.

If serialOM times out or fails to detect a RRF controller during initialisation OM.machineMode will be empty (''), otherwise it will reflect the controller mode; currently 'FFF', 'CNC' or 'Laser'.

The provided 'miniDemo.py' script is more detailed and shows the use of the OM.update() and OM.getResponse() methods.

Blocking:

When being initialised, updated or making requests serialOM is blocking, it implements it's own request timeouts and will return if the connected device times out. This 'per request' timeout can be passed at init(). During update()s serialOM will make 2 requests minimum, plus one request per additional OM key. The maximum blocking period is the sum total of these, plus processing time. During init it may be longer due to the firmware check cycle.

The Serial() device neeeds to have it's own blocking timeouts set lower than the Request timeout. This is done during init by serialOM itself and does not need to be specified when creating PySerial or UART objects.

Exceptions:

serialOM catches all exceptions coming from serial devices during read and write operations and will raise it's own serialOMError exception in response, with the original exception in the body. This allows the calling script to retry/re-initialise the connection as needed (handy for USB serial which disconnects when the controller reboots).

Comminication error tolerant:

If a timeout happens when waiting for a response, or a garbled response is recieved the update() method will return False, and the specific reason shown (unless in quiet mode). If this happens during init() the machineMode will be empty when init returns.

It is up to the calling program to track failures and determine when communications have been 'lost'. Unless an exception occurs (see above) serialOM will blindly continue sending requests and looking for replies when called.

Use

Init:

Import serialOM:

from serialOM import serialOM

And create an instance of it with:

OM = serialOM(rrf, omKeys, requestTimeout=500, rawLog=None, quiet=False, noCheck=False)

where:

rrf            = a pyserial.Serial or machine.UART object; or similar
omKeys         = per-mode lists of keys to sync, (dict, see below)
                 omKeys = {'machineMode':['OMkey1','OMkey2',..],etc..}
                          Empty lists [] are allowed.
                          At least one machineMode must be specified.
rawLog         = raw log, or None (writable file object, default None)
quiet          = Suppress info messages (bool, default False)
noCheck        = Skip M115 firmware check during init (bool, default False)

If the initial connection and update are successful the property OM.machineMode will be populated, otherwise it will return an empty string.

The fetched ObjectModel is returned in the OM.model property as a dictionary of keys that match the keys obtained from the controller.

Methods and Properties:

The principle method is

OM.update()

This initiates a refresh and update of the model property from the controller. Returns True for success, False if timeouts occurred.

OM.update() deals gracefully with machineMode changes and upTime rollbacks (controller reboots); refreshing the entire model and (re)setting OM.machineMode as needed.

The OM.model property contains the fetched model as a dictionary.

OM.machineMode will be set to the machine mode, or an empty string if not connected.

There are two further methods provided by serialOM for convenience:

serialOM.sendGcode('code')

Sends the specified code to the controller, has no return value.

serialOM.getResponse('code',json)

Sends code and waits for a response, if json is true and it sees a line beginning with { and ending with } it will return immediately with that as a single list item. Otherwise it waits for the requestTimeout and returns a list of all recieved lines. Conforms to the request timeout as described above and returns an empty list if no valid response recieved in time.

Operation:

serialOM Implements a RRF ObjectModel fetch and update cycle based on using M409 commands to query the ObjectModel on the controller, the responses are gathered and merged into a local Dictionary structure.

For CPython serialOM requires pyserial, or a compatible 'serial()' object.

There is no reason why this would not run on Windows, but I have not tried that. You will need a viable Python 3.7+ install with pyserial, and change the device path to the windows 'COM' equivalent.

Notes:

Written in CPython; but I am trying to keep all the logic and data handling simple and low-memory for porting to microPython.

printPy.py for CPython:

For microPython see the equivalent 'printMPy.py' in the printMPy folder and it's README

serialOM comes with a full implementation of a datalogging script in the printPy folder.

This uses the features above to implement a robust data gathering loop. This loop calls an independent output class to process and display the data being gathered.

outputXXX.py class

In the printPy demo this is a text implementation of the class which logs to the console, and optionally to a log file with timestamps.

See the full description at printPy/README.md

Here is an example of starting printPy.py with a 10s update time late into a very small and fast print (it's PC-ABS, hence the temps.)

$ python printPy 10000
printPy.py is starting at: 2024-2-29 01:02:54 (device localtime)
starting output
device "/dev/ttyACM0" available
connected to: /dev/ttyACM0 @57600
serialOM is starting
checking for connected RRF controller
> M115
>> FIRMWARE_NAME: RepRapFirmware for Duet 2 WiFi/Ethernet FIRMWARE_VERSION: 3.4.6 ELECTRONICS: Duet WiFi 1.02 or later FIRMWARE_DATE: 2023-07-21 14:08:28
>> ok
controller is connected
making initial data set request
connected to ObjectModel
info: RepRapFirmware for Duet 2 WiFi/Ethernet v3.4.6 on Duet 2 WiFi
      Controller is in "FFF" mode
      Vin: 23.8V | mcu: 41.0C
status: processing | uptime: 8:45:57 | wifi: 10.0.0.30 | progress: 88.5% | bed: 100.1 (100.0) | e0: 280.2 (280.0) | message: Template_For_Diagrams.gcode
status: processing | uptime: 8:46:07 | wifi: 10.0.0.30 | progress: 92.5% | bed: 100.0 (100.0) | e0: 279.5 (280.0) | message: Template_For_Diagrams.gcode
status: processing | uptime: 8:46:17 | wifi: 10.0.0.30 | progress: 94.4% | bed: 100.0 (100.0) | e0: 280.2 (280.0) | message: Template_For_Diagrams.gcode
status: processing | uptime: 8:46:27 | wifi: 10.0.0.30 | progress: 95.9% | bed: 100.0 (0.0) | e0: 280.4 (0.0)
status: processing | uptime: 8:46:37 | wifi: 10.0.0.30 | progress: 95.9% | bed: 98.5 (0.0) | e0: 276.9 (0.0)
status: idle | uptime: 8:46:47 | wifi: 10.0.0.30 | bed: 96.8 (0.0) | e0: 268.2 (0.0)
status: idle | uptime: 8:46:57 | wifi: 10.0.0.30 | bed: 95.2 (0.0) | e0: 258.9 (0.0)