โโโโโโโ โโโโโโโ โโโ โโโโโโโ โโโโโโโโโโ โโโโโโ โโโโโโโ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโ โโโโโโ โโโโโโ โโโโโโโโโโโโโโ โโโ
โโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโ โโโโโโโโโโโโโโ โโโ
โโโ โโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โโโ โโโโโโ โโโโโ โโโโโโโ โโโโโโโโโโ โโโโโโ โโโโโโโ
It has no strings โ yet it lets you pull all 40 of them.
(And unlike its namesake, status never lies about a pin.)
PINochIO is a single-file GPIO controller for the Raspberry Pi Zero 2 W. Flip pins from one-shot shell commands, drive PWM on the hardware-PWM-capable pins, talk over the UART/IยฒC/SPI alternate functions, or drop into a full-color interactive TUI and conduct the whole 40-pin header like a marionette rig.
- ๐ One-shot CLI โ
on,off,toggle,readany BCM pin straight from the shell - ๐ PWM done right โ value defaults to
0(PWM off);1โ255sets the duty cycle, and only the genuinely PWM-capable pins (BCM 12, 13, 18, 19) accept it - ๐ฅ๏ธ Interactive TUI โ live color-coded table of every pin with keyboard control and an embedded
:command prompt - ๐ก Alternate functions โ UART send/receive, IยฒC scan/read/write, SPI transfers
- ๐ก๏ธ Guard rails โ the HAT-EEPROM pins (BCM 0/1) are protected behind
--force, PWM values are range-checked, non-PWM pins tell you which pins do support it - ๐งฉ Importable โ
import gpioctl; gpioctl.on(17)from any Python project, no CLI required - ๐ Topic-based help โ
help pwmon the CLI,gpioctl.usage("pwm")in Python,:help pwmin the TUI - ๐งช Mock backend โ runs on any machine without a Pi, so you can rehearse the show before opening night
- ๐ฆ Zero-install core โ one file, standard library +
RPi.GPIO(pre-installed on Raspberry Pi OS)
| Component | Needed for | Install |
|---|---|---|
| Python 3.7+ | everything | ships with Raspberry Pi OS |
RPi.GPIO |
pin control & PWM | pre-installed on Raspberry Pi OS |
pyserial |
serial commands |
sudo apt install python3-serial |
smbus2 |
i2c commands |
pip install smbus2 |
spidev |
spi commands |
pip install spidev |
Or grab everything in one go:
pip3 install -r requirements.txtEnable the interfaces you plan to use with sudo raspi-config โ Interface Options (Serial Port with login shell disabled, I2C, SPI).
pip3 install pinochio # library + CLI anywhere (mock backend off the Pi)
pip3 install "pinochio[pi]" # on the Pi: pulls RPi.GPIO, pyserial, smbus2, spidevThat gives you the pinochio console command (with a gpioctl alias) and both import names:
pinochio status
pinochio on 17
pinochio tuiimport pinochio # or: import gpioctl โ same API
pinochio.pwm(18, 128)On Raspberry Pi OS Bookworm, system Python is externally managed โ install inside a venv (
python3 -m venv --system-site-packages ~/env) or add--break-system-packages.
There is no build โ PINochIO is a single script. Get it onto the Pi and make it executable:
git clone <this-repo> && cd PINochIO
scp gpioctl.py pi@raspberrypi.local:~/
ssh pi@raspberrypi.local 'chmod +x gpioctl.py'Optional: put it on your PATH as pinochio:
sudo cp gpioctl.py /usr/local/bin/pinochio./gpioctl.py status # every pin: mode, level, PWM, functions
./gpioctl.py on 17 # GPIO17 high
./gpioctl.py off 17 # GPIO17 low
./gpioctl.py toggle 17
./gpioctl.py read 4 --pull up # read GPIO4 with internal pull-up
./gpioctl.py all-off # curtain call โ everything low
./gpioctl.py test # self-test: blink every pin 1s apart,
# then heartbeat the PWM pins for 5sThe same self-test runs inside the TUI (:test) with the pin table animating live as each string gets pulled.
The value argument defaults to 0, meaning PWM off. Anything from 1โ255 enables PWM at that duty cycle:
./gpioctl.py pwm 18 # 0 -> PWM disabled, pin driven low
./gpioctl.py pwm 18 128 # ~50% duty @ 1 kHz
./gpioctl.py pwm 18 255 # full sendNote:
RPi.GPIOPWM is software PWM and lives inside the process, so thepwmcommand holds the terminal untilCtrl+C(plain on/off states persist after exit). Want daemon-backed, fire-and-forget PWM?pigpiois the roadmap item.
./gpioctl.py serial send "hello" --baud 115200 # UART TXD = GPIO14
./gpioctl.py serial read --seconds 5 # UART RXD = GPIO15
./gpioctl.py i2c scan # SDA = GPIO2, SCL = GPIO3
./gpioctl.py i2c read 0x48 0x00
./gpioctl.py i2c write 0x48 0x01 0xFF
./gpioctl.py spi xfer 0x9F 0x00 0x00 # SPI0: GPIO9/10/11, CE0/1 = GPIO8/7./gpioctl.py tui| Key | Action |
|---|---|
โ / โ (or j/k) |
select a pin |
Space / Enter |
toggle the selected pin |
1 / 0 |
pin on / off |
+ / - |
nudge PWM by ยฑ15 |
p |
set an exact PWM value (0โ255) |
r |
read the selected pin as input |
: |
open the command prompt โ accepts every CLI command (on 17, pwm 18 200, serial send hi, i2c scan, โฆ) |
q |
quit |
No strings attached to the CLI either โ drop gpioctl.py next to your code (or on PYTHONPATH) and import it:
import gpioctl
gpioctl.on(17) # -> 1 (new level)
gpioctl.pwm(18, 128) # PWM while your process runs; 0 disables
level = gpioctl.read(4, pull="up")
gpioctl.serial_send("hello") # -> bytes written
gpioctl.i2c_scan() # -> [0x48, ...]
gpioctl.all_off()
gpioctl.release() # stop PWM threads on shutdownFunctions return values (levels, bytes, address lists) rather than printed strings, and raise gpioctl.GpioDomainException subclasses on bad input. gpioctl.configure(mock=True) forces the simulator โ handy in unit tests. The DDD building blocks (GpioBoard, create_backend, the adapters) are also exported if you want to wire your own aggregate.
Topic-based help with sub-commands, reachable from all three doors โ in Python it's named usage() so it never shadows the built-in help():
python3 gpioctl.py help # overview + topic list
python3 gpioctl.py help pwm # zero in on one commandgpioctl.usage("serial") # same topics from PythonIn the TUI, :help i2c opens a help overlay (any key closes it). Topics: on, off, toggle, read, pwm, status, all-off, serial, i2c, spi, pins, tui, import, help โ plus friendly aliases like uart, alloff, and scripting.
The suite (134 tests under tests/) covers every line of gpioctl.py โ domain, application, adapters, CLI, and the TUI. Hardware libraries (RPi.GPIO, pyserial, smbus2, spidev) and curses are all faked in-memory, so the tests run anywhere, Pi or not:
pip3 install pytest pytest-cov
pytest # fails if coverage drops below 100%The coverage gate lives in pytest.ini (--cov-fail-under=100) and is enforced on every push and pull request by the CI workflow across Python 3.9, 3.11, and 3.12. If your PR leaves an untested line, the wooden boy's nose grows and the build goes red.
No Pi? No problem. The mock backend simulates the header on any machine (--mock, also auto-selected when RPi.GPIO is absent):
python3 gpioctl.py --mock status
python3 gpioctl.py --mock on 17
python3 gpioctl.py --mock pwm 18 128
python3 gpioctl.py --mock pwm 22 100 # correctly rejected: not a PWM pin
python3 gpioctl.py --mock on 0 # correctly rejected: reserved (HAT EEPROM)
python3 gpioctl.py --mock tui # full TUI rehearsalOn real hardware, the classic smoke test โ LED + resistor on GPIO17:
./gpioctl.py on 17 && sleep 1 && ./gpioctl.py off 17
./gpioctl.py pwm 18 64 # gentle glow on GPIO18| BCM | Header | Special functions |
|---|---|---|
| 0 / 1 | 27 / 28 | HAT ID EEPROM โ reserved, needs --force |
| 2 / 3 | 3 / 5 | IยฒC1 SDA / SCL |
| 4 | 7 | GPCLK0, 1-Wire (default) |
| 7โ11 | 26/24/21/19/23 | SPI0 (CE1, CE0, MISO, MOSI, SCLK) |
| 12 / 13 | 32 / 33 | PWM0 / PWM1 |
| 14 / 15 | 8 / 10 | UART TXD / RXD |
| 16 / 17 | 36 / 11 | SPI1 CE2 / CE1 |
| 18 / 19 | 12 / 35 | PWM0 / PWM1, SPI1, PCM |
| 20 / 21 | 38 / 40 | SPI1 MOSI / SCLK, PCM |
| 5, 6, 22โ27 | โ | general-purpose digital I/O |
PINochIO follows Domain-Driven Design, collapsed into one deployable file within the GpioControl bounded context:
- Domain โ
Pinentity,PinDefinitionvalue object,GpioBoardaggregate root (all invariants โ PWM range, PWM capability, reserved pins โ enforced here), domain exceptions - Application โ
GpioApplicationService/BusApplicationServiceorchestration,PinStatusDto, TUI command interpreter - Infrastructure โ
IGpioBackendport withRpiGpioBackendandMockGpioBackendadapters; UART/IยฒC/SPI adapters - Presentation โ argparse CLI and the curses TUI
The domain depends on nothing but the IGpioBackend abstraction โ swap in a pigpio or lgpio adapter without touching a single business rule.
-
pigpiobackend for daemon-persistent, true hardware PWM -
watchcommand โ live-poll an input pin - Named pin aliases (
on led-status) - 1-Wire helper on GPIO4
PRs welcome โ this repo follows git-flow (see CONTRIBUTING.md): branch feature/* from develop, PR back into develop; releases and hotfixes are the only strings that reach main, and only through a PR with every CI check green. Keep the DDD layering intact (business rules stay in GpioBoard), run pytest before submitting โ CI rejects anything under 100% coverage โ and remember: every time you bypass the aggregate root, a wooden boy tells a lie.
Released under the MIT License โ free to use, no strings attached. (Sorry.)