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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +103 -0
  3. data/LICENSE +21 -0
  4. data/PLAN.md +347 -0
  5. data/README.md +969 -0
  6. data/examples/adc.rb +60 -0
  7. data/examples/adc_led.rb +44 -0
  8. data/examples/button.rb +33 -0
  9. data/examples/lcd.rb +47 -0
  10. data/examples/lcd_thermometer.rb +57 -0
  11. data/examples/led.rb +31 -0
  12. data/examples/lowlevel/blink.rb +46 -0
  13. data/examples/lowlevel/button.rb +69 -0
  14. data/examples/lowlevel/servo.rb +76 -0
  15. data/examples/motion_sensor.rb +70 -0
  16. data/examples/motor.rb +38 -0
  17. data/examples/pwm_info.rb +68 -0
  18. data/examples/pwm_jitter.rb +139 -0
  19. data/examples/pwm_led.rb +56 -0
  20. data/examples/rgb_balance.rb +65 -0
  21. data/examples/rgb_led.rb +72 -0
  22. data/examples/servo.rb +70 -0
  23. data/examples/temperature.rb +53 -0
  24. data/lib/rgpio/bytes.rb +12 -0
  25. data/lib/rgpio/chip.rb +271 -0
  26. data/lib/rgpio/devices/adt7410.rb +116 -0
  27. data/lib/rgpio/devices/device.rb +41 -0
  28. data/lib/rgpio/devices/input_device.rb +150 -0
  29. data/lib/rgpio/devices/mcp3208.rb +104 -0
  30. data/lib/rgpio/devices/motor.rb +51 -0
  31. data/lib/rgpio/devices/output_device.rb +66 -0
  32. data/lib/rgpio/devices/pwm_channel.rb +25 -0
  33. data/lib/rgpio/devices/pwm_output_device.rb +109 -0
  34. data/lib/rgpio/devices/rgb_led.rb +175 -0
  35. data/lib/rgpio/devices/servo.rb +161 -0
  36. data/lib/rgpio/devices/st7032.rb +234 -0
  37. data/lib/rgpio/i2c.rb +175 -0
  38. data/lib/rgpio/line_request.rb +184 -0
  39. data/lib/rgpio/native.rb +238 -0
  40. data/lib/rgpio/pwm.rb +321 -0
  41. data/lib/rgpio/software_pwm.rb +290 -0
  42. data/lib/rgpio/spi.rb +208 -0
  43. data/lib/rgpio/version.rb +3 -0
  44. data/lib/rgpio.rb +99 -0
  45. 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.