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
@@ -0,0 +1,290 @@
1
+ module Rgpio
2
+ # PWM generated in Ruby on any GPIO line, for the pins the hardware PWM
3
+ # peripheral cannot reach.
4
+ #
5
+ # {HardwarePWM} is jitter-free but needs a dtoverlay in config.txt and only
6
+ # reaches GPIO12/13/18/19, two channels at a time on the 40-pin header. This
7
+ # class needs no configuration at all and works on every line, at the cost of
8
+ # timing accuracy — the same trade-off Python's gpiozero makes, which drives
9
+ # all of its PWM through `lgpio.tx_pwm` ("software timed PWM").
10
+ #
11
+ # Usage (block form — recommended):
12
+ # Rgpio::SoftwarePWM.open(18) do |pwm|
13
+ # pwm.frequency = 100
14
+ # pwm.duty_cycle = 0.25
15
+ # pwm.enable
16
+ # sleep 2
17
+ # end
18
+ #
19
+ # Accuracy: the generating thread sleeps until shortly before each edge and
20
+ # then spins for the last {#spin_us} microseconds, because a bare `sleep`
21
+ # overshoots a microsecond-scale deadline badly — with no spin at all, a 50 Hz
22
+ # 1500 us pulse measured on a Pi 5 spread over 72..6756 us. Spinning holds the
23
+ # GVL, so it is a tax on the main thread, capped at {MAX_SPIN_FRACTION} of the
24
+ # period per edge.
25
+ #
26
+ # Nothing can fix the other direction: while the main thread holds the GVL in
27
+ # a long computation, this thread cannot wake at all. Python has the same
28
+ # limitation with the GIL.
29
+ class SoftwarePWM
30
+ # gpiozero's default for PWMLED, and fast enough that an LED does not
31
+ # visibly flicker.
32
+ DEFAULT_FREQUENCY = 100
33
+
34
+ # How long before each edge to stop sleeping and start spinning. Measured
35
+ # on a Pi 5 at 50 Hz: 0 us gives a 386 us standard deviation and pulses as
36
+ # long as 6.7 ms, 100 us gives 29 us, 300 us gives 13 us, and 1000 us is no
37
+ # better than 300.
38
+ DEFAULT_SPIN_US = 300
39
+
40
+ # Cap on the spin as a fraction of the period, per edge. Spinning holds the
41
+ # GVL, so a fixed 300 us would cost 60% of a core at 1 kHz; this keeps the
42
+ # tax at 10% of one core whatever the frequency, and a frequency that high
43
+ # is driving an LED, where a few microseconds of edge placement is invisible.
44
+ MAX_SPIN_FRACTION = 0.05
45
+
46
+ # The range lgpio accepts, for parity. Above roughly 1 kHz the duty cycle
47
+ # of a Ruby-generated waveform stops being accurate — measure before
48
+ # trusting it.
49
+ FREQUENCY_RANGE = (0.1..10_000)
50
+
51
+ # Open a channel, yield it, then close it.
52
+ # @return [SoftwarePWM, Object] the channel, or the block's value
53
+ def self.open(gpio, **)
54
+ pwm = new(gpio, **)
55
+ return pwm unless block_given?
56
+
57
+ begin
58
+ yield pwm
59
+ ensure
60
+ pwm.close
61
+ end
62
+ end
63
+
64
+ # @param gpio [Integer] GPIO line offset (BCM numbering)
65
+ # @param frequency [Numeric] Hz
66
+ # @param duty_cycle [Float] 0.0..1.0
67
+ # @param spin_us [Integer] microseconds to spin before each edge
68
+ # @param chip [Chip, nil] chip to share, or nil to open one
69
+ # @param consumer [String] name shown in the kernel's request list
70
+ def initialize(gpio, frequency: DEFAULT_FREQUENCY, duty_cycle: 0.0, spin_us: DEFAULT_SPIN_US,
71
+ chip: nil, consumer: "rgpio")
72
+ @gpio = gpio
73
+ @spin = validate_spin(spin_us) / 1_000_000.0
74
+ @mutex = Mutex.new
75
+ @frequency = validate_frequency(frequency)
76
+ @duty = validate_duty(duty_cycle)
77
+ @running = false
78
+ @closed = false
79
+ @owns_chip = chip.nil?
80
+ @chip = chip || Chip.new
81
+ @request = @chip.request_lines(
82
+ offsets: [gpio],
83
+ direction: :output,
84
+ initial_value: :inactive,
85
+ consumer: consumer
86
+ )
87
+ end
88
+
89
+ # @return [Integer] the GPIO line this channel drives
90
+ attr_reader :gpio
91
+
92
+ # @return [Numeric] frequency in Hz
93
+ attr_reader :frequency
94
+
95
+ # @return [Float] duty cycle as a ratio, 0.0..1.0
96
+ def duty_cycle
97
+ @duty
98
+ end
99
+
100
+ # Alias for parity with {HardwarePWM}, which calls it duty_ratio.
101
+ alias duty_ratio duty_cycle
102
+
103
+ # @return [Integer] microseconds spent spinning before each edge
104
+ def spin_us
105
+ (@spin * 1_000_000).round
106
+ end
107
+
108
+ def frequency=(hz)
109
+ hz = validate_frequency(hz)
110
+ @mutex.synchronize { @frequency = hz }
111
+ hz
112
+ end
113
+
114
+ def duty_cycle=(ratio)
115
+ ratio = validate_duty(ratio)
116
+ @mutex.synchronize { @duty = ratio }
117
+ ratio
118
+ end
119
+
120
+ # Set the high time directly, the way a servo is addressed.
121
+ # @param us [Numeric] pulse width in microseconds
122
+ def pulse_width_us=(us)
123
+ raise ArgumentError, "pulse width must not be negative, got #{us}" if us.negative?
124
+
125
+ self.duty_cycle = us / period_us
126
+ end
127
+
128
+ # @return [Float] current pulse width in microseconds
129
+ def pulse_width_us
130
+ @duty * period_us
131
+ end
132
+
133
+ # Start generating. Safe to call when already running.
134
+ def enable
135
+ raise Error, "SoftwarePWM on GPIO#{@gpio} is closed" if @closed
136
+ return self if @running
137
+
138
+ @running = true
139
+ @thread = Thread.new { generate }
140
+ self
141
+ end
142
+
143
+ # Stop generating and leave the line inactive.
144
+ def disable
145
+ return self unless @running
146
+
147
+ @running = false
148
+ @thread&.join
149
+ @thread = nil
150
+ @request.set_value(@gpio, :inactive)
151
+ self
152
+ end
153
+
154
+ def enabled?
155
+ @running
156
+ end
157
+
158
+ # Stop, release the line, and close the chip if this channel opened it.
159
+ # Safe to call multiple times.
160
+ def close
161
+ return if @closed
162
+
163
+ disable
164
+ @closed = true
165
+ @request.release
166
+ @chip.close if @owns_chip
167
+ end
168
+
169
+ def closed?
170
+ @closed
171
+ end
172
+
173
+ def inspect
174
+ format("#<%s gpio=%d frequency=%gHz duty_cycle=%.3f%s>",
175
+ self.class, @gpio, @frequency, @duty, @running ? " running" : "")
176
+ end
177
+
178
+ private
179
+
180
+ def now
181
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
182
+ end
183
+
184
+ def period_us
185
+ 1_000_000.0 / @frequency
186
+ end
187
+
188
+ # The generating loop. Deadlines are absolute so that a late wake-up does
189
+ # not push every later edge back by the same amount.
190
+ #
191
+ # The settings are re-read after the falling edge rather than before the
192
+ # rising one: any work done between the deadline and the rising edge is
193
+ # taken straight out of the high time, and reading them under the mutex
194
+ # cost a measurable 17 us of every pulse when it sat there.
195
+ def generate
196
+ period = high = nil
197
+ read_settings do |p, h|
198
+ period = p
199
+ high = h
200
+ end
201
+ spin = effective_spin(period)
202
+ deadline = now
203
+
204
+ while @running
205
+ # A line that is fully on or fully off needs no edges at all, and
206
+ # toggling it anyway would put a one-cycle glitch in the output.
207
+ if high <= 0 || high >= period
208
+ @request.set_value(@gpio, high <= 0 ? :inactive : :active)
209
+ else
210
+ # Time the high phase from the edge that actually happened, not from
211
+ # the nominal deadline: the rise lands a little after it (waking from
212
+ # the long low phase overshoots), and measuring from the deadline took
213
+ # that lateness straight out of the pulse — a measured 15 us of every
214
+ # one. The period stays locked to the absolute deadline below, so only
215
+ # the falling edge's phase drifts, which nothing can observe.
216
+ #
217
+ # The clock is read *before* each set_value so that the call's own
218
+ # latency cancels: it delays both edges by the same amount.
219
+ # Warm the call path with a write of the level the line already holds:
220
+ # the first set_value after waking from the long low phase is both slow
221
+ # and erratic (cold cache, possibly another core), and that lands
222
+ # entirely inside the pulse. Paying it before the clock read leaves the
223
+ # real edge to a warm path.
224
+ @request.set_value(@gpio, :inactive)
225
+ rise = now
226
+ @request.set_value(@gpio, :active)
227
+ wait_until(rise + high, spin)
228
+ @request.set_value(@gpio, :inactive)
229
+ end
230
+
231
+ deadline += period
232
+ read_settings do |p, h|
233
+ period = p
234
+ high = h
235
+ end
236
+ # If the thread was starved for longer than a whole cycle, catching up
237
+ # would mean running with no waits at all. Give up the lost cycles.
238
+ deadline = now if deadline < now - period
239
+ spin = effective_spin(period)
240
+ wait_until(deadline, spin)
241
+ end
242
+ rescue StandardError => e
243
+ warn "rgpio: SoftwarePWM on GPIO#{@gpio} stopped: #{e.class}: #{e.message}"
244
+ ensure
245
+ @running = false
246
+ end
247
+
248
+ # The spin is capped relative to the period so that a high frequency cannot
249
+ # turn the generating thread into a busy loop.
250
+ def effective_spin(period)
251
+ [@spin, period * MAX_SPIN_FRACTION].min
252
+ end
253
+
254
+ # Yield the period and high time in seconds. Kept allocation-free: building
255
+ # a two-element array here showed up as jitter.
256
+ def read_settings
257
+ @mutex.synchronize { yield 1.0 / @frequency, @duty / @frequency }
258
+ end
259
+
260
+ # Sleep for the bulk of the wait, then spin: sleep alone overshoots a
261
+ # microsecond-scale deadline by more than the pulse widths we are aiming for.
262
+ def wait_until(target, spin)
263
+ coarse = target - now - spin
264
+ sleep coarse if coarse.positive?
265
+ nil while now < target
266
+ end
267
+
268
+ def validate_frequency(hz)
269
+ unless hz.is_a?(Numeric) && FREQUENCY_RANGE.cover?(hz)
270
+ raise ArgumentError, "frequency must be in #{FREQUENCY_RANGE} Hz, got #{hz.inspect}"
271
+ end
272
+
273
+ hz
274
+ end
275
+
276
+ def validate_duty(ratio)
277
+ unless ratio.is_a?(Numeric) && (0.0..1.0).cover?(ratio)
278
+ raise ArgumentError, "duty cycle must be in 0.0..1.0, got #{ratio.inspect}"
279
+ end
280
+
281
+ ratio.to_f
282
+ end
283
+
284
+ def validate_spin(us)
285
+ raise ArgumentError, "spin_us must not be negative, got #{us}" unless us.is_a?(Numeric) && !us.negative?
286
+
287
+ us
288
+ end
289
+ end
290
+ end
data/lib/rgpio/spi.rb ADDED
@@ -0,0 +1,208 @@
1
+ require "fiddle"
2
+
3
+ module Rgpio
4
+ # A device on a Linux spidev bus (/dev/spidevB.D).
5
+ #
6
+ # Like {I2C}, this is ioctl work on a character device, so it needs no
7
+ # libgpiod and works even where Rgpio.available? is false.
8
+ #
9
+ # Usage (block form — recommended):
10
+ # Rgpio::SPI.open(bus: 0, device: 0, speed_hz: 1_000_000) do |spi|
11
+ # rx = spi.transfer([0x06, 0x00, 0x00])
12
+ # end
13
+ #
14
+ # SPI is full duplex: every transfer clocks the same number of bytes in each
15
+ # direction, so {#transfer} always answers with as many bytes as it was given.
16
+ # {#write} and {#read} are that same transfer with one direction ignored.
17
+ #
18
+ # The header bus is SPI0 (GPIO10 = MOSI, GPIO9 = MISO, GPIO11 = SCLK, GPIO8 =
19
+ # CE0, GPIO7 = CE1), and only appears once it is enabled — see README for the
20
+ # dtparam line.
21
+ class SPI
22
+ # ioctl numbers from <linux/spi/spidev.h>. They are built here rather than
23
+ # written out so the derivation stays checkable: direction in bits 30-31,
24
+ # payload size in 16-29, a 'k' magic in 8-15, and the request in 0-7.
25
+ IOC_MAGIC = 0x6b
26
+ IOC_WRITE = 1
27
+ IOC_READ = 2
28
+
29
+ # struct spi_ioc_transfer is { __u64 tx_buf; __u64 rx_buf; __u32 len;
30
+ # __u32 speed_hz; __u16 delay_usecs; __u8 bits_per_word, cs_change,
31
+ # tx_nbits, rx_nbits, word_delay_usecs, pad; } — exactly 32 bytes, and the
32
+ # header promises the same layout in 32- and 64-bit userspace. The two
33
+ # buffer fields are __u64 even where a pointer is 32 bits wide.
34
+ TRANSFER_SIZE = 32
35
+
36
+ DEFAULT_SPEED_HZ = 1_000_000
37
+ DEFAULT_BITS_PER_WORD = 8
38
+
39
+ # Clock polarity and phase, as the kernel numbers them.
40
+ MODE_RANGE = (0..3)
41
+
42
+ # @return [Integer] the ioctl request for a message of `count` transfers
43
+ def self.message_ioctl(count)
44
+ ioctl_number(IOC_WRITE, 0, TRANSFER_SIZE * count)
45
+ end
46
+
47
+ # @return [Integer] an encoded ioctl request
48
+ def self.ioctl_number(direction, request, size)
49
+ (direction << 30) | (size << 16) | (IOC_MAGIC << 8) | request
50
+ end
51
+
52
+ IOC_WR_MODE = ioctl_number(IOC_WRITE, 1, 1)
53
+ IOC_RD_MODE = ioctl_number(IOC_READ, 1, 1)
54
+ IOC_WR_LSB_FIRST = ioctl_number(IOC_WRITE, 2, 1)
55
+ IOC_WR_BITS_PER_WORD = ioctl_number(IOC_WRITE, 3, 1)
56
+ IOC_RD_BITS_PER_WORD = ioctl_number(IOC_READ, 3, 1)
57
+ IOC_WR_MAX_SPEED_HZ = ioctl_number(IOC_WRITE, 4, 4)
58
+ IOC_RD_MAX_SPEED_HZ = ioctl_number(IOC_READ, 4, 4)
59
+
60
+ # Pack a struct spi_ioc_transfer. Kept at class level so the layout can be
61
+ # checked without a bus present.
62
+ # @return [String]
63
+ def self.pack_transfer(tx_addr, rx_addr, len, speed_hz, bits_per_word: DEFAULT_BITS_PER_WORD,
64
+ delay_us: 0, cs_change: false)
65
+ # "Q", not "J": the two address fields are __u64 even where a pointer is
66
+ # only 32 bits wide.
67
+ [tx_addr, rx_addr].pack("Q2") +
68
+ [len, speed_hz].pack("LL") +
69
+ [delay_us].pack("S") +
70
+ [bits_per_word, cs_change ? 1 : 0, 0, 0, 0, 0].pack("C6")
71
+ end
72
+
73
+ # @return [Array<Array(Integer, Integer)>] [bus, device] of every spidev node
74
+ def self.devices
75
+ Dir.glob("/dev/spidev*").filter_map do |path|
76
+ match = path.match(%r{/dev/spidev(\d+)\.(\d+)\z})
77
+ [match[1].to_i, match[2].to_i] if match
78
+ end.sort
79
+ end
80
+
81
+ # Open a device, yielding it and closing it afterwards when a block is given.
82
+ # @return [SPI, Object] the device, or the block's value
83
+ def self.open(**)
84
+ spi = new(**)
85
+ return spi unless block_given?
86
+
87
+ begin
88
+ yield spi
89
+ ensure
90
+ spi.close
91
+ end
92
+ end
93
+
94
+ # @param bus [Integer] spidev bus number; 0 is the 40-pin header
95
+ # @param device [Integer] chip-select index on that bus
96
+ # @param speed_hz [Integer] clock rate
97
+ # @param mode [Integer] 0..3, clock polarity and phase
98
+ # @param bits_per_word [Integer] word size in bits
99
+ def initialize(bus: 0, device: 0, speed_hz: DEFAULT_SPEED_HZ, mode: 0,
100
+ bits_per_word: DEFAULT_BITS_PER_WORD)
101
+ @bus = bus
102
+ @device = device
103
+ @path = "/dev/spidev#{bus}.#{device}"
104
+ unless File.exist?(@path)
105
+ raise SPIError,
106
+ "#{@path} not found. Enable the header bus with `dtparam=spi=on` in /boot/firmware/config.txt"
107
+ end
108
+
109
+ @io = File.open(@path, "r+b")
110
+ @closed = false
111
+ self.mode = mode
112
+ self.bits_per_word = bits_per_word
113
+ self.speed_hz = speed_hz
114
+ end
115
+
116
+ # @return [Integer] spidev bus number
117
+ attr_reader :bus
118
+
119
+ # @return [Integer] chip-select index
120
+ attr_reader :device
121
+
122
+ # @return [String] path of the bus character device
123
+ attr_reader :path
124
+
125
+ # @return [Integer] clock rate in Hz
126
+ attr_reader :speed_hz
127
+
128
+ # @return [Integer] 0..3
129
+ attr_reader :mode
130
+
131
+ # @return [Integer] word size in bits
132
+ attr_reader :bits_per_word
133
+
134
+ def speed_hz=(hz)
135
+ raise ArgumentError, "speed_hz must be positive, got #{hz.inspect}" unless hz.is_a?(Integer) && hz.positive?
136
+
137
+ @io.ioctl(IOC_WR_MAX_SPEED_HZ, [hz].pack("L"))
138
+ @speed_hz = hz
139
+ end
140
+
141
+ def mode=(value)
142
+ raise ArgumentError, "mode must be in #{MODE_RANGE}, got #{value.inspect}" unless MODE_RANGE.cover?(value)
143
+
144
+ @io.ioctl(IOC_WR_MODE, [value].pack("C"))
145
+ @mode = value
146
+ end
147
+
148
+ def bits_per_word=(bits)
149
+ unless bits.is_a?(Integer) && bits.positive?
150
+ raise ArgumentError,
151
+ "bits_per_word must be positive, got #{bits.inspect}"
152
+ end
153
+
154
+ @io.ioctl(IOC_WR_BITS_PER_WORD, [bits].pack("C"))
155
+ @bits_per_word = bits
156
+ end
157
+
158
+ # Clock bytes out and in at the same time.
159
+ # @param bytes [Array<Integer>, String] bytes to send
160
+ # @param speed_hz [Integer, nil] override the clock for this transfer only
161
+ # @param delay_us [Integer] hold the chip select this long afterwards
162
+ # @return [Array<Integer>] the bytes that came back, one per byte sent
163
+ def transfer(bytes, speed_hz: nil, delay_us: 0)
164
+ out = Bytes.pack(bytes)
165
+ raise ArgumentError, "transfer needs at least one byte" if out.empty?
166
+
167
+ tx = buffer(out)
168
+ rx = Fiddle::Pointer.malloc(out.bytesize, Fiddle::RUBY_FREE)
169
+ message = self.class.pack_transfer(tx.to_i, rx.to_i, out.bytesize, speed_hz || @speed_hz,
170
+ bits_per_word: @bits_per_word, delay_us: delay_us)
171
+ @io.ioctl(self.class.message_ioctl(1), message)
172
+ rx[0, out.bytesize].unpack("C*")
173
+ end
174
+
175
+ # Send bytes, ignoring what comes back.
176
+ # @return [Integer] number of bytes sent
177
+ def write(*bytes)
178
+ transfer(bytes).size
179
+ end
180
+
181
+ # Clock in `count` bytes, sending zeros.
182
+ # @return [Array<Integer>]
183
+ def read(count)
184
+ raise ArgumentError, "count must be positive, got #{count}" unless count.positive?
185
+
186
+ transfer([0] * count)
187
+ end
188
+
189
+ def close
190
+ return if @closed
191
+
192
+ @closed = true
193
+ @io.close
194
+ end
195
+
196
+ def closed?
197
+ @closed
198
+ end
199
+
200
+ private
201
+
202
+ def buffer(str)
203
+ ptr = Fiddle::Pointer.malloc(str.bytesize, Fiddle::RUBY_FREE)
204
+ ptr[0, str.bytesize] = str
205
+ ptr
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,3 @@
1
+ module Rgpio
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/rgpio.rb ADDED
@@ -0,0 +1,99 @@
1
+ require_relative "rgpio/version"
2
+ require_relative "rgpio/native"
3
+ require_relative "rgpio/chip"
4
+ require_relative "rgpio/line_request"
5
+ require_relative "rgpio/pwm"
6
+ require_relative "rgpio/software_pwm"
7
+ require_relative "rgpio/bytes"
8
+ require_relative "rgpio/i2c"
9
+ require_relative "rgpio/spi"
10
+ require_relative "rgpio/devices/device"
11
+ require_relative "rgpio/devices/output_device"
12
+ require_relative "rgpio/devices/input_device"
13
+ require_relative "rgpio/devices/motor"
14
+ require_relative "rgpio/devices/pwm_channel"
15
+ require_relative "rgpio/devices/pwm_output_device"
16
+ require_relative "rgpio/devices/rgb_led"
17
+ require_relative "rgpio/devices/servo"
18
+ require_relative "rgpio/devices/adt7410"
19
+ require_relative "rgpio/devices/st7032"
20
+ require_relative "rgpio/devices/mcp3208"
21
+
22
+ # Ruby bindings for libgpiod v2 (Linux GPIO character device), bound through
23
+ # the stdlib `fiddle`. Targets Debian Trixie (libgpiod >= 2.1) on Raspberry Pi.
24
+ #
25
+ # Quick start — GPIO output:
26
+ # Rgpio::Chip.open do |chip|
27
+ # req = chip.request_lines(offsets: [17], direction: :output, consumer: "led")
28
+ # req.set_value(17, :active)
29
+ # sleep 1
30
+ # req.set_value(17, :inactive)
31
+ # req.release
32
+ # end
33
+ #
34
+ # Quick start — Hardware PWM (servo):
35
+ # Rgpio::HardwarePWM.open(gpio: 18) do |pwm|
36
+ # pwm.frequency = 50
37
+ # pwm.duty_cycle = 0.075
38
+ # pwm.enable
39
+ # sleep 2
40
+ # end
41
+ #
42
+ # Quick start — high-level devices:
43
+ # led = Rgpio::LED.new(4)
44
+ # led.on
45
+ #
46
+ # button = Rgpio::Button.new(17)
47
+ # button.when_pressed { puts "Pressed" }
48
+ # Rgpio.pause
49
+ #
50
+ # Quick start — I2C devices:
51
+ # puts Rgpio::ADT7410.new.temperature
52
+ #
53
+ # lcd = Rgpio::ST7032.new
54
+ # lcd.message = "Hello\nrgpio"
55
+ module Rgpio
56
+ # Raised for gem-level errors not covered by stdlib Errno classes.
57
+ class Error < StandardError; end
58
+
59
+ # Raised when libgpiod shared library cannot be loaded on the current system.
60
+ class NotAvailableError < Error; end
61
+
62
+ # Raised for PWM-related errors.
63
+ class PWMError < Error; end
64
+
65
+ # Raised for I2C-related errors not reported as an Errno by the kernel.
66
+ class I2CError < Error; end
67
+
68
+ # Raised for SPI-related errors not reported as an Errno by the kernel.
69
+ class SPIError < Error; end
70
+
71
+ # @return [Boolean] whether the libgpiod shared library is loaded
72
+ def self.available?
73
+ Native::LIBRARY_AVAILABLE
74
+ end
75
+
76
+ # Raise NotAvailableError unless libgpiod is loaded.
77
+ def self.assert_available!
78
+ return if available?
79
+
80
+ raise NotAvailableError,
81
+ "libgpiod shared library not found. " \
82
+ "Install on Debian/Raspbian: sudo apt install libgpiod3"
83
+ end
84
+
85
+ # @return [String, nil] libgpiod version string (e.g. "2.1.3"), or nil if unavailable
86
+ def self.version
87
+ return nil unless available?
88
+
89
+ Native.gpiod_api_version
90
+ end
91
+
92
+ # Block the main thread until Ctrl-C, letting device callbacks run.
93
+ # The Ruby counterpart of Python's signal.pause().
94
+ def self.pause
95
+ sleep
96
+ rescue Interrupt
97
+ nil
98
+ end
99
+ end