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
data/lib/rgpio/chip.rb
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# Represents an open GPIO chip (e.g. /dev/gpiochip0).
|
|
3
|
+
#
|
|
4
|
+
# Usage (block form — recommended, ensures close on exit):
|
|
5
|
+
# # Auto-detect the GPIO controller wired to the 40-pin header
|
|
6
|
+
# # (works on Pi 5 / Pi 4 / Pi Zero without code changes):
|
|
7
|
+
# Rgpio::Chip.open do |chip|
|
|
8
|
+
# request = chip.request_lines(offsets: [17], direction: :output)
|
|
9
|
+
# # ...
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # Or open a specific device explicitly:
|
|
13
|
+
# Rgpio::Chip.open("/dev/gpiochip0") { |chip| ... }
|
|
14
|
+
#
|
|
15
|
+
# Usage (manual):
|
|
16
|
+
# chip = Rgpio::Chip.new # auto-detect
|
|
17
|
+
# # ...
|
|
18
|
+
# chip.close
|
|
19
|
+
class Chip
|
|
20
|
+
# Glob matching every GPIO character device exposed by the kernel.
|
|
21
|
+
DEVICE_GLOB = "/dev/gpiochip*".freeze
|
|
22
|
+
|
|
23
|
+
# Labels of the GPIO controller wired to the 40-pin header, in detection
|
|
24
|
+
# priority order (newest SoC first). The label comes from the chip's
|
|
25
|
+
# info record (gpiod_chip_info_get_label) and is stable per SoC family:
|
|
26
|
+
#
|
|
27
|
+
# pinctrl-rp1 → Raspberry Pi 5 (RP1 I/O controller)
|
|
28
|
+
# pinctrl-bcm2711 → Raspberry Pi 4 / 400
|
|
29
|
+
# pinctrl-bcm2835 → Pi Zero / Zero W / Zero 2 W / Pi 1 / 2 / 3
|
|
30
|
+
#
|
|
31
|
+
# On Pi 5 the SoC also exposes several "gpio-brcmstb@..." chips that are
|
|
32
|
+
# NOT on the header — matching by label avoids selecting those.
|
|
33
|
+
HEADER_CHIP_LABELS = %w[
|
|
34
|
+
pinctrl-rp1
|
|
35
|
+
pinctrl-bcm2711
|
|
36
|
+
pinctrl-bcm2835
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# @param path [String, nil] path to the GPIO chip device. When nil
|
|
40
|
+
# (the default), the header GPIO controller is auto-detected.
|
|
41
|
+
def initialize(path = nil)
|
|
42
|
+
Rgpio.assert_available!
|
|
43
|
+
@path = path || self.class.detect_path
|
|
44
|
+
@chip_ptr = Native.gpiod_chip_open(@path)
|
|
45
|
+
return unless @chip_ptr.null?
|
|
46
|
+
|
|
47
|
+
raise SystemCallError.new("gpiod_chip_open(#{@path})", Native.errno)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Open a chip, yield it to the block, then close it.
|
|
51
|
+
# With no path, auto-detects the header GPIO controller.
|
|
52
|
+
def self.open(path = nil, &block)
|
|
53
|
+
chip = new(path)
|
|
54
|
+
block.call(chip)
|
|
55
|
+
ensure
|
|
56
|
+
chip&.close
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Enumerate every GPIO chip present on the system.
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<Hash>] one entry per chip, sorted by device index:
|
|
62
|
+
# { path:, name:, label:, num_lines: }
|
|
63
|
+
def self.list
|
|
64
|
+
Rgpio.assert_available!
|
|
65
|
+
Dir.glob(DEVICE_GLOB).sort_by { |p| device_index(p) }.filter_map do |path|
|
|
66
|
+
chip = new(path)
|
|
67
|
+
begin
|
|
68
|
+
{ path: path, name: chip.name, label: chip.label, num_lines: chip.num_lines }
|
|
69
|
+
ensure
|
|
70
|
+
chip.close
|
|
71
|
+
end
|
|
72
|
+
rescue SystemCallError
|
|
73
|
+
# Skip chips we cannot open (e.g. permissions, races) rather than abort.
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolve the device path of the GPIO controller wired to the 40-pin
|
|
79
|
+
# header. Pass `chips` to test the selection logic without hardware.
|
|
80
|
+
#
|
|
81
|
+
# @param chips [Array<Hash>] chip records as returned by {.list}
|
|
82
|
+
# @return [String] device path (e.g. "/dev/gpiochip0")
|
|
83
|
+
# @raise [NotAvailableError] when no usable GPIO chip is found
|
|
84
|
+
def self.detect_path(chips = list)
|
|
85
|
+
if chips.empty?
|
|
86
|
+
raise NotAvailableError,
|
|
87
|
+
"No GPIO chips found under #{DEVICE_GLOB}. Is this a Raspberry Pi?"
|
|
88
|
+
end
|
|
89
|
+
select_header_chip(chips).fetch(:path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Pick the header GPIO controller from a list of chip records.
|
|
93
|
+
# Prefers a known SoC label; falls back to the chip with the most lines
|
|
94
|
+
# (the header controller is, in practice, the largest one).
|
|
95
|
+
#
|
|
96
|
+
# @param chips [Array<Hash>] chip records as returned by {.list}
|
|
97
|
+
# @return [Hash] the selected chip record
|
|
98
|
+
def self.select_header_chip(chips)
|
|
99
|
+
HEADER_CHIP_LABELS.each do |label|
|
|
100
|
+
match = chips.find { |c| c[:label] == label }
|
|
101
|
+
return match if match
|
|
102
|
+
end
|
|
103
|
+
chips.max_by { |c| c[:num_lines] }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Numeric index of a gpiochip device path ("/dev/gpiochip10" → 10),
|
|
107
|
+
# so chips sort numerically rather than lexically.
|
|
108
|
+
def self.device_index(path)
|
|
109
|
+
File.basename(path).delete_prefix("gpiochip").to_i
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [String] device path this chip was opened with
|
|
113
|
+
attr_reader :path
|
|
114
|
+
|
|
115
|
+
# @return [String] kernel name of the chip (e.g. "gpiochip0")
|
|
116
|
+
def name
|
|
117
|
+
with_info { |info| Native.gpiod_chip_info_get_name(info) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [String] label identifying the GPIO controller
|
|
121
|
+
# (e.g. "pinctrl-rp1" on Raspberry Pi 5)
|
|
122
|
+
def label
|
|
123
|
+
with_info { |info| Native.gpiod_chip_info_get_label(info) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [Integer] total number of GPIO lines on this chip
|
|
127
|
+
def num_lines
|
|
128
|
+
with_info { |info| Native.gpiod_chip_info_get_num_lines(info) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Request one or more lines for exclusive use.
|
|
132
|
+
#
|
|
133
|
+
# @param offsets [Array<Integer>] GPIO line offsets to request
|
|
134
|
+
# @param direction [:input, :output] line direction
|
|
135
|
+
# @param edge [:none, :rising, :falling, :both] edge detection
|
|
136
|
+
# (only meaningful when direction is :input)
|
|
137
|
+
# @param bias [:as_is, :disabled, :pull_up, :pull_down]
|
|
138
|
+
# @param active_low [Boolean] invert active/inactive logic
|
|
139
|
+
# @param debounce_us [Integer] kernel-level debounce period in microseconds
|
|
140
|
+
# (0 = disabled). Suppresses bounces shorter than this window.
|
|
141
|
+
# Typical value for mechanical buttons: 5_000 (5 ms).
|
|
142
|
+
# @param initial_value [:active, :inactive] initial output value
|
|
143
|
+
# (only meaningful when direction is :output)
|
|
144
|
+
# @param consumer [String] name shown in kernel request list (optional)
|
|
145
|
+
# @return [LineRequest]
|
|
146
|
+
def request_lines(offsets:, direction:, edge: :none, bias: :as_is,
|
|
147
|
+
active_low: false, debounce_us: 0, initial_value: :inactive, consumer: nil)
|
|
148
|
+
assert_open!
|
|
149
|
+
|
|
150
|
+
settings_ptr = Native.gpiod_line_settings_new
|
|
151
|
+
raise Error, "gpiod_line_settings_new failed" if settings_ptr.null?
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
check! Native.gpiod_line_settings_set_direction(settings_ptr, direction_value(direction)),
|
|
155
|
+
"gpiod_line_settings_set_direction"
|
|
156
|
+
check! Native.gpiod_line_settings_set_edge_detection(settings_ptr, edge_value(edge)),
|
|
157
|
+
"gpiod_line_settings_set_edge_detection"
|
|
158
|
+
check! Native.gpiod_line_settings_set_bias(settings_ptr, bias_value(bias)),
|
|
159
|
+
"gpiod_line_settings_set_bias"
|
|
160
|
+
Native.gpiod_line_settings_set_active_low(settings_ptr, active_low)
|
|
161
|
+
if debounce_us&.positive?
|
|
162
|
+
check! Native.gpiod_line_settings_set_debounce_period_us(settings_ptr, debounce_us),
|
|
163
|
+
"gpiod_line_settings_set_debounce_period_us"
|
|
164
|
+
end
|
|
165
|
+
if direction == :output
|
|
166
|
+
check! Native.gpiod_line_settings_set_output_value(
|
|
167
|
+
settings_ptr, initial_value == :active ? Native::LINE_VALUE_ACTIVE : Native::LINE_VALUE_INACTIVE
|
|
168
|
+
), "gpiod_line_settings_set_output_value"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
line_config_ptr = Native.gpiod_line_config_new
|
|
172
|
+
raise Error, "gpiod_line_config_new failed" if line_config_ptr.null?
|
|
173
|
+
|
|
174
|
+
begin
|
|
175
|
+
offsets_arr = Array(offsets)
|
|
176
|
+
offsets_ptr = Native.uint32_buffer(offsets_arr)
|
|
177
|
+
|
|
178
|
+
check! Native.gpiod_line_config_add_line_settings(
|
|
179
|
+
line_config_ptr, offsets_ptr, offsets_arr.size, settings_ptr
|
|
180
|
+
), "gpiod_line_config_add_line_settings"
|
|
181
|
+
|
|
182
|
+
req_config_ptr = build_request_config(consumer)
|
|
183
|
+
begin
|
|
184
|
+
request_ptr = Native.gpiod_chip_request_lines(@chip_ptr, req_config_ptr, line_config_ptr)
|
|
185
|
+
raise SystemCallError.new("gpiod_chip_request_lines", Native.errno) if request_ptr.null?
|
|
186
|
+
|
|
187
|
+
LineRequest.new(request_ptr, offsets_arr)
|
|
188
|
+
ensure
|
|
189
|
+
Native.gpiod_request_config_free(req_config_ptr) unless req_config_ptr.null?
|
|
190
|
+
end
|
|
191
|
+
ensure
|
|
192
|
+
Native.gpiod_line_config_free(line_config_ptr)
|
|
193
|
+
end
|
|
194
|
+
ensure
|
|
195
|
+
Native.gpiod_line_settings_free(settings_ptr)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Close the chip and release all kernel resources.
|
|
200
|
+
# Safe to call multiple times.
|
|
201
|
+
def close
|
|
202
|
+
return unless @chip_ptr && !@chip_ptr.null?
|
|
203
|
+
|
|
204
|
+
Native.gpiod_chip_close(@chip_ptr)
|
|
205
|
+
@chip_ptr = nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def closed?
|
|
209
|
+
@chip_ptr.nil?
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def assert_open!
|
|
215
|
+
raise Error, "Chip is already closed" if closed?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def with_info
|
|
219
|
+
assert_open!
|
|
220
|
+
info = Native.gpiod_chip_get_info(@chip_ptr)
|
|
221
|
+
raise SystemCallError.new("gpiod_chip_get_info", Native.errno) if info.null?
|
|
222
|
+
|
|
223
|
+
begin
|
|
224
|
+
yield info
|
|
225
|
+
ensure
|
|
226
|
+
Native.gpiod_chip_info_free(info)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_request_config(consumer)
|
|
231
|
+
ptr = Native.gpiod_request_config_new
|
|
232
|
+
return Native::NULL if ptr.null?
|
|
233
|
+
|
|
234
|
+
Native.gpiod_request_config_set_consumer(ptr, consumer) if consumer
|
|
235
|
+
ptr
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def check!(ret, fn_name)
|
|
239
|
+
raise SystemCallError.new(fn_name, Native.errno) if ret == -1
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def direction_value(sym)
|
|
243
|
+
case sym
|
|
244
|
+
when :as_is then Native::LINE_DIRECTION_AS_IS
|
|
245
|
+
when :input then Native::LINE_DIRECTION_INPUT
|
|
246
|
+
when :output then Native::LINE_DIRECTION_OUTPUT
|
|
247
|
+
else raise ArgumentError, "Unknown direction: #{sym.inspect}"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def edge_value(sym)
|
|
252
|
+
case sym
|
|
253
|
+
when :none then Native::LINE_EDGE_NONE
|
|
254
|
+
when :rising then Native::LINE_EDGE_RISING
|
|
255
|
+
when :falling then Native::LINE_EDGE_FALLING
|
|
256
|
+
when :both then Native::LINE_EDGE_BOTH
|
|
257
|
+
else raise ArgumentError, "Unknown edge: #{sym.inspect}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def bias_value(sym)
|
|
262
|
+
case sym
|
|
263
|
+
when :as_is then Native::LINE_BIAS_AS_IS
|
|
264
|
+
when :disabled then Native::LINE_BIAS_DISABLED
|
|
265
|
+
when :pull_up then Native::LINE_BIAS_PULL_UP
|
|
266
|
+
when :pull_down then Native::LINE_BIAS_PULL_DOWN
|
|
267
|
+
else raise ArgumentError, "Unknown bias: #{sym.inspect}"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# An ADT7410 I2C temperature sensor (Analog Devices).
|
|
3
|
+
#
|
|
4
|
+
# sensor = Rgpio::ADT7410.new
|
|
5
|
+
# puts sensor.temperature # => 24.5 (degrees Celsius)
|
|
6
|
+
# sensor.close
|
|
7
|
+
#
|
|
8
|
+
# The address is set by the A1/A0 pins: 0x48 with both tied low (the default
|
|
9
|
+
# on the common breakout boards), through 0x4b with both high.
|
|
10
|
+
#
|
|
11
|
+
# The sensor powers up in 13-bit mode, which resolves 0.0625 degC and is what
|
|
12
|
+
# the datasheet calls the default; 16-bit mode resolves 0.0078 degC and is
|
|
13
|
+
# selected with `resolution: 16`.
|
|
14
|
+
class ADT7410
|
|
15
|
+
DEFAULT_ADDRESS = 0x48
|
|
16
|
+
|
|
17
|
+
# Register map (the subset this driver uses).
|
|
18
|
+
REG_TEMPERATURE = 0x00
|
|
19
|
+
REG_STATUS = 0x02
|
|
20
|
+
REG_CONFIG = 0x03
|
|
21
|
+
REG_ID = 0x0b
|
|
22
|
+
|
|
23
|
+
# Configuration register bit 7 selects 16-bit resolution.
|
|
24
|
+
CONFIG_RESOLUTION_16 = 0x80
|
|
25
|
+
|
|
26
|
+
# Upper five bits of the ID register are the manufacturer ID; the lower
|
|
27
|
+
# three are the silicon revision.
|
|
28
|
+
MANUFACTURER_ID = 0b11001
|
|
29
|
+
|
|
30
|
+
# Counts per degree Celsius, per resolution. In 13-bit mode the three low
|
|
31
|
+
# bits of the register hold the Tcrit/Thigh/Tlow flags instead of data.
|
|
32
|
+
COUNTS_PER_DEGREE = { 13 => 16.0, 16 => 128.0 }.freeze
|
|
33
|
+
|
|
34
|
+
# The first conversion after power-up takes 240 ms; reading before it has
|
|
35
|
+
# finished returns the 0 degC reset value rather than an error.
|
|
36
|
+
CONVERSION_TIME = 0.24
|
|
37
|
+
|
|
38
|
+
# @param address [Integer] 7-bit address, 0x48..0x4b
|
|
39
|
+
# @param bus [Integer] i2c-dev bus number
|
|
40
|
+
# @param resolution [Integer] 13 or 16 bits
|
|
41
|
+
# @param i2c [I2C, nil] an open device to share, or nil to open one
|
|
42
|
+
def initialize(address: DEFAULT_ADDRESS, bus: I2C::DEFAULT_BUS, resolution: 13, i2c: nil)
|
|
43
|
+
@owns_i2c = i2c.nil?
|
|
44
|
+
@i2c = i2c || I2C.new(address: address, bus: bus)
|
|
45
|
+
@closed = false
|
|
46
|
+
self.resolution = resolution
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [I2C] the bus device this sensor is read through
|
|
50
|
+
attr_reader :i2c
|
|
51
|
+
|
|
52
|
+
# @return [Integer] 13 or 16
|
|
53
|
+
attr_reader :resolution
|
|
54
|
+
|
|
55
|
+
# @param bits [Integer] 13 or 16
|
|
56
|
+
def resolution=(bits)
|
|
57
|
+
raise ArgumentError, "resolution must be 13 or 16, got #{bits.inspect}" unless COUNTS_PER_DEGREE.key?(bits)
|
|
58
|
+
|
|
59
|
+
config = @i2c.read_register(REG_CONFIG).first
|
|
60
|
+
config = bits == 16 ? config | CONFIG_RESOLUTION_16 : config & ~CONFIG_RESOLUTION_16
|
|
61
|
+
@i2c.write_register(REG_CONFIG, config & 0xff)
|
|
62
|
+
@resolution = bits
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Float] temperature in degrees Celsius
|
|
66
|
+
def temperature
|
|
67
|
+
msb, lsb = @i2c.read_register(REG_TEMPERATURE, 2)
|
|
68
|
+
self.class.convert(msb, lsb, @resolution)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias value temperature
|
|
72
|
+
|
|
73
|
+
# @return [Array<Integer>] the raw two temperature bytes, MSB first
|
|
74
|
+
def raw_temperature
|
|
75
|
+
@i2c.read_register(REG_TEMPERATURE, 2)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Integer] the ID register: manufacturer ID in bits 7..3,
|
|
79
|
+
# silicon revision in bits 2..0
|
|
80
|
+
def id
|
|
81
|
+
@i2c.read_register(REG_ID).first
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A cheap "is the sensor really there" check for examples and diagnostics.
|
|
85
|
+
# @return [Boolean] true when the ID register reports Analog Devices
|
|
86
|
+
def detected?
|
|
87
|
+
(id >> 3) == MANUFACTURER_ID
|
|
88
|
+
rescue SystemCallError
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Convert a raw register pair to degrees Celsius. Both resolutions are
|
|
93
|
+
# two's complement, so a set sign bit means the reading is below 0 degC.
|
|
94
|
+
# @return [Float]
|
|
95
|
+
def self.convert(msb, lsb, resolution = 13)
|
|
96
|
+
counts = COUNTS_PER_DEGREE.fetch(resolution)
|
|
97
|
+
raw = ((msb << 8) | lsb)
|
|
98
|
+
raw >>= 3 if resolution == 13
|
|
99
|
+
sign_bit = resolution == 13 ? 0x1000 : 0x8000
|
|
100
|
+
raw -= sign_bit * 2 if raw & sign_bit != 0
|
|
101
|
+
raw / counts
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Close the bus device, but only if this sensor opened it.
|
|
105
|
+
def close
|
|
106
|
+
return if @closed
|
|
107
|
+
|
|
108
|
+
@closed = true
|
|
109
|
+
@i2c.close if @owns_i2c
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def closed?
|
|
113
|
+
@closed
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# Base class for the high-level device API (LED, Button, MotionSensor, Motor).
|
|
3
|
+
#
|
|
4
|
+
# A device either borrows a Chip given by the caller or opens its own. Only a
|
|
5
|
+
# chip the device opened itself is closed by #close, so several devices can
|
|
6
|
+
# share one chip handle:
|
|
7
|
+
#
|
|
8
|
+
# chip = Rgpio::Chip.new
|
|
9
|
+
# red = Rgpio::LED.new(17, chip: chip)
|
|
10
|
+
# green = Rgpio::LED.new(27, chip: chip)
|
|
11
|
+
class Device
|
|
12
|
+
# @param chip [Chip, nil] chip to drive this device on; nil opens (and
|
|
13
|
+
# later closes) a chip of its own via auto-detection
|
|
14
|
+
def initialize(chip: nil)
|
|
15
|
+
@owns_chip = chip.nil?
|
|
16
|
+
@chip = chip || Chip.new
|
|
17
|
+
@closed = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Chip] the chip this device is driven on
|
|
21
|
+
attr_reader :chip
|
|
22
|
+
|
|
23
|
+
# Release the device's lines, and the chip too if this device opened it.
|
|
24
|
+
# Safe to call multiple times.
|
|
25
|
+
def close
|
|
26
|
+
return if @closed
|
|
27
|
+
|
|
28
|
+
@closed = true
|
|
29
|
+
release_resources
|
|
30
|
+
@chip.close if @owns_chip
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def closed?
|
|
34
|
+
@closed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def release_resources; end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# A single GPIO line read as an input, with optional edge callbacks.
|
|
3
|
+
#
|
|
4
|
+
# Callbacks run on a background thread that waits on kernel edge events, so
|
|
5
|
+
# the main thread is free (see {Rgpio.pause}). The thread is started by the
|
|
6
|
+
# first callback registration and stopped by #close.
|
|
7
|
+
class InputDevice < Device
|
|
8
|
+
# How long a single edge wait blocks before the watcher rechecks whether it
|
|
9
|
+
# has been asked to stop.
|
|
10
|
+
WATCH_TIMEOUT = 0.2
|
|
11
|
+
|
|
12
|
+
# @param gpio [Integer] GPIO line offset (BCM numbering)
|
|
13
|
+
# @param pull_up [Boolean, nil] internal bias: true = pull-up (wire the
|
|
14
|
+
# switch to GND), false = pull-down (wire it to 3.3 V),
|
|
15
|
+
# nil = no bias (external resistor)
|
|
16
|
+
# @param active_low [Boolean] when true, a low line reads as active
|
|
17
|
+
# @param debounce_us [Integer] kernel debounce window in microseconds
|
|
18
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
19
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
20
|
+
def initialize(gpio, pull_up: false, active_low: false, debounce_us: 0, chip: nil, consumer: "rgpio")
|
|
21
|
+
super(chip: chip)
|
|
22
|
+
@gpio = gpio
|
|
23
|
+
@callbacks = {}
|
|
24
|
+
@request = @chip.request_lines(
|
|
25
|
+
offsets: [gpio],
|
|
26
|
+
direction: :input,
|
|
27
|
+
edge: :both,
|
|
28
|
+
bias: bias_for(pull_up),
|
|
29
|
+
active_low: active_low,
|
|
30
|
+
debounce_us: debounce_us,
|
|
31
|
+
consumer: consumer
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Integer] the GPIO line offset this device reads
|
|
36
|
+
attr_reader :gpio
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] true when the line is at its active level
|
|
39
|
+
def value
|
|
40
|
+
@request.get_value(@gpio) == :active
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias active? value
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def bias_for(pull_up)
|
|
48
|
+
case pull_up
|
|
49
|
+
when true then :pull_up
|
|
50
|
+
when false then :pull_down
|
|
51
|
+
when nil then :disabled
|
|
52
|
+
else raise ArgumentError, "pull_up must be true, false or nil, got #{pull_up.inspect}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Register +block+ for the transition to :active or :inactive, and make sure
|
|
57
|
+
# the watcher thread is running.
|
|
58
|
+
def on_edge(state, &block)
|
|
59
|
+
raise ArgumentError, "a callback block is required" unless block
|
|
60
|
+
|
|
61
|
+
@callbacks[state] = block
|
|
62
|
+
start_watching
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def start_watching
|
|
67
|
+
return if @watcher
|
|
68
|
+
|
|
69
|
+
@watching = true
|
|
70
|
+
@watcher = Thread.new { watch_loop }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def watch_loop
|
|
74
|
+
while @watching
|
|
75
|
+
@request.read_edge_events(timeout: WATCH_TIMEOUT).each do |event|
|
|
76
|
+
dispatch(event[:type] == :rising ? :active : :inactive)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
warn "rgpio: edge watcher for GPIO#{@gpio} stopped: #{e.class}: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A raising callback must not take the watcher down with it, or the device
|
|
84
|
+
# would go quiet with no indication why.
|
|
85
|
+
def dispatch(state)
|
|
86
|
+
callback = @callbacks[state]
|
|
87
|
+
return unless callback
|
|
88
|
+
|
|
89
|
+
callback.call
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
warn "rgpio: #{self.class} callback for GPIO#{@gpio} raised #{e.class}: #{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Stop the watcher before releasing the request: a wait in progress holds a
|
|
95
|
+
# pointer into the request that libgpiod would free underneath it.
|
|
96
|
+
def release_resources
|
|
97
|
+
@watching = false
|
|
98
|
+
@watcher&.join
|
|
99
|
+
@watcher = nil
|
|
100
|
+
@request.release
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A push button or switch.
|
|
105
|
+
#
|
|
106
|
+
# button = Rgpio::Button.new(4)
|
|
107
|
+
# button.when_pressed { puts "Pressed" }
|
|
108
|
+
# button.when_released { puts "Released" }
|
|
109
|
+
# Rgpio.pause
|
|
110
|
+
#
|
|
111
|
+
# The default pull-down bias suits a switch wired between the GPIO line and
|
|
112
|
+
# 3.3 V. Pass pull_up: true for a switch wired to GND instead.
|
|
113
|
+
class Button < InputDevice
|
|
114
|
+
# Mechanical contacts bounce for a few milliseconds; without this one press
|
|
115
|
+
# fires the callback several times.
|
|
116
|
+
DEBOUNCE_US = 5_000
|
|
117
|
+
|
|
118
|
+
def initialize(gpio, pull_up: false, active_low: false, debounce_us: DEBOUNCE_US, chip: nil, consumer: "rgpio")
|
|
119
|
+
super
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def when_pressed(&)
|
|
123
|
+
on_edge(:active, &)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def when_released(&)
|
|
127
|
+
on_edge(:inactive, &)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
alias pressed? value
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A PIR motion sensor. Its output is a clean digital signal, so no debounce
|
|
134
|
+
# is applied.
|
|
135
|
+
#
|
|
136
|
+
# sensor = Rgpio::MotionSensor.new(4)
|
|
137
|
+
# sensor.when_motion { puts "motion detected!" }
|
|
138
|
+
# Rgpio.pause
|
|
139
|
+
class MotionSensor < InputDevice
|
|
140
|
+
def when_motion(&)
|
|
141
|
+
on_edge(:active, &)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def when_no_motion(&)
|
|
145
|
+
on_edge(:inactive, &)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
alias motion_detected? value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# An MCP3208 analogue-to-digital converter: eight 12-bit channels over SPI.
|
|
3
|
+
#
|
|
4
|
+
# adc = Rgpio::MCP3208.new
|
|
5
|
+
# adc.read(0) # => 0..4095, the raw code
|
|
6
|
+
# adc.voltage(0) # => volts, against the reference
|
|
7
|
+
# adc.close
|
|
8
|
+
#
|
|
9
|
+
# Wiring is SPI plus a reference: VDD and VREF to 3.3 V, AGND and DGND to
|
|
10
|
+
# ground, CLK/DOUT/DIN to SCLK/MISO/MOSI, CS to CE0.
|
|
11
|
+
#
|
|
12
|
+
# The MCP3204 is the same part with four channels and the same protocol, so it
|
|
13
|
+
# works through this class with `channels: 4`.
|
|
14
|
+
#
|
|
15
|
+
# Clock rate matters for correctness, not just speed: the datasheet allows
|
|
16
|
+
# 1 MHz at 2.7 V and 2 MHz at 5 V, and a converter clocked past its sampling
|
|
17
|
+
# rate returns values that look plausible and are wrong. The 1 MHz default is
|
|
18
|
+
# inside the envelope for a 3.3 V supply.
|
|
19
|
+
class MCP3208
|
|
20
|
+
DEFAULT_CHANNELS = 8
|
|
21
|
+
|
|
22
|
+
# 12 bits, so 4096 codes: code 4095 means the input is at the reference.
|
|
23
|
+
RESOLUTION = 4096
|
|
24
|
+
|
|
25
|
+
DEFAULT_REFERENCE_VOLTAGE = 3.3
|
|
26
|
+
DEFAULT_SPEED_HZ = 1_000_000
|
|
27
|
+
|
|
28
|
+
# First command byte: a start bit, then SGL/DIFF, then the top channel bit.
|
|
29
|
+
START_SINGLE = 0x06
|
|
30
|
+
START_DIFFERENTIAL = 0x04
|
|
31
|
+
|
|
32
|
+
# @param channels [Integer] 8 for an MCP3208, 4 for an MCP3204
|
|
33
|
+
# @param reference_voltage [Float] volts on the VREF pin
|
|
34
|
+
# @param bus [Integer] spidev bus number
|
|
35
|
+
# @param device [Integer] chip-select index
|
|
36
|
+
# @param speed_hz [Integer] clock rate
|
|
37
|
+
# @param spi [SPI, nil] an open bus device to share, or nil to open one
|
|
38
|
+
def initialize(channels: DEFAULT_CHANNELS, reference_voltage: DEFAULT_REFERENCE_VOLTAGE,
|
|
39
|
+
bus: 0, device: 0, speed_hz: DEFAULT_SPEED_HZ, spi: nil)
|
|
40
|
+
@channels = channels
|
|
41
|
+
@reference_voltage = reference_voltage.to_f
|
|
42
|
+
@owns_spi = spi.nil?
|
|
43
|
+
@spi = spi || SPI.new(bus: bus, device: device, speed_hz: speed_hz, mode: 0)
|
|
44
|
+
@closed = false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [SPI] the bus device readings go through
|
|
48
|
+
attr_reader :spi
|
|
49
|
+
|
|
50
|
+
# @return [Integer] how many input channels this part has
|
|
51
|
+
attr_reader :channels
|
|
52
|
+
|
|
53
|
+
# @return [Float] volts on the VREF pin
|
|
54
|
+
attr_reader :reference_voltage
|
|
55
|
+
|
|
56
|
+
# Read one channel.
|
|
57
|
+
# @param channel [Integer] 0...channels
|
|
58
|
+
# @param differential [Boolean] measure the channel pair rather than the
|
|
59
|
+
# single input: 0/1, 2/3, 4/5, 6/7, with the even channel
|
|
60
|
+
# as IN+ when `channel` is even
|
|
61
|
+
# @return [Integer] the raw code, 0..4095
|
|
62
|
+
def read(channel, differential: false)
|
|
63
|
+
raise Error, "MCP3208 is closed" if @closed
|
|
64
|
+
unless channel.is_a?(Integer) && (0...@channels).cover?(channel)
|
|
65
|
+
raise ArgumentError, "channel must be in 0...#{@channels}, got #{channel.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
start = differential ? START_DIFFERENTIAL : START_SINGLE
|
|
69
|
+
received = @spi.transfer([start | ((channel & 0x04) >> 2), (channel & 0x03) << 6, 0x00])
|
|
70
|
+
# The conversion arrives across the last two bytes: four null bits, then
|
|
71
|
+
# the twelve data bits, most significant first.
|
|
72
|
+
((received[1] & 0x0f) << 8) | received[2]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Float] the channel as a fraction of the reference, 0.0..1.0
|
|
76
|
+
def value(channel, differential: false)
|
|
77
|
+
read(channel, differential: differential) / (RESOLUTION - 1).to_f
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Float] the channel in volts
|
|
81
|
+
def voltage(channel, differential: false)
|
|
82
|
+
value(channel, differential: differential) * @reference_voltage
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Read every channel in turn. Not simultaneous — the part has one converter
|
|
86
|
+
# behind a multiplexer, so these are consecutive samples.
|
|
87
|
+
# @return [Array<Integer>] raw codes, channel 0 first
|
|
88
|
+
def read_all
|
|
89
|
+
Array.new(@channels) { |channel| read(channel) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Close the bus device, but only if this converter opened it.
|
|
93
|
+
def close
|
|
94
|
+
return if @closed
|
|
95
|
+
|
|
96
|
+
@closed = true
|
|
97
|
+
@spi.close if @owns_spi
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def closed?
|
|
101
|
+
@closed
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|