{"id":21856823,"url":"https://github.com/easytarget/pypwmd","last_synced_at":"2025-09-09T11:18:17.104Z","repository":{"id":257902750,"uuid":"863694311","full_name":"easytarget/pyPWMd","owner":"easytarget","description":"PWM control from userland in python","archived":false,"fork":false,"pushed_at":"2024-10-23T16:34:58.000Z","size":132,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-23T22:36:12.124Z","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":"other","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}},"created_at":"2024-09-26T18:35:51.000Z","updated_at":"2024-10-17T13:01:02.000Z","dependencies_parsed_at":"2024-10-23T19:24:23.039Z","dependency_job_id":null,"html_url":"https://github.com/easytarget/pyPWMd","commit_stats":null,"previous_names":["easytarget/pypwmd"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FpyPWMd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FpyPWMd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FpyPWMd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easytarget%2FpyPWMd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/easytarget","download_url":"https://codeload.github.com/easytarget/pyPWMd/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248936449,"owners_count":21186034,"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":[],"created_at":"2024-11-28T02:23:38.525Z","updated_at":"2025-04-14T18:31:01.111Z","avatar_url":"https://github.com/easytarget.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Python PWM Timer Control Daemon\n\nHardware based **P**ulse **W**idth **M**odulation ([PWM](https://learn.sparkfun.com/tutorials/pulse-width-modulation/all)) is a common feature of modern Single Board Computers.\n\nThe chipsets on these boards can produce very precice PWM signals using onboard timers, typically there are a number of timers which are then assigned to specific GPIO lines. Many have a GPIO connector based on the 'standard' set by the Raspberry Pi.\n\nSometimes PWM timers are used internally on the board to drive status LED's, LCD panel illumination and other board features. But they can also be used to control external devices such as LED strips, Servos and Heaters via the gpio connector pins.\n\n## Hardware PWM in linux\n\nPWM control in linux is provided by a generic API which is implemented in kernel drivers by device manufacturers. The Device Tree and pinctl are then used to enable PWM timers and map them onto GPIO pins.\n\nThe PWM Timers are controlled [via the API](https://www.kernel.org/doc/html/latest/driver-api/pwm.html), and the `/sys/class/pwm` tree.\n- Individual GPIO pins are muxed (mapped) to the timers, this is done via device tree overlays.\n- Generally there is a limited set of mappings available.\n\nThis is highly device dependent, and this guide does not attempt to cover the hardware and software aspects of identifying and mapping the pins.\n  - For the MangoPI MQ Pro I have a guide here: https://github.com/easytarget/MQ-Pro-IO\n  - A lot of the 'custom device tree' [building and installing](https://github.com/easytarget/MQ-Pro-IO/blob/main/build-trees/README.md) information in that guide is generic for any modern SBC running recent Linux versions.\n  - On the Raspberry Pi there are standard Device Tree Overlays you can apply provided with Raspi OS, eg: https://raspberrypi.stackexchange.com/a/143644\n\n### The Issue: By default *only* the root user can control the PWM timers.\n\nThere is not (yet) a good generic solution for allowing userland (non-root) access to the PWM timer devices; controlling them as the root user is straightforward (see the API doc), but ordinary users have no permission to access the tree.\n- On Raspberry PI's this is provided by the `raspi-gpio` package and `RPi.GPIO` python library.\n- I have two non-Pi Single Board Computers based on risc-v. These have PWM lines and drivers available, but the Pi packages are specific to the Pi hardware and not compatible with my boards\n- A 'generic' and device neutral approach is needed\n\n## A Python based generic approach to providing Userland control of PWM timers in linux.\n\n**pyPWMd** is a tool that can run a daemon process as root which controls the timers via the `/sys/class/pwm` tree and provides a simple socket based interface to the timers.\n\nIt also provides two clients for the daemon; a commandline interface and a python class.\n\nI have tested this on my MangoPI MQ-Pro (Allwinner D1 risc-v) and a Raspberry Pi 3A. It will work on any system that correctly implements the Linux PWM API.\n\n## Requirements\n- python3 (3.7+)\n- A recent and updated linux distro\n- Timers enabled and mapped to a gpio pin via a device tree or overlay\n\n## Timer control\nThe PWM timers are arranged by chip number, then timer number.\n\nBy default timers do not have a control node open. Before you can read or write timer properties the node must be opened (eg created at `/sys/class/pwm/pwmchip\u003cchip#\u003e/pwm\u003ctimer#\u003e`). When control is no longer needed the node can be closed again.\n\nOnce a node is open you can read and set properties; for each timer there are four (integer) values:\n* **enable** : Enable/disable the PWM signal (read/write).\n  * 0 = disabled, 1 - enabled\n* **period** : The total period of the PWM signal (read/write).\n  * Value is in nanoseconds and is the sum of the active and inactive time of the PWM.\n* **duty_cycle** : The active time of the PWM signal (read/write).\n  * Value is in nanoseconds and must be less than or equal to the period.\n* **polarity** : Changes the polarity of the PWM signal (read/write).\n  * Value is an integer. 0 for “normal” or 1 for “inversed”.\n  * This is not mandatory, the PWM timer itself may not support it. The api does not mandate that this property should be settable; only that it is present.\n  * pyPWMd does not attempot to set the polarity, but it takes it into account when calculating duty cycles and pulses so that 'on' (output high) time is set correctly.\n\nThe pyPWMd server is a front-end to the (legacy) sysFS interface; the kernel.org PWM API describes this in more detail:\nhttps://www.kernel.org/doc/html/latest/driver-api/pwm.html#using-pwms-with-the-sysfs-interface\n\nAll the clients provide the same set of commands;\n* `open \u003cchip\u003e \u003ctimer\u003e`\n* `close \u003cchip\u003e \u003ctimer\u003e`\n  * Open and Close timer nodes\n* `pwm \u003cchip\u003e \u003ctimer\u003e [\u003cpwm-ratio\u003e]`\n  * sets or gets the pwm 'ratio' (ontime) as a float between 0-\u003e1\n* `pwmfreq [\u003cfrequency\u003e]`\n  * sets or gets the pwm frequency (float, default 1KHz)\n* `servo \u003cchip\u003e \u003ctimer\u003e [\u003cservo-ratio\u003e]`\n  * sets or gets the servo position (ratio) as a float between 0-\u003e1\n* `servoset [\u003cmin-period\u003e \u003cmax-period\u003e [\u003cinterval\u003e]]`\n  * sets or gets servo minimum and maximum pulse periods, and optionally the pulse interval.\n  * specified in nanoseconds; defaults to: 0.6ms / 2.3ms for the min / max, 20ms between pulses.\n* `disable \u003cchip\u003e \u003ctimer\u003e`\n  * Immediately disables the timer, useful with servos to stop jittering\n* `states`\n  * Lists the *open*/*closed* state of all available PWM timers, if a timer is open it's properties are returned\n* `info`\n  * Returns the *version*, *pid*, *uid*, *gid* and *sysfs root path* of the server\n\n## Installing\n\n### Standalone server for testing or one-off use\nNote that the server socket and directory also needs to be created and have it's permissions set.\nThe following example is from a Raspberry Pi 3A with 2 pwm timers.\n\n```console\n$ git clone https://github.com/easytarget/pyPWMd.git\n$ cd pyPWMd\n$ sudo mkdir -p /run/pwm \u0026\u0026 sudo chmod 755 /run/pwm\n$ sudo ./pyPWMd.py server --verbose \u0026\n[1] 43146\nStarting Python PWM server v0.1\nMon Sep 30 12:09:43 2024 :: Server init\nMon Sep 30 12:09:43 2024 :: Scanning for pwm timers\nMon Sep 30 12:09:43 2024 :: PWM devices:\nMon Sep 30 12:09:43 2024 :: - /sys/class/pwm/pwmchip0 with 2 timers\nMon Sep 30 12:09:43 2024 :: Listening on: /run/pwm/pyPWMd.socket\n$ sudo chmod 777 /run/pwm/pyPWMd.socket\n$ alias pwmtimerctl=`pwd`/pyPWMd.py\n```\nThis will put the server into a background process, the `--verbose` optin will show a lot of useful debugging info and this can get intrusive. Omit it as needed.\n\nOnce the server is running you can use `pwmtimerctl` on the commandline, or `import pyPWMd` in python to work with the client. See below.\n\nStop: Once you are done with the server; terminate it by killing the PID\n```console\n$ pwmtimerctl info\n('0.1', 43146, 0, 0, '/sys/class/pwm')\n$ kill 43146\n[1]+  Terminated              sudo ./pyPWMd.py server\n# (could also do kill %1 since the server is backgrounded as #1)\n$ sudo rmdir /run/pwm\n$ unalias pwmtimerctl\n```\n\n### Systemd service (Daemon)\nThe `pyPWMd.service` file will create a pwm server instance at `/run/pwm/pyPWMd.socket` accessible to all users in the group `pwm`.\n\nCreate a 'pwm' system group for users:\n```console\n$ sudo groupadd -K GID_MIN=100 -K GID_MAX=499 pwm\n$ getent group pwm\npwm:x:115:\n```\nAdd the required users to the `pwm` group\n```console\n$ sudo usermod -a -G pwm \u003cusername\u003e\n$ id \u003cusername\u003e\nuid=1000(\u003cusername\u003e) gid=1000(\u003cusergroup\u003e) groups=1000(\u003cusergroup\u003e),...,115(pwm)\n```\nAfter being added the users need to log out then back in for the new group to be available to them.\n\nClone the pyPWMd repo to the root home directory, link the `.service` file into `/etc/systemd/service/`, register the service with systemd then enable+start the service:\n```console\n$ sudo git clone https://github.com/easytarget/pyPWMd.git /usr/local/lib/pyPWMd\n$ sudo ln -s /usr/local/lib/pyPWMd/pyPWMd.service /etc/systemd/system/\n$ sudo systemctl daemon-reload\n$ sudo systemctl enable --now pyPWMd.service\n```\nThe service should now be running at `/run/pwm/pyPWMd.socket`: Check with `$ sudo systemctl status pyPWMd.service`, logfiles will be generated in `/var/log/pwm/`.\n\n### Commandline Client: `pwmtimerctl`\nLink `pyPWMd.py` as `/usr/local/bin/pwmtimerctl`\n```console\n$ sudo ln -s /usr/local/lib/pyPWMd/pyPWMd.py /usr/local/bin/pwmtimerctl\n```\nTest!\n* Make sure you have a **new** user login shell, *with the user in the `pwm` group!*\n```console\n$ pwmtimerctl info\n```\n\n### A little note on security..\nThe daemon process runs as the root user, and is written by 'some bloke on the internet' in python. Be sure you trust it before using it..\n- You can look at the code, of course. It only reads/writes to files in the /sys/class/pwm folder.\n- Python is considered quite secure, and this tool only uses libraries from the python standard library (no random libraries from PiPy etc..)\n- It uses a standard python [multiprocessing comms socket](https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.connection) for communication\n  - By default a local unix filesystem socket is used, permissions can be set on this to allow access via groups.\n  - There is an authentication mechanism on the socket, by default the api version string is used as the token. This could be modified to provide additional control.\n\n## Use\n\n### Commandline client\nA simple example from a Raspberry Pi (2 pwm timers):\n* Also see the demos [client-demo.sh](./client-demo.sh) and [servo-demo.sh](./servo-demo.sh).\n* Start a server if needed (see above); the example server here was started with the `--verbose` flag.\n\nThen control the PWM timers with:\n```console\n$ pwmtimerctl states\n{'0': {0: None, 1: None}}\n$ pwmtimerctl open 0 1\npyPWMd.py: info: opened: /sys/class/pwm/pwmchip0/pwm1\n$ pwmtimerctl states\n{'0': {0: None, 1: (0, 0, 0, 'normal')}}\n$ pwmtimerctl pwmfreq\n1000\n$ pwmtimerctl pwm 0 1 0.5\npyPWMd.py: info: set /sys/class/pwm/pwmchip0/pwm1 = [1, 1000000, 500000, 'normal']\n$ pwmtimerctl pwmfreq 5000\npyPWMd.py: info: pwm default frequency set to 5000.0\n5000\n$ pwmtimerctl pwm 0 1 0.5\npyPWMd.py: info: set /sys/class/pwm/pwmchip0/pwm1 = [1, 200000, 100000, 'normal']\n$ pwmtimerctl states\n{'0': {0: None, 1: (1, 200000, 100000, 'normal')}}\n$ pwmtimerctl disable 0 1\npyPWMd.py: info: disabling 0 1\n$ pwmtimerctl states\n{'0': {0: None, 1: (0, 200000, 100000, 'normal')}}\n$ pwmtimerctl close 0 1\npyPWMd.py: info: closed: /sys/class/pwm/pwmchip0/pwm1\n```\nRun `pwmtimerctl help` to see the full command set and syntax.\n\n## Python Client\nYou need to import the library, then create a `pypwm_client()` object. This will provide:\n```console\nmethods:\n-------\npypwm_client.open(chip, timer):\n      Returns 'True' if the node was successfully opened, or already open\n      Returns an error string on failure\n\npypwm_client.close(chip, timer):\n      Returns 'True' if the close was successful or node already closed\n      Returns an error string on failure\n\npypwm_client.pwm(chip, timer, ratio = None):\n      Sets the PWM ontime according to `ratio` (a float between 0 and 1)\n      Uses the default frequency as defined by `pwmfreq`\n      Returns an error string if the value was not set\n      If `ratio` is None it will calculate and return the current ratio and frequency from the pin\n\npypwm_client.pwmfreq(chip, timer, frequency = None):\n      If a frequency (float, in Hz) is supplied it is set as the default PWM frequency\n      Returns the (new) default value\n\npypwm_client.servo(chip, timer, ratio):\n      Sets the servo position between min and max according to `ratio`, uses the default servo timings\n      Returns an error string if the servo was not set\n\npypwm_client.servoset(chip, timer, min-period = None, max-period = None, Interval = None):\n      Sets the default servo minimum and maximum pulse periods as required, plus pulse interval\n      Returns the (new) default values, or an error string if the new values are are non-sensical\n\npypwm_client.disable(chip, timer):\n      Immediately disables the specified timer\n      Returns 'True' if the disable was successful, or an error string on failure\n\npypwm_client.states():\n      Reads the /sys/class/pwm/ tree and returns the state map as a dict\n\npypwm_client.info():\n      Returns the server details\n\nProperties:\n-----------\npypwm_client.connected\n      A bool, giving the last known client-server connection status\n```\n\n### python client install\nCreate a softlink to the library in your project folder (or copy/clone there)\n```console\n$ ln -s /usr/local/lib/pyPWMd/pyPWMd.py .\n```\n\n### python client example\nHere is an example of using the library on my MQ-Pro (8 pwm timers):\n* Also see the demos [client-demo.py](./client-demo.py) and [servo-demo.py](./servo-demo.py).\n```python\n$ python3\nPython 3.12.3 (main, Sep 11 2024, 14:17:37) [GCC 13.2.0] on linux\nType \"help\", \"copyright\", \"credits\" or \"license\" for more information.\n\u003e\u003e\u003e import pyPWMd\n\u003e\u003e\u003e pwm = pyPWMd.pypwm_client(verbose=True)\n\u003e\u003e\u003e pwm.info()\n('1.0', 10352, 0, 115, '/sys/class/pwm')\n\u003e\u003e\u003e pwm.states()\n{'0': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None}}\n\u003e\u003e\u003e pwm.open(0,2)\nTrue\n\u003e\u003e\u003e pwm.states()\n{'0': {0: None, 1: None, 2: (0, 0, 0, 'inversed'), 3: None, 4: None, 5: None, 6: None, 7: None}}\n\u003e\u003e\u003e pwm.pwmfreq()\n1000\n\u003e\u003e\u003e pwm.pwm(0, 2, 0.5)\nTrue\n\u003e\u003e\u003e pwm.pwm(0, 2)\n(0.5, 1000.0)\n\u003e\u003e\u003e pwm.pwmfreq(5000)\n5000.0\n\u003e\u003e\u003e pwm.pwm(0, 2, 0.25)\nTrue\n\u003e\u003e\u003e pwm.pwm(0, 2)\n(0.25, 5000.0)\n\u003e\u003e\u003e pwm.states()\n{'0': {0: None, 1: None, 2: (1, 200000, 150000, 'inversed'), 3: None, 4: None, 5: None, 6: None, 7: None}}\n\u003e\u003e\u003e pwm.disable(0, 2)\nTrue\n\u003e\u003e\u003e pwm.states()\n{'0': {0: None, 1: None, 2: (0, 200000, 150000, 'inversed'), 3: None, 4: None, 5: None, 6: None, 7: None}}\n\u003e\u003e\u003e pwm.close(0, 2)\nTrue\n\u003e\u003e\u003e pwm.states()\n{'0': {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None}}\n```\n\n## Upgrading\n- Read and follow release notes (if any)\n- `cd /usr/local/lib/pyPWMd`\n- `git pull`\n- `sudo systemctl restart pyPWMd.service`\n\n-----------------------------\n# Commandline help reference\n```console\n$ pwmtimerctl help\nUsage: v1.0\n    pwmtimerctl command \u003coptions\u003e\n    where 'command' is one of:\n        server [\u003clogfile\u003e] [--verbose]\n        states\n        open \u003cchip\u003e \u003ctimer\u003e\n        close \u003cchip\u003e \u003ctimer\u003e\n        pwm \u003cchip\u003e \u003ctimer\u003e [\u003cpwm-ratio\u003e]\n        pwmfreq [\u003cfrequency\u003e]\n        servo \u003cchip\u003e \u003ctimer\u003e \u003cservo-ratio\u003e\n        servoset [\u003cmin-period\u003e \u003cmax-period\u003e [\u003cinterval\u003e]]\n        disable \u003cchip\u003e \u003ctimer\u003e\n        info\n\n    \u003cchip\u003e and \u003ctimer\u003e are integers.\n    - PWM timers are organised by chip, then timer index on the chip.\n\n    'server' starts a server on /run/pwm/pyPWMd.socket.\n    - needs to run as root, see the main documentation for more.\n    - an optional logfile or log directory can be supplied and\n      adding the option '--verbose' enables extended logging.\n\n    All other commands are sent to the server.\n\n    'states' lists the available pwm chips, timers, and their status.\n    - If a node entry is unexported it is shown as 'None'.\n    - Exported entries are a list of the current parameters;\n      enabled, period, duty_cycle, polarity. Followed by the timer's\n      node path in the /sys/class/pwm/ tree, as per kernel pwm api docs.\n\n    'open' and 'close' export and unexport timer nodes.\n    - To access a timer's status and settings the timer node must first\n      be exported.\n    - Timers continue to run even when unexported.\n\n    'pwm' enables and sets the timer to a pwm ratio.\n    - The ratio is a float between 0 and 1 giving the 'on' time ratio.\n    - The frequency is taken from the current pwmfreq setting.\n    - If called with no ratio specified it will return the current\n      (frequency, ratio) read from the pin status.\n\n    'pwmfreq' shows or sets the default PWM frequency in Hz.\n    - Default is 1000 (1KHz).\n    - If called with no argument it returns the current setting.\n\n    'servo' enables and sets the timer to output servo pulses.\n    - The position is a float between 0 (min) and 1 (max) positions.\n\n    'servoset' shows or sets the servo timings and interval.\n    - The first two arguments are the minimum and maximum pulse width\n      times for the servo in seconds (floats).\n    - The third (optional) argument is the interval between pulses in\n      seconds (float).\n    - Default is 0.6ms and 2.3ms for minimum and maximum pulse width,\n      and 20ms for the interval. These are typical figures for small\n      hobby servo motors. Check datasheets and test for your motors as needed.\n    - If called with no argument it returns the current timings in seconds.\n\n    'disable' immediately disables the timer.\n    - This should be used as needed with the servo commands to stop the servo\n      after it has moved to position to avoid hunting and jittering.\n    - The kernel pwm api does not specify the output when disabled, typically\n      it defaults to high-impedance but you should test this.\n\n    'info' returns a tuple with server details.\n      ('version', pid, uid, gid, '\u003csyspath\u003e')\n\n    Homepage: https://github.com/easytarget/pyPWMd\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasytarget%2Fpypwmd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feasytarget%2Fpypwmd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasytarget%2Fpypwmd/lists"}