{"id":15288259,"url":"https://github.com/xlfe/reticul8","last_synced_at":"2025-04-13T07:40:09.032Z","repository":{"id":57461647,"uuid":"144840866","full_name":"xlfe/reticul8","owner":"xlfe","description":"Remotely articulated MCU endpoints for Python","archived":false,"fork":false,"pushed_at":"2020-07-05T12:06:12.000Z","size":256,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-26T23:42:50.668Z","etag":null,"topics":["arduino","esp32","internet-of-things","iot","iot-platform","mcu","microcontroller-firmware","protocol-buffers","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xlfe.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}},"created_at":"2018-08-15T10:44:38.000Z","updated_at":"2021-07-09T05:27:27.000Z","dependencies_parsed_at":"2022-08-28T01:51:03.229Z","dependency_job_id":null,"html_url":"https://github.com/xlfe/reticul8","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xlfe%2Freticul8","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xlfe%2Freticul8/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xlfe%2Freticul8/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xlfe%2Freticul8/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xlfe","download_url":"https://codeload.github.com/xlfe/reticul8/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248345265,"owners_count":21088242,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["arduino","esp32","internet-of-things","iot","iot-platform","mcu","microcontroller-firmware","protocol-buffers","python"],"created_at":"2024-09-30T15:44:56.458Z","updated_at":"2025-04-13T07:40:08.887Z","avatar_url":"https://github.com/xlfe.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"## reticul8\n\n**Remotely articulated MCU endpoints for Python**\n\nreticul8 allows you to use Python to remotely control a compatible microcontroller \nsuch as an Arduino or ESP32.\n\nOn the Python side, it uses Python 3.5+ and asyncio\n\nOn the microcontroller side it uses [PJON](https://github.com/gioblu/PJON) \nand [PJON-cython](https://github.com/xlfe/PJON-cython) to communicate with \nthe micro controller - anything uC that can run PJON should be compatible.\n\nIt also uses [protocol buffers](reticul8.proto) to encapsulate the RPC messages.\n\nFor example, you could use the following setup to wirelessly control an\nESP32 using ESPNOW\n\n```\n                                          \n                                     \t\t\t\t      ┏┅┅┅┅[PJON/SWBB]┅┅┅┅┅┅▶ **Node**(ARDUINO)\n\t\t\t             \t\t\t\t      ┃\n**Controller**(Python)◀┅┅┅[Serial/UART]┅┅┅┅▶**Master**(ESP32)◀┅┅┅┅┅┅┅┅┫\n\t\t\t\t     \t\t\t\t      ┃\n                                     \t\t\t\t      ┗┅┅┅[PJON/ESPNOW]┅┅┅┅┅┅▶ **Node**(ESP32)\n```\n\n### Rationale\n\nreticul8 is designed to meet the following requirements :-\n\n* The system should be able to run \"complex application logic\" and be \"internet connected\"\n* Nodes in the system should be able to connect to the hub using a variety of media (wired and wireless)\n* Nodes should be able to run on common MCU hardware (Arduino and ESP32 targeted initially)\n* Nodes should be fast and reliable, but don't need to be \"smart\" - application logic can live elsewhere\n* Communication between nodes and controller should be fast and reliable (ie not over the internet!)\n\nNotice that one key requirement is the **absence of internet connectivity**. What happens to your home\nautomation system when the internet goes down? \n\nreticul8 is designed for a home automation system where the nodes are not (necessarily) directly connected\nto the internet. This also has the benefit of making communication between the controller/hub and the nodes much \nfaster than something like pub/sub (\u003c70ms rtt for a two node setup with ESPNOW and ThroughSerial).\n\nBuilding on PJON as the communication medium between the nodes allows for plenty of options.\n\nreticul8 is designed to be part of a home automation system - specifically it allows nodes (eg an ESP32 or Arduino) to \noperate as dumb remote endpoints controlled by a smart controller (eg Python running on RaspberryPi).\n\nCompeting projects include :-\n\n* Mongoose OS - An open source Operating System for the Internet of Things\n* MicroPython - Python for microcontrollers\n* Zerynth - The Middleware for IoT using Python on Microcontrollers\n\nBut when I looked at the features I required, none of these seemed like a good fit. MicroPython and Zerynth seemed to \nbe too \"resource heavy\" to run a simple dumb endpoint. Mongoose OS was a pretty close fit but it still assumes your \nnodes are on the internet.\n\n### Arduino-like API:\n\nThe nodes (endpoints) are controlled using Remote Procedure Calls (RPC) defined with [protocolbuf](reticul8.proto).\n\nAn Arduino-like API is provided :-\n\n```python\nimport asyncio\nimport uvloop\nfrom reticul8 import rpc, pjon_strategies\nfrom reticul8.arduino import *\n\nclass Node(rpc.Node):\n\n    async def notify_startup(self):\n        print(\"Received startup message from {}\".format(self.device_id))\n\n        with self:\n\n            # schedule the inbuilt LED to blink 10 times\n            with Schedule(count=10, after_ms=100, every_ms=500):\n                await digitalWrite(22, LOW)\n\n            with Schedule(count=10, after_ms=600, every_ms=500):\n                await digitalWrite(22, HIGH)\n\n            await asyncio.sleep(10)\n\n            #manually blink the LED \n\n            await pinMode(22, OUTPUT)\n            for i in range(5):\n                await digitalWrite(22, HIGH)\n                await sleep(.1)\n                await digitalWrite(22, LOW)\n                await sleep(.1)\n                \n            #read the value of the pin\n            await pinMode(19, INPUT_PULLUP)\n            value = await digitalRead(19)\n            print(\"HIGH\" if value == HIGH else \"LOW\")\n\n            #ping the remote node\n            for i in range(10):\n                await ping()\n\n            #an ESP32 feature - built in PWM\n            await PWM_config(22)\n            while True:\n                await PWM_fade(pin=22, duty=0, fade_ms=500)\n                await sleep(1)\n                await PWM_fade(pin=22, duty=8192, fade_ms=500)\n                await sleep(1)\n\n\nclass PJON(pjon_strategies.SerialAsyncio):\n\n    def notify_connection_made(self):\n        print(\"ESP32 connected\")\n\n    def notify_connection_lost(self):\n        asyncio.get_event_loop().stop()\n        \n\nasyncio.set_event_loop_policy(uvloop.EventLoopPolicy())\nloop = asyncio.get_event_loop()\ntransport = PJON(device_id=10, url=\"/dev/ttyUSB0\", baudrate=115200)\nNode(remote_device_id=11, transport=PJON)\nloop.run_forever()\nloop.close()\n```\n\n## Supported RPCs\n\nGPIO \n* pinMode()\n* digitalRead()\n* digitalWrite()\n* INPUT -\u003e Watch pin for changes with callback on change, debounce\n\nI2C\n* i2c_read\n* i2c_write\n\nESP32 specific features:\n* PWM (ledc)\n* OTA Update \n\nreticul8 helpers\n* schedule commands to run repeatedly\n* run multiple commands \n\n\n## Planned features\n\n* Analog output\n* Analog input\n* Touch sensor (ESP32)\n* Pulse counter (ESP32)\n\n\n## Performance\n\n* The controller keeps track of requests and waiting for responses from the node\n* The controller will place one request at a time\n* The master is the micro-controller with device ID 0, connected to the controller via SERIAL \n* The master is responsible for routing messages to other Nodes\n* Master and nodes must not perform any blocking actions\n* All communication is async in each direction\n\n\n\n## Building using PlatformIO\n\n```bash\n\nsource setup_container.sh\ncd \ncd micro\npio run\n```\n\n\n\n## Building an ESP-IDF component node\n\n[Create a new ESP-IDF project](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html), \nand [add the Arduino component](https://github.com/espressif/arduino-esp32/blob/master/docs/esp-idf_component.md).\n\nAdd reticul8 as a component :-\n\n```bash\ncd components\ngit clone https://github.com/xlfe/reticul8\n```\n\nYour `main.cpp` just needs to setup your PJON buses, and pass these to the reticul8 class. Call setup and loop as per\nthe arduino functions.\n\n```cpp\n\n// Define Wifi config for ESPNOW \n\n#include \"esp_wifi_types.h\"\nstatic wifi_country_t wifi_country = {\n        cc:     \"AU\",\n        schan:  1,\n        nchan:  14,\n        max_tx_power: 80, // Level 10\n        policy: WIFI_COUNTRY_POLICY_MANUAL\n};\n\n#include \"Arduino.h\"\n\n# PJON defines\n\n#define PJON_INCLUDE_ANY\n#define PJON_INCLUDE_TSA\n#define PJON_INCLUDE_EN\n#define TSA_RESPONSE_TIME_OUT 100000\n\n#include \u003creticul8.h\u003e\n\nPJON\u003cAny\u003e *bus = NULL;\nRETICUL8 *r8 = NULL;\n\nvoid loop() {\n    r8-\u003eloop();\n}\n\nvoid setup() {\n\n    Serial.begin(115200);\n    Serial.flush();\n\n    //EPSNOW\n    StrategyLink \u003cESPNOW\u003e *link_esp = new StrategyLink\u003cESPNOW\u003e;\n    PJON\u003cAny\u003e *bus_esp = new PJON\u003cAny\u003e();\n\n    bus_esp-\u003eset_asynchronous_acknowledge(false);\n    bus_esp-\u003eset_synchronous_acknowledge(true);\n    bus_esp-\u003eset_packet_id(true);\n    bus_esp-\u003eset_crc_32(true);\n    bus_esp-\u003estrategy.set_link(link_esp);\n\n    //Uncomment the line below to make a single bus device (eg leaf)\n    // otherwise the device is initialised as a bridge between esp-now and serial\n\n    // r8 = new RETICUL8(bus_esp, 10); /*\n\n\n    //Serial\n    StrategyLink \u003cThroughSerialAsync\u003e *link_tsa = new StrategyLink\u003cThroughSerialAsync\u003e;\n    link_tsa-\u003estrategy.set_serial(\u0026Serial);\n\n    bus = new PJON\u003cAny\u003e(11);\n    bus-\u003estrategy.set_link(link_tsa);\n    bus-\u003eset_asynchronous_acknowledge(false);\n    bus-\u003eset_synchronous_acknowledge(false);\n    bus-\u003eset_packet_id(false);\n    bus-\u003eset_crc_32(false);\n\n    PJON\u003cAny\u003e *secondary[1] = {bus_esp};\n    r8 = new RETICUL8(bus, 10, secondary, 1);\n    //*/\n\n    r8-\u003ebegin();\n}\n```\n\nFinally, make sure your `component.mk` (in same directory as main.cpp) includes the following :-\n\n```cmake\nCOMPONENT_DEPENDS += reticul8\n\n#Used for build timestamp\nCPPFLAGS += -D\"__COMPILE_TIME__ =`date '+%s'`\"\n```\n\n\n\n## Speed tests\n\n| Device | Communication Method | RTT | Sample size (different devices) |\n| ------ | -------------------- | --- | ------------------------------- |\n| SiliconLabs CP2104 | USB Serial | ~1.6 ms | n = 3 |\n| SiliconLabs CP2102N | USB Serial | ~2.9 ms | n = 1 |\n| QinHeng HL340 / CH340C | USB Seria | ~ 3.5 ms | n = 3 |\n| FTDI FT232 | USB Serial | ~1.86 ms | n =2 (using setserial low_latency) |\n| Raspberry PI 4 | Hardware Serial | 1.4 ms | n = 1 |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxlfe%2Freticul8","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxlfe%2Freticul8","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxlfe%2Freticul8/lists"}