rgpio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +103 -0
- data/LICENSE +21 -0
- data/PLAN.md +347 -0
- data/README.md +969 -0
- data/examples/adc.rb +60 -0
- data/examples/adc_led.rb +44 -0
- data/examples/button.rb +33 -0
- data/examples/lcd.rb +47 -0
- data/examples/lcd_thermometer.rb +57 -0
- data/examples/led.rb +31 -0
- data/examples/lowlevel/blink.rb +46 -0
- data/examples/lowlevel/button.rb +69 -0
- data/examples/lowlevel/servo.rb +76 -0
- data/examples/motion_sensor.rb +70 -0
- data/examples/motor.rb +38 -0
- data/examples/pwm_info.rb +68 -0
- data/examples/pwm_jitter.rb +139 -0
- data/examples/pwm_led.rb +56 -0
- data/examples/rgb_balance.rb +65 -0
- data/examples/rgb_led.rb +72 -0
- data/examples/servo.rb +70 -0
- data/examples/temperature.rb +53 -0
- data/lib/rgpio/bytes.rb +12 -0
- data/lib/rgpio/chip.rb +271 -0
- data/lib/rgpio/devices/adt7410.rb +116 -0
- data/lib/rgpio/devices/device.rb +41 -0
- data/lib/rgpio/devices/input_device.rb +150 -0
- data/lib/rgpio/devices/mcp3208.rb +104 -0
- data/lib/rgpio/devices/motor.rb +51 -0
- data/lib/rgpio/devices/output_device.rb +66 -0
- data/lib/rgpio/devices/pwm_channel.rb +25 -0
- data/lib/rgpio/devices/pwm_output_device.rb +109 -0
- data/lib/rgpio/devices/rgb_led.rb +175 -0
- data/lib/rgpio/devices/servo.rb +161 -0
- data/lib/rgpio/devices/st7032.rb +234 -0
- data/lib/rgpio/i2c.rb +175 -0
- data/lib/rgpio/line_request.rb +184 -0
- data/lib/rgpio/native.rb +238 -0
- data/lib/rgpio/pwm.rb +321 -0
- data/lib/rgpio/software_pwm.rb +290 -0
- data/lib/rgpio/spi.rb +208 -0
- data/lib/rgpio/version.rb +3 -0
- data/lib/rgpio.rb +99 -0
- metadata +152 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 76c3380bf641f76772f8b72ec379cfb212ac2bb5a4b106a1865f276f69abdd05
|
|
4
|
+
data.tar.gz: c5f23d56eaf54ee5e2a12d9055ff6533d9b60efb58172b6f6244f09195d62117
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0cec4b12b7f9f59cec1432eb056a254f391f6db93fdfdf7080b7128fbcda299b2421ded48fbb7f6b367a5630c7fb3df0447e934e52f3c4344a1a5e4ab7759f69
|
|
7
|
+
data.tar.gz: f1b031936c7d0db2fbf9235ddc03be74623462ec0bbeed03053dd64252c44123377e191bdeaccae5a5221f09be3c74539561caf396545cd62d3b55bfa87b4fe4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
Nothing yet.
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-09-29
|
|
13
|
+
|
|
14
|
+
First release. GPIO, PWM, I2C and SPI on a Raspberry Pi from Ruby, with a
|
|
15
|
+
gpiozero-style device layer on top. Verified on Pi 5 and Pi 4 hardware; see
|
|
16
|
+
[PLAN.md](PLAN.md) for what has been measured on what, and for the parts still
|
|
17
|
+
waiting on hardware (Pi Zero / Pi 1, and `MotionSensor`).
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **GPIO character-device I/O via libgpiod v2** (`Rgpio::Chip`, `Rgpio::LineRequest`)
|
|
22
|
+
bound through the stdlib `fiddle`. fiddle is built with the interpreter, so it
|
|
23
|
+
matches whatever architecture the Pi is, where the precompiled `ffi` gem
|
|
24
|
+
crashes on ARMv6 (Pi Zero / Pi 1) — those boards are not verified yet, but this
|
|
25
|
+
is the reason the binding is written this way.
|
|
26
|
+
- `Rgpio::Chip.open` / `.new` with block form that closes the chip on exit.
|
|
27
|
+
- Line requests with `direction`, `edge`, `bias`, `active_low`, `debounce_us`,
|
|
28
|
+
`initial_value`, and `consumer` options.
|
|
29
|
+
- Edge-event detection: `LineRequest#wait_edge_events` and `#read_edge_events`
|
|
30
|
+
returning `{ type:, offset:, timestamp_ns: }` hashes.
|
|
31
|
+
- Automatic detection of the 40-pin header GPIO controller by chip label
|
|
32
|
+
(`pinctrl-rp1` / `pinctrl-bcm2711` / `pinctrl-bcm2835`), so the same code
|
|
33
|
+
targets Pi 5 / Pi 4 / Pi Zero without changes. `Rgpio::Chip.list` and
|
|
34
|
+
`.detect_path` expose the selection.
|
|
35
|
+
- **Hardware PWM via the Linux PWM sysfs interface** (`Rgpio::HardwarePWM`) with
|
|
36
|
+
no FFI required. Supports `frequency=`, `duty_cycle=`, `pulse_width_us=`,
|
|
37
|
+
`enable`/`disable`, and block-form `.open`.
|
|
38
|
+
- Automatic RP1 PWM chip/channel detection on Pi 5, including `gpio:`-based
|
|
39
|
+
channel lookup for GPIO12/13/18/19 and a udev-race-safe channel export.
|
|
40
|
+
- Board-aware hardware PWM: `HardwarePWM.detect_board` reads the device-tree
|
|
41
|
+
model, and `.new(gpio:, board:)` maps header pins to channels per board.
|
|
42
|
+
Verified on both Pi 5 (RP1) and Pi 4 (BCM2711, chip at `fe20c000`, 2 channels).
|
|
43
|
+
`#board` exposes the resolved family.
|
|
44
|
+
- `examples/pwm_info.rb`: a non-destructive diagnostic that prints the detected
|
|
45
|
+
board, the PWM chips in sysfs, and which chip/channel each header GPIO resolves
|
|
46
|
+
to — without exporting anything.
|
|
47
|
+
- **High-level device API** (`Rgpio::LED`, `Button`, `MotionSensor`, `Motor`,
|
|
48
|
+
and the `OutputDevice` / `InputDevice` bases they are built on), a
|
|
49
|
+
gpiozero-style layer over `Chip` / `LineRequest`. Devices open their own chip
|
|
50
|
+
or share one passed as `chip:`, and `#close` releases only what they own.
|
|
51
|
+
- Edge callbacks on input devices — `button.when_pressed { }`,
|
|
52
|
+
`sensor.when_motion { }` — dispatched from a background watcher thread that
|
|
53
|
+
survives a raising callback, plus `Rgpio.pause` as the counterpart to Python's
|
|
54
|
+
`signal.pause()`.
|
|
55
|
+
- **I2C support via the Linux i2c-dev interface** (`Rgpio::I2C`): `write`,
|
|
56
|
+
`read`, `write_read` (repeated START), `read_register` / `write_register`,
|
|
57
|
+
block-form `.open`, and `.buses`. Built on `ioctl` against `/dev/i2c-N`, so it
|
|
58
|
+
needs no libgpiod and works wherever `Rgpio.available?` is false. Verified on
|
|
59
|
+
Pi 5 against an EEPROM (128-byte EDID read, valid checksum).
|
|
60
|
+
- **I2C device drivers**: `Rgpio::ADT7410` (temperature sensor — 13/16-bit
|
|
61
|
+
resolution, `#temperature`, `#detected?`) and `Rgpio::ST7032` (AQM0802 /
|
|
62
|
+
AQM1602 character LCD — `#message=`, `#print`, `#move_to`, `#contrast=`,
|
|
63
|
+
`#display_on` / `#display_off`). Both take an `i2c:` to share a bus device, or
|
|
64
|
+
open their own. Verified on Pi 5 with both modules on the header bus at once.
|
|
65
|
+
- `examples/temperature.rb`, `lcd.rb`, `lcd_thermometer.rb`: I2C examples.
|
|
66
|
+
- **SPI support via the Linux spidev interface** (`Rgpio::SPI`): full-duplex
|
|
67
|
+
`transfer`, `write`, `read`, `mode=`, `speed_hz=`, `bits_per_word=`, block-form
|
|
68
|
+
`.open` and `.devices`. Like the I2C support it is `ioctl` work on a character
|
|
69
|
+
device, so it needs no libgpiod.
|
|
70
|
+
- **`Rgpio::MCP3208`**: eight 12-bit ADC channels (`read` for the raw code,
|
|
71
|
+
`value` for a ratio, `voltage` for volts, `read_all`, plus differential pairs).
|
|
72
|
+
`channels: 4` covers the MCP3204, which shares the protocol.
|
|
73
|
+
- `examples/adc.rb` prints every channel; `examples/adc_led.rb` dims an LED from
|
|
74
|
+
a potentiometer, tying the ADC to `PWMLED`.
|
|
75
|
+
- **Software PWM** (`Rgpio::SoftwarePWM`): PWM generated in Ruby on any GPIO
|
|
76
|
+
line, with no dtoverlay and no config.txt entry. `frequency=`, `duty_cycle=`,
|
|
77
|
+
`pulse_width_us=`, `enable`/`disable`, block-form `.open` — the same interface
|
|
78
|
+
as `HardwarePWM`, so the device classes take either. The generating thread
|
|
79
|
+
sleeps until shortly before each edge and then spins, capped at 5% of the
|
|
80
|
+
period. Measured on a Pi 5: a 50 Hz 1500 us pulse held to 6 us of standard
|
|
81
|
+
deviation for 2.7% of one core.
|
|
82
|
+
- **PWM-backed devices**: `Rgpio::PWMLED` (brightness), `Rgpio::RGBLED` (three
|
|
83
|
+
channels, named colours, common-anode support via `active_low:`) and
|
|
84
|
+
`Rgpio::Servo` (`value`, `angle`, `min`/`mid`/`max`, `detach`, calibratable
|
|
85
|
+
pulse range). All default to software PWM and take `pwm: :hardware` — or a
|
|
86
|
+
channel object — to drive the PWM peripheral instead. `RGBLED` takes a
|
|
87
|
+
`balance:` scale per channel, because an RGB LED's three dies are not equally
|
|
88
|
+
bright for equal duty and its white comes out tinted without one.
|
|
89
|
+
- `examples/pwm_led.rb`, `rgb_led.rb`, `servo.rb`: PWM device examples.
|
|
90
|
+
`examples/pwm_jitter.rb` measures the waveform a PWM channel really produces,
|
|
91
|
+
using the kernel's edge timestamps and a jumper between two header pins.
|
|
92
|
+
- `examples/led.rb`, `button.rb`, `motion_sensor.rb`, `motor.rb`: device-class
|
|
93
|
+
examples, with `examples/lowlevel/` holding the same demos written straight
|
|
94
|
+
against `Chip` / `LineRequest` and `HardwarePWM`.
|
|
95
|
+
- `Rgpio.available?` and `Rgpio.version` for probing the libgpiod library, and
|
|
96
|
+
`Rgpio.pause` as the counterpart to Python's `signal.pause()`.
|
|
97
|
+
- Graceful handling of systems that ship libgpiod 1.x (e.g. Debian Bookworm,
|
|
98
|
+
where it is `libgpiod.so.2`): `require "rgpio"` probes for a libgpiod v2 symbol
|
|
99
|
+
and reports `Rgpio.available? == false` rather than raising, so the sysfs-only
|
|
100
|
+
`HardwarePWM` and the `ioctl`-only `I2C` and `SPI` stay usable there.
|
|
101
|
+
|
|
102
|
+
[Unreleased]: https://github.com/lumbermill/rgpio/compare/v0.1.0...HEAD
|
|
103
|
+
[0.1.0]: https://github.com/lumbermill/rgpio/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ITO Yosei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/PLAN.md
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# rgpio — Development Plan
|
|
2
|
+
|
|
3
|
+
This document tracks **unconfirmed plans, in-progress work, and caveats** that
|
|
4
|
+
are not yet settled specification. Anything documented in [README.md](README.md)
|
|
5
|
+
is considered confirmed and supported; anything here is subject to change.
|
|
6
|
+
|
|
7
|
+
## Roadmap
|
|
8
|
+
|
|
9
|
+
| Phase | Scope | Status |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **1** | Pi 5: GPIO I/O + hardware PWM | ✅ Done — verified on Pi 5 hardware |
|
|
12
|
+
| **2** | Auto-detect header gpiochip by label; Pi 4 / Pi Zero support | 🟢 Pi 4 GPIO + PWM verified (Trixie); Pi Zero **still pending** |
|
|
13
|
+
| **3** | High-level API (`LED`, `Button`, `PWMLED`, `Servo`, …) | 🟢 3a–3d verified on Pi 5 (`MotionSensor` deferred); 3e pending |
|
|
14
|
+
|
|
15
|
+
## Multi-board support — validation status
|
|
16
|
+
|
|
17
|
+
The chip auto-detection selects the 40-pin header controller by SoC label
|
|
18
|
+
(`pinctrl-rp1` → Pi 5, `pinctrl-bcm2711` → Pi 4/400, `pinctrl-bcm2835` →
|
|
19
|
+
Pi Zero / 1 / 2 / 3). The selection logic is unit-tested and works on Pi 5.
|
|
20
|
+
|
|
21
|
+
**Validated on real hardware:**
|
|
22
|
+
|
|
23
|
+
- Pi 4 GPIO via libgpiod (Model B Rev 1.2, Trixie, libgpiod 2.2.1, Ruby 3.3.8 —
|
|
24
|
+
`raspi26.local`): header auto-detect → `gpiochip0 [pinctrl-bcm2711]` (58 lines),
|
|
25
|
+
single + batch `get_value(s)` / `set_value(s)` (bias reads and output
|
|
26
|
+
round-trip), and the edge-event API. Full suite (47) green on 3.3.8.
|
|
27
|
+
Verified 2026-09-05.
|
|
28
|
+
- Pi 5 device API (`LED` / `Button` / `Motor`, Trixie): LED blink on GPIO4,
|
|
29
|
+
`Button` press/release with the default pull-down bias and 5 ms debounce, and
|
|
30
|
+
`Motor` forward/backward through a DRV8835 on GPIO2/GPIO14. `active_low: true`
|
|
31
|
+
on `Button` inverts the callbacks too, settling the edge-polarity question:
|
|
32
|
+
the kernel does report edges in logical terms, as `InputDevice` assumed.
|
|
33
|
+
Verified 2026-09-18.
|
|
34
|
+
- Pi 5 I2C bus layer (`Rgpio::I2C`, Trixie): read the 128-byte EDID of the
|
|
35
|
+
HDMI DDC EEPROM (`/dev/i2c-13`, address 0x50) with a valid checksum, both as
|
|
36
|
+
separate write + read and as a `write_read` repeated-START transfer, with the
|
|
37
|
+
two agreeing byte for byte. A bus with nothing at the address reports
|
|
38
|
+
`Errno::EREMOTEIO` rather than hanging. This exercises the `i2c_msg` /
|
|
39
|
+
`i2c_rdwr_ioctl_data` packing, which is where a mistake would corrupt
|
|
40
|
+
transfers silently. Verified 2026-09-23.
|
|
41
|
+
- Pi 5 I2C devices (`ADT7410` + `ST7032`, Trixie, header bus `/dev/i2c-1`): both
|
|
42
|
+
modules on one bus (0x48 and 0x3e, one `I2C` object each) with no contention.
|
|
43
|
+
ADT7410: ID register 0xcb, room temperature in 0.0625 degC steps in 13-bit
|
|
44
|
+
mode, 0.0078 degC steps in 16-bit mode, and switching resolution at runtime.
|
|
45
|
+
ST7032 on an AQM0802 (8x2): both rows legible at the 3.3 V defaults
|
|
46
|
+
(`contrast: 0x20`, booster on) with no adjustment needed, `move_to` addressing
|
|
47
|
+
each row, and in-place overwrite. Verified 2026-09-27.
|
|
48
|
+
- Pi 5 software PWM accuracy (`Rgpio::SoftwarePWM`, Trixie): measured with a
|
|
49
|
+
jumper from GPIO23 to GPIO24 and the kernel's own edge timestamps
|
|
50
|
+
(`examples/pwm_jitter.rb`; the generator runs in a forked process so it does
|
|
51
|
+
not share a GVL with the measuring loop). At 50 Hz / 1500 us — a servo's centre
|
|
52
|
+
position — the pulse came out at 1504.9 us mean, **6.2 us standard deviation**,
|
|
53
|
+
4.7 us median error, 27 us p99, over three runs that agreed to within 0.5 us of
|
|
54
|
+
mean. At 100 Hz / duty 0.5 the pulse held 5007.7 us with 4.9 us sd. No dropped
|
|
55
|
+
cycles in any run. CPU cost of the generating thread: 2.7% of one core at
|
|
56
|
+
50 Hz, 5.3% at 100 Hz, 4.3% at 1 kHz (the spin cap keeps the high frequency
|
|
57
|
+
cheap). Verified 2026-09-27.
|
|
58
|
+
- Pi 5 gpiozero comparison, same wiring and same measurement: gpiozero's
|
|
59
|
+
`PWMOutputDevice` at 50 Hz / duty 0.075 produced a **1424 us** pulse (82 us
|
|
60
|
+
median error) and quantises duty to whole percent — 0.070, 0.075 and 0.079 all
|
|
61
|
+
came out at ~1410-1424 us and 0.080 jumped to 1621 us, i.e. **200 us steps** on
|
|
62
|
+
a 20 ms frame. Its cause is in gpiozero, not lgpio: the lgpio pin driver passes
|
|
63
|
+
`int(value * 100)`. A 180-degree servo therefore has about ten reachable
|
|
64
|
+
positions under Python on a Pi 5, against continuous positioning here. At
|
|
65
|
+
100 Hz / duty 0.5 (where 50% is exactly representable) the two are equivalent
|
|
66
|
+
(2.5 us vs 2.9 us median error). Verified 2026-09-27.
|
|
67
|
+
- Pi 5 PWM device classes (`Servo`, `PWMLED`, `RGBLED`, Trixie): verified against
|
|
68
|
+
the same GPIO23-to-GPIO24 loopback, which measures the waveform the classes
|
|
69
|
+
actually put on the line rather than trusting the arithmetic. `Servo` value
|
|
70
|
+
-1.0/0.0/+1.0 produced 1004.8/1505.2/2004.6 us, `angle = 45` produced 1756.6 us,
|
|
71
|
+
a 500..2500 us range produced 504.2 us at its minimum, and `detach` produced no
|
|
72
|
+
edges at all. `PWMLED` value 0.25/0.50/0.75 produced 25.1/50.1/75.1% duty,
|
|
73
|
+
`active_low: true` inverted it, and 400 Hz framed correctly. `RGBLED` set one
|
|
74
|
+
channel to 0.6 while its other two threads ran, and that channel measured 60.1%.
|
|
75
|
+
Every reading sat 4-8 us above target, the constant offset noted below.
|
|
76
|
+
Verified 2026-09-27. The parts themselves followed on 2026-09-28: an LED faded
|
|
77
|
+
smoothly and held each fixed level without flicker down to 5% duty, an RGB LED
|
|
78
|
+
showed every named colour correctly once balanced, and a servo swept
|
|
79
|
+
continuously by angle — the step-free sweep being exactly what Python cannot do
|
|
80
|
+
here — with `detach` releasing the horn.
|
|
81
|
+
- Pi 5 SPI bus layer (`Rgpio::SPI`, Trixie, `/dev/spidev0.0`): verified with a
|
|
82
|
+
jumper from GPIO10 (MOSI, pin 19) to GPIO9 (MISO, pin 21), which makes every
|
|
83
|
+
byte sent come straight back — the data path cannot be confirmed without it,
|
|
84
|
+
since an unconnected MISO reads 0x00 whether the implementation is right or
|
|
85
|
+
wrong. A six-byte pattern, 64 bytes, a single byte and a String argument all
|
|
86
|
+
came back identical, at 100 kHz, 1, 4, 16 and 32 MHz, in all four SPI modes;
|
|
87
|
+
`read` clocked zeros out and `write` reported the byte count. Verified
|
|
88
|
+
2026-09-28.
|
|
89
|
+
- Pi 5 `MCP3208` (Trixie, CE0, 1 MHz, VREF 3.3 V, 10 kohm potentiometer on CH0):
|
|
90
|
+
a full sweep of the knob covered all sixteen sixteenths of the code range,
|
|
91
|
+
1..3929 with 245 distinct values out of 550 samples — the top end short of 4095
|
|
92
|
+
only because the knob stopped short of its travel. Repeated reads of a still
|
|
93
|
+
input scattered by 0.42 LSB (0.34 mV), and the reading did not move between
|
|
94
|
+
100 kHz and 2 MHz. Absolute ends confirmed by wiring CH1 to 3.3 V and CH2 to
|
|
95
|
+
ground: CH1 reached code **4095** (mean 4090.1, sd 7.0) and CH2 read 0..1 (mean
|
|
96
|
+
1.00, an offset of about 1 LSB or 0.8 mV). Neither mean sits exactly on its end
|
|
97
|
+
code, and that is arithmetic rather than error — with the input at the same
|
|
98
|
+
potential as VREF, noise can only move a sample downwards, so the distribution
|
|
99
|
+
is clipped and its mean must fall short. A potentiometer's wiper is a
|
|
100
|
+
high-impedance source and reads noisier than a rail: 27 LSB against 7.
|
|
101
|
+
Verified 2026-09-29.
|
|
102
|
+
- Pi 4 hardware PWM (Model B Rev 1.5, Bookworm — `raspi24.local`): board
|
|
103
|
+
detection → `:pi4`, chip detection (`fe20c000`, `npwm == 2`), `GPIO18 →
|
|
104
|
+
channel 0`, full export/frequency/duty round-trip. Verified 2026-08-27.
|
|
105
|
+
|
|
106
|
+
**Not yet validated on real hardware:**
|
|
107
|
+
|
|
108
|
+
- Pi Zero / Zero W / Zero 2 W / Pi 1 (`pinctrl-bcm2835`), including ARMv6 fiddle
|
|
109
|
+
behaviour under load. The `i2c_msg` struct layout is 32-bit-aware (the buffer
|
|
110
|
+
pointer sits at offset 8 either way) but has only been exercised on aarch64.
|
|
111
|
+
- `Rgpio::ST7032` on a 16-column AQM1602, and on a 5 V module (`booster: false`);
|
|
112
|
+
only the 8x2 AQM0802 at 3.3 V has been on the bus.
|
|
113
|
+
- `Rgpio::ADT7410` below 0 degC — the negative branch of the conversion is
|
|
114
|
+
unit-tested against the datasheet's codes but has never come off real silicon.
|
|
115
|
+
- `MotionSensor` — deferred, see Phase 3 below.
|
|
116
|
+
|
|
117
|
+
Until validated, treat GPIO (libgpiod) on Pi Zero / Pi 1 as best-effort.
|
|
118
|
+
|
|
119
|
+
## Planned API additions (current gem)
|
|
120
|
+
|
|
121
|
+
These extend the existing `Rgpio::*` classes and are candidates before `0.1.0`
|
|
122
|
+
is published:
|
|
123
|
+
|
|
124
|
+
- _(none pending — see Done below)_
|
|
125
|
+
|
|
126
|
+
### Done
|
|
127
|
+
|
|
128
|
+
- ~~**Pi 4 hardware-PWM mapping**~~ — board-aware `HardwarePWM`: `detect_board`
|
|
129
|
+
reads `/proc/device-tree/model`; `.new(gpio:, board:)` selects the channel per
|
|
130
|
+
board (Pi 4: `GPIO12/18 → 0`, `GPIO13/19 → 1`; chip at `fe20c000`, `npwm == 2`).
|
|
131
|
+
Verified on a real Pi 4 (2026-08-27). Now confirmed spec — see README.
|
|
132
|
+
- ~~**Batch multi-line I/O**~~ — `LineRequest#get_values` / `#set_values` via
|
|
133
|
+
`gpiod_line_request_get_values_subset` / `set_values_subset`. Verified on Pi 5
|
|
134
|
+
hardware (atomic reads/writes and subset addressing). Now confirmed spec — see
|
|
135
|
+
README.
|
|
136
|
+
- ~~**Graceful libgpiod v1 handling**~~ — `require "rgpio"` used to crash when
|
|
137
|
+
only libgpiod 1.x was present (e.g. Bookworm's `libgpiod.so.2`). The loader now
|
|
138
|
+
probes for a v2 symbol and reports `Rgpio.available? == false` instead. Found
|
|
139
|
+
and fixed while validating PWM on the Bookworm Pi 4.
|
|
140
|
+
|
|
141
|
+
## Phase 3 — high-level API
|
|
142
|
+
|
|
143
|
+
A gpiozero-style convenience layer on top of the low-level classes. The goal is
|
|
144
|
+
concrete: port every `gpiozero` / `RPi.GPIO` sample in 実践課題3 of
|
|
145
|
+
*Dive into Raspberry Pi 2026* (<https://lmlab.net/books/2601_raspi/>) to Ruby,
|
|
146
|
+
ship each one as an `examples/` script, and verify it on real hardware. The
|
|
147
|
+
examples are named after what they demonstrate rather than after the book's
|
|
148
|
+
Python filenames, so they stand on their own for anyone reading the gem.
|
|
149
|
+
|
|
150
|
+
**Settled decisions**
|
|
151
|
+
|
|
152
|
+
- **One gem, not two.** The device layer lives in `lib/rgpio/devices/` but keeps
|
|
153
|
+
the `Rgpio::` namespace, so `require "rgpio"` is all a reader needs. It adds
|
|
154
|
+
no dependencies, so bundling costs nothing, and splitting later is a directory
|
|
155
|
+
move. A separate gem would buy independent release cycles — worth little for a
|
|
156
|
+
single maintainer — at the cost of version-range bookkeeping and a two-gem
|
|
157
|
+
install for the book's readers.
|
|
158
|
+
- **Software PWM by default, hardware PWM opt-in** (revised 2026-09-27; this
|
|
159
|
+
reverses the earlier "hardware PWM only" decision). The constraint that
|
|
160
|
+
settled it: *the book's readers must not have to edit config.txt*, and hardware
|
|
161
|
+
PWM cannot be reached without a dtoverlay — on a Pi 5 the stock overlays route
|
|
162
|
+
at most **two** header pins (`pwm-2chan`: `pin` from 12/18, `pin2` from 13/19),
|
|
163
|
+
so an `RGBLED` could not work at all. `Rgpio::SoftwarePWM` needs no
|
|
164
|
+
configuration, drives any line, and — measured, see below — beats gpiozero on
|
|
165
|
+
its own ground, so the book keeps its GPIO2/3/4 wiring and no revision is
|
|
166
|
+
needed. `pwm: :hardware` stays available for anyone who can set the overlay.
|
|
167
|
+
What gpiozero does, for the record: everything goes through `lgpio.tx_pwm`,
|
|
168
|
+
which its own docs call "software timed PWM" — it never touches the kernel PWM
|
|
169
|
+
interface, which is why it needs no config.txt either.
|
|
170
|
+
- **Callbacks over blocks, dispatched from one watcher thread per device.**
|
|
171
|
+
`button.when_pressed { ... }` rather than gpiozero's attribute assignment.
|
|
172
|
+
The thread blocks in `read_edge_events` with a finite timeout so `#close` can
|
|
173
|
+
stop it; fiddle releases the GVL during the call, so the main thread stays
|
|
174
|
+
responsive. `Rgpio.pause` stands in for Python's `signal.pause()`.
|
|
175
|
+
|
|
176
|
+
**Staging**
|
|
177
|
+
|
|
178
|
+
| Stage | Scope | Book sections | Status |
|
|
179
|
+
|---|---|---|---|
|
|
180
|
+
| 3a | `LED` / `Button` / `Motor` / `Rgpio.pause` | LED点滅, スイッチ, モータードライバ | ✅ verified on Pi 5 — confirmed spec, see README |
|
|
181
|
+
| 3a′ | `MotionSensor` | モーションセンサ | ⏸ written + unit-tested, hardware verification deferred |
|
|
182
|
+
| 3b | `Rgpio::I2C` + ADT7410 / ST7032 examples | 温度センサ, LCD | ✅ verified on Pi 5 — confirmed spec, see README |
|
|
183
|
+
| 3c | `Servo` / `PWMLED` / `RGBLED` over `SoftwarePWM` (hardware opt-in) | サーボ, フルカラーLED | ✅ verified on Pi 5 — confirmed spec, see README |
|
|
184
|
+
| 3d | `Rgpio::SPI` + `MCP3208` | ADコンバータ | ✅ verified on Pi 5 — confirmed spec, see README |
|
|
185
|
+
| 3e | Camera examples shelling out to `rpicam-still` | モーション+撮影, 測距センサ | ⬜ |
|
|
186
|
+
|
|
187
|
+
I2C and SPI need no libgpiod: they are `ioctl` calls on `/dev/i2c-N` and
|
|
188
|
+
`/dev/spidevN.M`, so they stay dependency-free like the sysfs PWM code.
|
|
189
|
+
|
|
190
|
+
**Phase 3b notes**
|
|
191
|
+
|
|
192
|
+
- `Rgpio::I2C` is one object per address: `I2C.new(address:, bus: 1)` claims the
|
|
193
|
+
address with the `I2C_SLAVE` ioctl and keeps the file open. Two devices on one
|
|
194
|
+
bus (the sensor at 0x48 and the display at 0x3e) are therefore two `I2C`
|
|
195
|
+
objects, not a shared one — that is what the kernel interface models, and it
|
|
196
|
+
keeps `write`/`read` free of an address argument.
|
|
197
|
+
- `write_read` issues a repeated START through `I2C_RDWR` rather than a write
|
|
198
|
+
followed by a separate read. Both work for the ADT7410, but only the former is
|
|
199
|
+
safe if another master shares the bus.
|
|
200
|
+
- No SMBus (`I2C_SMBUS`) layer: the ioctl only reaches adapters that implement
|
|
201
|
+
the SMBus subset, and plain I2C transfers cover every device in the book.
|
|
202
|
+
- No bus scan (`i2cdetect`-style) yet. A scan has to guess between a quick-write
|
|
203
|
+
and a read probe per address, and probing write-only devices can change their
|
|
204
|
+
state; `i2cdetect -y 1` already does it safely from the shell.
|
|
205
|
+
- Redrawing with `clear` once a second visibly flickers on these panels, because
|
|
206
|
+
the clear blanks the row for as long as the next transfer takes. The examples
|
|
207
|
+
write the label once and then overwrite the value in place, padded to the row
|
|
208
|
+
width. Found while verifying on the AQM0802.
|
|
209
|
+
- `ST7032#print` drops text that would run past the last column instead of
|
|
210
|
+
wrapping, because the controller's DDRAM addresses are not contiguous between
|
|
211
|
+
rows — an overrun scatters characters into invisible addresses rather than
|
|
212
|
+
continuing on the next line.
|
|
213
|
+
- Contrast is the one setting that cannot be read back, and a wrong value looks
|
|
214
|
+
exactly like a dead panel. 0x20 with the booster on is the 3.3 V default; the
|
|
215
|
+
5 V panels want roughly 0x28 with the booster off.
|
|
216
|
+
|
|
217
|
+
**Phase 3c notes**
|
|
218
|
+
|
|
219
|
+
- `SoftwarePWM` sleeps until shortly before each edge and then spins. The spin is
|
|
220
|
+
what makes it usable: with `spin_us: 0`, a 50 Hz 1500 us pulse spread over
|
|
221
|
+
72..6756 us (386 us sd) — a servo would visibly slam around. 300 us of spin
|
|
222
|
+
brought that to 6 us sd; 1000 us was no better. The spin is capped at 5% of the
|
|
223
|
+
period per edge so a high frequency cannot turn the thread into a busy loop.
|
|
224
|
+
- Two accuracy bugs, both found by measuring and both worth remembering:
|
|
225
|
+
re-reading the frequency/duty under the mutex *between* the deadline and the
|
|
226
|
+
rising edge charged that work to the pulse, and timing the high phase from the
|
|
227
|
+
nominal deadline charged it the wake-up overshoot too (15 us of every pulse).
|
|
228
|
+
The high phase is now timed from a clock read taken just before the rising
|
|
229
|
+
edge's `set_value`, and a **write of the level the line already holds** goes
|
|
230
|
+
out before that read: the first call after waking from the long low phase is
|
|
231
|
+
slow and erratic, and paying it in advance leaves the real edge on a warm path.
|
|
232
|
+
That one line took the spread from 20 us to 6 us.
|
|
233
|
+
- Remaining bias is +5 us (pulse slightly long), stable run to run. On a
|
|
234
|
+
180-degree servo that is 0.4 degrees, and a constant offset is what the
|
|
235
|
+
per-servo `min_pulse_us` / `max_pulse_us` calibration absorbs anyway.
|
|
236
|
+
- Spread depends on what else the machine is doing: the same configuration
|
|
237
|
+
measured 6 us sd on an idle box and 30 us sd with an editor indexing in the
|
|
238
|
+
background. Ruby cannot do better — while the main thread holds the GVL, the
|
|
239
|
+
generating thread cannot wake. Python has the same limitation with the GIL.
|
|
240
|
+
- `RGBLED` runs three `SoftwarePWM` channels, so three generating threads
|
|
241
|
+
that each spin. One thread multiplexing three lines with batch `set_values`
|
|
242
|
+
would be cheaper and is the obvious optimisation if it proves necessary; for
|
|
243
|
+
LEDs the edge placement is invisible, so it has not been.
|
|
244
|
+
|
|
245
|
+
**Phase 3c findings on real parts**
|
|
246
|
+
|
|
247
|
+
- An RGB LED's white is tinted unless the channels are scaled. On the LED used
|
|
248
|
+
for verification (common cathode, 330 ohm on each channel, 3.3 V) white read as
|
|
249
|
+
bluish and `balance: [1.0, 0.8, 0.8]` corrected it — **red was the weak
|
|
250
|
+
channel**, despite carrying the most current: 1.3 V of headroom over its ~1.9 V
|
|
251
|
+
drop against 0.2-0.3 V over green's and blue's ~3.1 V, so roughly 3.9 mA against
|
|
252
|
+
0.9 mA. Modern InGaN green and blue dies are efficient enough per mA, and the
|
|
253
|
+
eye sensitive enough around 555 nm, to overturn a 4x current difference.
|
|
254
|
+
- Doing that correction with resistors instead would mean raising green's and
|
|
255
|
+
blue's, and it is the worse tool: PWM removes time rather than current, so each
|
|
256
|
+
die keeps its rated current and therefore its efficiency and wavelength, while
|
|
257
|
+
running an InGaN die at a fraction of its current shifts the colour slightly.
|
|
258
|
+
The scale factor also travels with the code. `balance:` is per-part, so the gem
|
|
259
|
+
defaults to no correction and `examples/rgb_balance.rb` finds the value by eye.
|
|
260
|
+
|
|
261
|
+
**Phase 3d notes**
|
|
262
|
+
|
|
263
|
+
- `Rgpio::SPI` builds its ioctl numbers from the encoding rather than writing
|
|
264
|
+
them out (`ioctl_number(direction, request, size)`), and the unit tests assert
|
|
265
|
+
the results against the values in `<linux/spi/spidev.h>`. The struct is exactly
|
|
266
|
+
32 bytes and the header promises the same layout in 32- and 64-bit userspace,
|
|
267
|
+
so unlike `i2c_msg` there is nothing architecture-dependent to get wrong. Its
|
|
268
|
+
two buffer fields are `__u64` even on a 32-bit system, so they pack as "Q",
|
|
269
|
+
never "J".
|
|
270
|
+
- Every SPI transfer is full duplex, so `#transfer` answers with as many bytes as
|
|
271
|
+
it was given, and `#write` / `#read` are that transfer with one direction
|
|
272
|
+
ignored. There is no equivalent of I2C's repeated-START question.
|
|
273
|
+
- On this Pi 5, `/dev/spidev0.0` and `0.1` are the header bus and `/dev/spidev10.0`
|
|
274
|
+
is something else entirely; GPIO8 (CE0) shows as a plain output held high
|
|
275
|
+
because the driver manages chip select as a GPIO. Both are normal.
|
|
276
|
+
- **Clock rate is a correctness question for the MCP3208, not just a speed one.**
|
|
277
|
+
The datasheet allows 1 MHz at 2.7 V and 2 MHz at 5 V; clocked faster than its
|
|
278
|
+
sampling rate the part returns plausible, wrong numbers. The default is 1 MHz,
|
|
279
|
+
inside the envelope for the 3.3 V supply the Pi gives it. **Measured** with CH1
|
|
280
|
+
tied to 3.3 V: the mean code held at 4086-4087 from 100 kHz through 2 MHz and
|
|
281
|
+
dropped to 4077 at 4 MHz — a 9 LSB droop, the datasheet limit becoming visible,
|
|
282
|
+
and exactly the kind of error that looks like a plausible reading.
|
|
283
|
+
- The MCP3204 is the same protocol with four channels, so `channels: 4` covers it.
|
|
284
|
+
The MCP3008 is *not* — it is 10-bit with a different command byte — and is not
|
|
285
|
+
implemented, since no book sample needs it.
|
|
286
|
+
|
|
287
|
+
**Open questions**
|
|
288
|
+
|
|
289
|
+
- The book's switch is wired to 3.3 V, so `Button` defaults to a pull-**down**
|
|
290
|
+
bias, unlike gpiozero's pull-up default. That wiring is confirmed on hardware;
|
|
291
|
+
what is left is to cross-check the book's circuit diagram before it is revised.
|
|
292
|
+
- `wait_for_press` / `LED#blink` are deliberately not implemented yet — no book
|
|
293
|
+
sample needs them.
|
|
294
|
+
- `Motor` has no speed control (it would need PWM on both lines); the book's
|
|
295
|
+
sample only uses full-speed forward/backward.
|
|
296
|
+
- `MotionSensor` is **deferred**: the PIR modules on hand are an unreliable
|
|
297
|
+
supply, so it is out of the 3a verification scope and stays out of the README
|
|
298
|
+
until a module can be tested end to end. The class, its unit tests and
|
|
299
|
+
`examples/motion_sensor.rb` ship as they are. What testing did show:
|
|
300
|
+
it reports every pulse the module emits, and the D-SUN (BISS0001)
|
|
301
|
+
board used for verification false-triggers on 5 V rail noise often enough to
|
|
302
|
+
be noticeable — 0.9 s pulses arriving with nothing moving. gpiozero smooths
|
|
303
|
+
this with `queue_len`: a thread polls at `sample_rate` and `is_active`
|
|
304
|
+
compares the windowed average against `threshold`, so isolated pulses fall
|
|
305
|
+
below the bar. Porting that directly would replace the kernel edge watcher
|
|
306
|
+
with a 10 Hz poll, a poor trade for a gem built on the character-device
|
|
307
|
+
interface; the edge-driven equivalent is to re-read `value` a fixed delay
|
|
308
|
+
after a rising edge and dispatch only if the line is still active.
|
|
309
|
+
`debounce_us` cannot stand in for either: a spurious pulse is stable for its
|
|
310
|
+
whole length, so any debounce long enough to drop it also drops real
|
|
311
|
+
detections. Not implemented — decoupling the sensor (100 µF + 0.1 µF across
|
|
312
|
+
VCC/GND) is the first fix, and no book sample needs the filtering.
|
|
313
|
+
|
|
314
|
+
## Release / tooling readiness
|
|
315
|
+
|
|
316
|
+
- [ ] Publish `0.1.0` to RubyGems. `mfa_required` is set, so the push needs an
|
|
317
|
+
OTP from the maintainer. Everything else is ready and checked: the name is
|
|
318
|
+
free, the metadata URLs match the repository, and the built gem (70 KB, 43
|
|
319
|
+
files) installs into a clean GEM_HOME and works — all 18 public classes
|
|
320
|
+
present, a real I2C read through the installed copy.
|
|
321
|
+
- [x] GitHub Actions CI running the logic-only test suite (no hardware needed) —
|
|
322
|
+
`.github/workflows/ci.yml`, Ruby 3.3 / 3.4 / 4.0, bundler-less (the committed
|
|
323
|
+
lock is pinned to the aarch64 dev box). A `RuboCop` lint job runs alongside.
|
|
324
|
+
- [x] RuboCop lint configuration — `.rubocop.yml` tuned to the project's style;
|
|
325
|
+
the tree is clean. `rake` runs test + rubocop.
|
|
326
|
+
- [ ] Integration tests for `LineRequest` / edge events (needs GPIO loopback
|
|
327
|
+
wiring; not runnable in CI). A manual Pi 5 smoke test already verified
|
|
328
|
+
batch `get_values`/`set_values`.
|
|
329
|
+
- [ ] Optional: RuboCop extensions (`rubocop-minitest`, `rubocop-rake`).
|
|
330
|
+
- [x] `required_ruby_version` lowered to `>= 3.3` to match Trixie's default
|
|
331
|
+
`ruby` (so `gem install rgpio` works on a stock Trixie box). Verified on
|
|
332
|
+
Ruby 3.3.8; CI tests 3.3, 3.4 and 4.0, and the dev box runs 4.0.4.
|
|
333
|
+
|
|
334
|
+
## Environment caveats (not yet pinned as spec)
|
|
335
|
+
|
|
336
|
+
- **PWM overlay parameters vary by kernel version.** The `dtoverlay` `pin`/`func`
|
|
337
|
+
values in the README are correct for current Trixie kernels; if they change,
|
|
338
|
+
the definitive list is in `/boot/firmware/overlays/README` on the Pi.
|
|
339
|
+
- **PWM sysfs chip number varies by kernel version.** On Pi 5 the RP1 header PWM
|
|
340
|
+
is typically `pwmchip2`, but this is auto-detected rather than assumed. (On the
|
|
341
|
+
current dev Pi 5 it is actually `pwmchip0`.)
|
|
342
|
+
- **Without the header PWM overlay, auto-detection can select the RP1 fan PWM.**
|
|
343
|
+
When the header PWM0 (`1f00098000`) is not enabled via dtoverlay, the only PWM
|
|
344
|
+
chip present may be the RP1 fan controller PWM1 (`1f0009c000`), which also
|
|
345
|
+
reports `npwm == 4`. `detect_pwm_chip!` prefers the header address first, but
|
|
346
|
+
falls back to `npwm == 4` and would then pick the fan chip. Enable the header
|
|
347
|
+
PWM overlay before using `HardwarePWM(gpio:)`, or pass `chip:` explicitly.
|