{"id":21856821,"url":"https://github.com/easytarget/serialom","last_synced_at":"2026-02-26T18:31:16.828Z","repository":{"id":225033472,"uuid":"764861240","full_name":"easytarget/serialOM","owner":"easytarget","description":"*python RRF serial ObjectModel library and tool","archived":false,"fork":false,"pushed_at":"2024-07-25T10:52:31.000Z","size":188,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-21T00:42:07.514Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc0-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/easytarget.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-02-28T21:00:27.000Z","updated_at":"2024-07-25T10:45:45.000Z","dependencies_parsed_at":"2024-03-12T00:35:35.860Z","dependency_job_id":"c22d755e-3255-43c2-aead-0697e99412bd","html_url":"https://github.com/easytarget/serialOM","commit_stats":null,"previous_names":["easytarget/serialom"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/easytarget/serialOM","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FserialOM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FserialOM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FserialOM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FserialOM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/easytarget","download_url":"https://codeload.github.com/easytarget/serialOM/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FserialOM/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29867533,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-26T18:27:06.972Z","status":"ssl_error","status_checked_at":"2026-02-26T18:26:57.848Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-28T02:23:38.415Z","updated_at":"2026-02-26T18:31:16.800Z","avatar_url":"https://github.com/easytarget.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# serialOM.py\nA [RepRapFirmware](https://github.com/Duet3D/RepRapFirmware) [ObjectModel](https://github.com/Duet3D/RepRapFirmware/wiki/Object-Model-Documentation) serial access tool for [Python3](https://www.python.org/) and [microPython](https://micropython.org/)\n\n**serialOM** implements a fetch and update cycle using [`M409`](https://docs.duet3d.com/User_manual/Reference/Gcodes#m409-query-object-model) 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.\n\n## Requirements:\n### Either: CPython 3.7+\n* For communications you need to install `PySerial` (https://pyserial.readthedocs.io/)\n  * *serialOM* expects to be passed a PySerial object (but other serial/stream devices may work too).\n\n### Or: microPython 1.21+\n* A well specified microPython based MCU with the latest official MPY firmware on it.\n\n### A Duet/RRF controller to talk to,\n* Connected to the python system via USB serial or UART\n* The USB/UART device must be specified with '[M575](https://docs.duet3d.com/User_manual/Reference/Gcodes#m575-set-serial-comms-parameters)' in `config.g`.\n  * For USB set `M575 P0 S2` in your config, this will set the USB port correctly.\n  * Other controller UART ports should use `S0` as the port mode (`S2` also works).\n  * CRC/Checksum modes are *not* supported, this includes the default `S1` mode.\n\n## Overview:\n**serialOM()** takes a 'serial' object at init, and a dictionary with the OM keys to gather for each machine mode.\n\nIt returns a *serialOM* object, with the `model` property populated with the requested keys (and the 'state' key)\n\nA bare bones example of *serialOM* can be as simple as:\n```python\nfrom serialOM import serialOM\nfrom serial import Serial\n\nrrf = Serial('/dev/ttyACM0',57600)\nOM  = serialOM(rrf, {'FFF':[],'CNC':[],'Laser':[]}, quiet=True)\nprint('state: ' + OM.model['state']['status']\n     + ', up: ' + str(OM.model['state']['upTime']) + 's')\n```\nThis 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.\n\nIf *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'`.\n\nThe provided 'miniDemo.py' script is more detailed and shows the use of the `OM.update()` and `OM.getResponse()` methods.\n\n### Blocking:\nWhen 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.\n\nThe `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.\n* If adapting for other serial classes than PySerial/UART you need to set the blocking correctly at init.\n\n### Exceptions:\n*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).\n\n### Comminication error tolerant:\nIf 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.\n\nIt 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.\n\n## Use\n\n### Init:\nImport serialOM:\n```python\nfrom serialOM import serialOM\n```\nAnd create an instance of it with:\n```python\nOM = serialOM(rrf, omKeys, requestTimeout=500, rawLog=None, quiet=False, noCheck=False)\n```\nwhere:\n```console\nrrf            = a pyserial.Serial or machine.UART object; or similar\nomKeys         = per-mode lists of keys to sync, (dict, see below)\n                 omKeys = {'machineMode':['OMkey1','OMkey2',..],etc..}\n                          Empty lists [] are allowed.\n                          At least one machineMode must be specified.\nrawLog         = raw log, or None (writable file object, default None)\nquiet          = Suppress info messages (bool, default False)\nnoCheck        = Skip M115 firmware check during init (bool, default False)\n```\nIf the initial connection and update are successful the property `OM.machineMode` will be populated, otherwise it will return an empty string.\n\nThe fetched ObjectModel is returned in the `OM.model` property as a dictionary of keys that match the keys obtained from the controller.\n\n### Methods and Properties:\nThe principle method is\n```python\nOM.update()\n```\nThis initiates a refresh and update of the *model* property from the controller. Returns `True` for success, `False` if timeouts occurred.\n\n*OM.update()* deals gracefully with `machineMode` changes and `upTime` rollbacks (controller reboots); refreshing the entire model and (re)setting `OM.machineMode` as needed.\n\nThe `OM.model` property contains the fetched model as a dictionary.\n\n`OM.machineMode` will be set to the machine mode, or an empty string if not connected.\n\n#### There are two further methods provided by *serialOM* for convenience:\n```python\nserialOM.sendGcode('code')\n```\nSends the specified `code` to the controller, has no return value.\n```python\nserialOM.getResponse('code',json)\n```\nSends `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.\nConforms to the request timeout as described above and returns an empty list if no valid response recieved in time.\n\n## Operation:\n*serialOM* Implements a RRF ObjectModel fetch and update cycle based on using [`M409`](https://docs.duet3d.com/User_manual/Reference/Gcodes#m409-query-object-model) commands to query the ObjectModel on the controller, the responses are gathered and merged into a local Dictionary structure.\n* *serialOM* Uses the `seqs` sequence number mechanism to limit load on the controller by only making verbose requests as needed.\n* *serialOM* fetches different sets of top level ObjectModel keys depending on the master machine mode `FFF`,`CNC` or `Laser`.\n  * This allows you to limit requests to only the keys you need for the mode.\n  * The `printPY.py` demo demonstrates how to use this.\n* *serialOM* provides `serialOM.model`, a dictionary structure with all currently known data.\n  * Each key in *serialOM.model* represents the corresponding OM top level key. The contents of the key will be a structure (lists and dicts) matching the ObjectModel.\n* All low-level serial errors are trapped, and *serialOM* provides it's own `serialOMError` exception that can be independently trapped to make connections robust.\n  * The `printPY.py` demo demonstrates this.\n* After initialisation calling update() will refresh the local OM as necesscary.\n  * The update will clean and re-populate the ObjectModel if either a machine mode change, or a restart of the controller is detected.\n\nFor CPython *serialOM* requires `pyserial`, or a compatible 'serial()' object.\n* Install your distros pyserial package: eg: `sudo apt install python-serial`, or `pip install --user pyserial`, or use a virtualenv (advanced users).\n* On linux make sure your user is in the 'dialout' group to access the devices.\n\nThere 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.\n\n## Notes:\nWritten in CPython; but I am trying to keep all the logic and data handling simple and low-memory for porting to microPython.\n* Non micropython standard libs are discouraged unless they have a easy micropython equivalent/local lib.\n* All times are in `ms` (micropython uses `int(ms)` for it's timing basics rather than `float(seconds)`).\n* You can specify a 'raw' log file handle at init; this is handy when debugging but will fill very rapidly and should never be used 'in production'!\n* Tested and developed on a RaspberryPI connected to my Duet2 wifi via USB/serial, running python 3.9.\nPublished under the CC0 (Creative Commons Zero) Licence; use however you want! Dont blame me if it all goes wrong..\n\n# printPy.py for CPython:\n*For microPython see the equivalent 'printMPy.py' in the [printMPy](printMPy) folder and it's [README](printMPy/README.md)*\n\n*serialOM* comes with a full implementation of a datalogging script in the [`printPy`](printPy) folder.\n\nThis 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.\n\n## outputXXX.py class\nIn the *printPy* demo this is a `text` implementation of the class which logs to the console, and optionally to a log file with timestamps.\n\nSee the full description at [printPy/README.md](printPy/README.md)\n* The text output class serves as a template for writing classes to display ObjectModel info on I2C/SPI or any other external display/data feed.\n* This will eventually be ported to microPython as the core of [PrintPy2040](https://github.com/easytarget/PrintPy2040/).\n\nHere 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.)\n\n```none\n$ python printPy 10000\nprintPy.py is starting at: 2024-2-29 01:02:54 (device localtime)\nstarting output\ndevice \"/dev/ttyACM0\" available\nconnected to: /dev/ttyACM0 @57600\nserialOM is starting\nchecking for connected RRF controller\n\u003e M115\n\u003e\u003e 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\n\u003e\u003e ok\ncontroller is connected\nmaking initial data set request\nconnected to ObjectModel\ninfo: RepRapFirmware for Duet 2 WiFi/Ethernet v3.4.6 on Duet 2 WiFi\n      Controller is in \"FFF\" mode\n      Vin: 23.8V | mcu: 41.0C\nstatus: 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\nstatus: 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\nstatus: 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\nstatus: processing | uptime: 8:46:27 | wifi: 10.0.0.30 | progress: 95.9% | bed: 100.0 (0.0) | e0: 280.4 (0.0)\nstatus: processing | uptime: 8:46:37 | wifi: 10.0.0.30 | progress: 95.9% | bed: 98.5 (0.0) | e0: 276.9 (0.0)\nstatus: idle | uptime: 8:46:47 | wifi: 10.0.0.30 | bed: 96.8 (0.0) | e0: 268.2 (0.0)\nstatus: idle | uptime: 8:46:57 | wifi: 10.0.0.30 | bed: 95.2 (0.0) | e0: 258.9 (0.0)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasytarget%2Fserialom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feasytarget%2Fserialom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasytarget%2Fserialom/lists"}