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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# A character LCD driven by a Sitronix ST7032 controller over I2C — the
|
|
3
|
+
# module in the Akizuki AQM0802 (8x2) and AQM1602 (16x2) boards.
|
|
4
|
+
#
|
|
5
|
+
# lcd = Rgpio::ST7032.new
|
|
6
|
+
# lcd.message = "Hello\nrgpio"
|
|
7
|
+
# lcd.close
|
|
8
|
+
#
|
|
9
|
+
# Every transfer is a control byte followed by payload: 0x00 for an
|
|
10
|
+
# instruction, 0x40 for display data.
|
|
11
|
+
#
|
|
12
|
+
# The boost converter and contrast settings below are the 3.3 V values. The
|
|
13
|
+
# controller has no way to read the panel back, so a blank display with the
|
|
14
|
+
# backlight on is almost always contrast: raise or lower it with #contrast=.
|
|
15
|
+
class ST7032
|
|
16
|
+
DEFAULT_ADDRESS = 0x3e
|
|
17
|
+
|
|
18
|
+
# Control byte: instruction vs. display data.
|
|
19
|
+
CONTROL_COMMAND = 0x00
|
|
20
|
+
CONTROL_DATA = 0x40
|
|
21
|
+
|
|
22
|
+
# Instruction set (IS = 0).
|
|
23
|
+
CMD_CLEAR = 0x01
|
|
24
|
+
CMD_HOME = 0x02
|
|
25
|
+
CMD_ENTRY_MODE = 0x06 # increment cursor, no display shift
|
|
26
|
+
CMD_DISPLAY_OFF = 0x08
|
|
27
|
+
CMD_DISPLAY_ON = 0x0c # display on, cursor off, blink off
|
|
28
|
+
CMD_FUNCTION_SET = 0x38 # 8-bit bus, 2 lines, normal instructions
|
|
29
|
+
CMD_SET_DDRAM = 0x80
|
|
30
|
+
|
|
31
|
+
# Extended instruction set (IS = 1).
|
|
32
|
+
CMD_FUNCTION_SET_EXT = 0x39
|
|
33
|
+
CMD_OSC_FREQUENCY = 0x14 # 1/5 bias, 183 kHz internal oscillator
|
|
34
|
+
CMD_FOLLOWER_ON = 0x6c # follower on, amplifier ratio 4
|
|
35
|
+
CMD_CONTRAST_LOW = 0x70 # | C3..C0
|
|
36
|
+
CMD_POWER_CONTRAST_HIGH = 0x50 # | Ion << 3 | Bon << 2 | C5..C4
|
|
37
|
+
BOOSTER_ON = 0x04
|
|
38
|
+
|
|
39
|
+
# Contrast is 6 bits split across two instructions. 0x20 suits 3.3 V panels.
|
|
40
|
+
CONTRAST_RANGE = (0..0x3f)
|
|
41
|
+
DEFAULT_CONTRAST = 0x20
|
|
42
|
+
|
|
43
|
+
# DDRAM address of each row's first column.
|
|
44
|
+
ROW_OFFSETS = [0x00, 0x40].freeze
|
|
45
|
+
|
|
46
|
+
# Instructions need 26.3 us to execute; clear and home need 1.08 ms. The
|
|
47
|
+
# follower circuit needs time to stabilise before the panel is driven.
|
|
48
|
+
EXECUTION_DELAY = 0.000_05
|
|
49
|
+
CLEAR_DELAY = 0.002
|
|
50
|
+
POWER_DELAY = 0.2
|
|
51
|
+
|
|
52
|
+
# Open a display, yielding it and closing it afterwards when a block is given.
|
|
53
|
+
# @return [ST7032, Object] the display, or the block's value
|
|
54
|
+
def self.open(**)
|
|
55
|
+
lcd = new(**)
|
|
56
|
+
return lcd unless block_given?
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
yield lcd
|
|
60
|
+
ensure
|
|
61
|
+
lcd.close
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param address [Integer] 7-bit address; the ST7032 is fixed at 0x3e
|
|
66
|
+
# @param bus [Integer] i2c-dev bus number
|
|
67
|
+
# @param columns [Integer] characters per row (8 for AQM0802, 16 for AQM1602)
|
|
68
|
+
# @param rows [Integer] number of rows
|
|
69
|
+
# @param contrast [Integer] 0..63
|
|
70
|
+
# @param booster [Boolean] enable the internal boost converter (needed at 3.3 V)
|
|
71
|
+
# @param i2c [I2C, nil] an open device to share, or nil to open one
|
|
72
|
+
def initialize(address: DEFAULT_ADDRESS, bus: I2C::DEFAULT_BUS, columns: 8, rows: 2,
|
|
73
|
+
contrast: DEFAULT_CONTRAST, booster: true, i2c: nil)
|
|
74
|
+
raise ArgumentError, "rows must be 1 or 2, got #{rows}" unless (1..ROW_OFFSETS.size).cover?(rows)
|
|
75
|
+
|
|
76
|
+
@owns_i2c = i2c.nil?
|
|
77
|
+
@i2c = i2c || I2C.new(address: address, bus: bus)
|
|
78
|
+
@columns = columns
|
|
79
|
+
@rows = rows
|
|
80
|
+
@contrast = validate_contrast(contrast)
|
|
81
|
+
@booster = booster
|
|
82
|
+
@closed = false
|
|
83
|
+
reset
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [I2C] the bus device this display is written through
|
|
87
|
+
attr_reader :i2c
|
|
88
|
+
|
|
89
|
+
# @return [Integer] characters per row
|
|
90
|
+
attr_reader :columns
|
|
91
|
+
|
|
92
|
+
# @return [Integer] number of rows
|
|
93
|
+
attr_reader :rows
|
|
94
|
+
|
|
95
|
+
# @return [Integer] current contrast setting, 0..63
|
|
96
|
+
attr_reader :contrast
|
|
97
|
+
|
|
98
|
+
# Run the power-on initialisation sequence. Called by .new; call it again
|
|
99
|
+
# after the panel has been power-cycled behind the driver's back.
|
|
100
|
+
def reset
|
|
101
|
+
command(CMD_FUNCTION_SET)
|
|
102
|
+
command(CMD_FUNCTION_SET_EXT)
|
|
103
|
+
command(CMD_OSC_FREQUENCY)
|
|
104
|
+
apply_contrast
|
|
105
|
+
command(CMD_FOLLOWER_ON)
|
|
106
|
+
sleep POWER_DELAY
|
|
107
|
+
command(CMD_FUNCTION_SET)
|
|
108
|
+
command(CMD_DISPLAY_ON)
|
|
109
|
+
command(CMD_ENTRY_MODE)
|
|
110
|
+
clear
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Blank the display and return the cursor to the top left.
|
|
115
|
+
def clear
|
|
116
|
+
command(CMD_CLEAR, delay: CLEAR_DELAY)
|
|
117
|
+
@col = 0
|
|
118
|
+
@row = 0
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Return the cursor to the top left, leaving the contents alone.
|
|
123
|
+
def home
|
|
124
|
+
command(CMD_HOME, delay: CLEAR_DELAY)
|
|
125
|
+
@col = 0
|
|
126
|
+
@row = 0
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Move the cursor. Out-of-range positions raise rather than wrapping, since
|
|
131
|
+
# the DDRAM address they would land on is rarely what the caller meant.
|
|
132
|
+
# @param col [Integer] 0-based column
|
|
133
|
+
# @param row [Integer] 0-based row
|
|
134
|
+
def move_to(col, row = 0)
|
|
135
|
+
raise ArgumentError, "column must be in 0...#{@columns}, got #{col}" unless (0...@columns).cover?(col)
|
|
136
|
+
raise ArgumentError, "row must be in 0...#{@rows}, got #{row}" unless (0...@rows).cover?(row)
|
|
137
|
+
|
|
138
|
+
command(CMD_SET_DDRAM | (ROW_OFFSETS[row] + col))
|
|
139
|
+
@col = col
|
|
140
|
+
@row = row
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
alias set_cursor move_to
|
|
145
|
+
|
|
146
|
+
# Write text at the cursor. A newline moves to the start of the next row;
|
|
147
|
+
# text that would run past the last column of a row is dropped, as the
|
|
148
|
+
# controller would otherwise scatter it into the other row's DDRAM.
|
|
149
|
+
def print(text)
|
|
150
|
+
text.to_s.split("\n", -1).each_with_index do |line, index|
|
|
151
|
+
if index.positive?
|
|
152
|
+
break if @row + 1 >= @rows
|
|
153
|
+
|
|
154
|
+
move_to(0, @row + 1)
|
|
155
|
+
end
|
|
156
|
+
write_data(line)
|
|
157
|
+
end
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Replace the whole display with +text+ (clear, then print).
|
|
162
|
+
def message=(text)
|
|
163
|
+
clear
|
|
164
|
+
print(text)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def display_on
|
|
168
|
+
command(CMD_DISPLAY_ON)
|
|
169
|
+
self
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def display_off
|
|
173
|
+
command(CMD_DISPLAY_OFF)
|
|
174
|
+
self
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @param value [Integer] 0..63
|
|
178
|
+
def contrast=(value)
|
|
179
|
+
@contrast = validate_contrast(value)
|
|
180
|
+
command(CMD_FUNCTION_SET_EXT)
|
|
181
|
+
apply_contrast
|
|
182
|
+
command(CMD_FUNCTION_SET)
|
|
183
|
+
value
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Send a raw instruction byte.
|
|
187
|
+
def command(byte, delay: EXECUTION_DELAY)
|
|
188
|
+
@i2c.write(CONTROL_COMMAND, byte)
|
|
189
|
+
sleep delay
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Send display data (a String is taken as its bytes — the ST7032 character
|
|
194
|
+
# ROM is ASCII in 0x20..0x7d, so non-ASCII text needs a custom mapping).
|
|
195
|
+
def write_data(text)
|
|
196
|
+
bytes = text.is_a?(String) ? text.b.bytes : Array(text)
|
|
197
|
+
bytes = bytes.first(@columns - @col)
|
|
198
|
+
return self if bytes.empty?
|
|
199
|
+
|
|
200
|
+
@i2c.write(CONTROL_DATA, bytes)
|
|
201
|
+
@col += bytes.size
|
|
202
|
+
sleep EXECUTION_DELAY
|
|
203
|
+
self
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Close the bus device, but only if this display opened it. The panel keeps
|
|
207
|
+
# showing whatever was written last.
|
|
208
|
+
def close
|
|
209
|
+
return if @closed
|
|
210
|
+
|
|
211
|
+
@closed = true
|
|
212
|
+
@i2c.close if @owns_i2c
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def closed?
|
|
216
|
+
@closed
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def validate_contrast(value)
|
|
222
|
+
raise ArgumentError, "contrast must be in 0..63, got #{value}" unless CONTRAST_RANGE.cover?(value)
|
|
223
|
+
|
|
224
|
+
value
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Both halves of the contrast value live in the extended instruction set,
|
|
228
|
+
# so the caller must have selected it first.
|
|
229
|
+
def apply_contrast
|
|
230
|
+
command(CMD_CONTRAST_LOW | (@contrast & 0x0f))
|
|
231
|
+
command(CMD_POWER_CONTRAST_HIGH | (@booster ? BOOSTER_ON : 0) | ((@contrast >> 4) & 0x03))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
data/lib/rgpio/i2c.rb
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
require "fiddle"
|
|
2
|
+
|
|
3
|
+
module Rgpio
|
|
4
|
+
# An I2C device on a Linux i2c-dev bus (/dev/i2c-N).
|
|
5
|
+
#
|
|
6
|
+
# No libgpiod involved: the kernel exposes the whole bus through a character
|
|
7
|
+
# device, so this works even where Rgpio.available? is false.
|
|
8
|
+
#
|
|
9
|
+
# Usage (block form — recommended):
|
|
10
|
+
# Rgpio::I2C.open(address: 0x48) do |i2c|
|
|
11
|
+
# msb, lsb = i2c.read_register(0x00, 2)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Usage (manual):
|
|
15
|
+
# i2c = Rgpio::I2C.new(address: 0x3e)
|
|
16
|
+
# i2c.write(0x00, 0x38)
|
|
17
|
+
# i2c.close
|
|
18
|
+
#
|
|
19
|
+
# The I2C bus on the 40-pin header (GPIO2 = SDA, GPIO3 = SCL) is bus 1, and
|
|
20
|
+
# only appears once it is enabled — see README for the dtparam line.
|
|
21
|
+
class I2C
|
|
22
|
+
# ioctl numbers from <linux/i2c-dev.h>.
|
|
23
|
+
I2C_SLAVE = 0x0703
|
|
24
|
+
I2C_SLAVE_FORCE = 0x0706
|
|
25
|
+
I2C_RDWR = 0x0707
|
|
26
|
+
|
|
27
|
+
# i2c_msg flag: this message is a read (from <linux/i2c.h>).
|
|
28
|
+
I2C_M_RD = 0x0001
|
|
29
|
+
|
|
30
|
+
# The 40-pin header bus. Bus 0 exists on some boards but is reserved for
|
|
31
|
+
# HAT EEPROMs and camera/display peripherals.
|
|
32
|
+
DEFAULT_BUS = 1
|
|
33
|
+
|
|
34
|
+
# Valid 7-bit addressing range: below 0x08 and above 0x77 is reserved.
|
|
35
|
+
ADDRESS_RANGE = (0x08..0x77)
|
|
36
|
+
|
|
37
|
+
# struct i2c_msg is { __u16 addr; __u16 flags; __u16 len; __u8 *buf; }:
|
|
38
|
+
# three shorts, then the pointer on its natural alignment (offset 8 on both
|
|
39
|
+
# 32- and 64-bit ARM, since the shorts are padded out to it).
|
|
40
|
+
MSG_BUF_OFFSET = 8
|
|
41
|
+
MSG_SIZE = MSG_BUF_OFFSET + Fiddle::SIZEOF_VOIDP
|
|
42
|
+
|
|
43
|
+
# struct i2c_rdwr_ioctl_data is { struct i2c_msg *msgs; __u32 nmsgs; },
|
|
44
|
+
# rounded up to pointer alignment.
|
|
45
|
+
RDWR_SIZE = Fiddle::SIZEOF_VOIDP * 2
|
|
46
|
+
|
|
47
|
+
# Pack a struct i2c_msg. Kept at class level so the layout can be checked
|
|
48
|
+
# without a bus present.
|
|
49
|
+
# @param buf_addr [Integer] address of the message's data buffer
|
|
50
|
+
# @return [String]
|
|
51
|
+
def self.pack_msg(address, flags, len, buf_addr)
|
|
52
|
+
[address, flags, len].pack("SSS").ljust(MSG_BUF_OFFSET, "\0") + [buf_addr].pack("J")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Pack a struct i2c_rdwr_ioctl_data pointing at `count` messages.
|
|
56
|
+
# @param msgs_addr [Integer] address of the message array
|
|
57
|
+
# @return [String]
|
|
58
|
+
def self.pack_rdwr(msgs_addr, count)
|
|
59
|
+
([msgs_addr].pack("J") + [count].pack("L")).ljust(RDWR_SIZE, "\0")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Array<Integer>] bus numbers with a /dev/i2c-N node, ascending
|
|
63
|
+
def self.buses
|
|
64
|
+
Dir.glob("/dev/i2c-*").filter_map { |path| path[%r{/dev/i2c-(\d+)\z}, 1]&.to_i }.sort
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Open a device, yielding it and closing it afterwards when a block is given.
|
|
68
|
+
# @return [I2C, Object] the device, or the block's value
|
|
69
|
+
def self.open(address:, bus: DEFAULT_BUS)
|
|
70
|
+
i2c = new(address: address, bus: bus)
|
|
71
|
+
return i2c unless block_given?
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
yield i2c
|
|
75
|
+
ensure
|
|
76
|
+
i2c.close
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param address [Integer] 7-bit device address (e.g. 0x48)
|
|
81
|
+
# @param bus [Integer] i2c-dev bus number; 1 is the 40-pin header
|
|
82
|
+
# @param force [Boolean] claim the address even if a kernel driver holds it
|
|
83
|
+
def initialize(address:, bus: DEFAULT_BUS, force: false)
|
|
84
|
+
unless ADDRESS_RANGE.cover?(address)
|
|
85
|
+
raise ArgumentError,
|
|
86
|
+
format("address must be in 0x%02x..0x%02x, got 0x%02x", ADDRESS_RANGE.first, ADDRESS_RANGE.last, address)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@address = address
|
|
90
|
+
@bus = bus
|
|
91
|
+
@path = "/dev/i2c-#{bus}"
|
|
92
|
+
unless File.exist?(@path)
|
|
93
|
+
raise I2CError,
|
|
94
|
+
"#{@path} not found. Enable the header bus with `dtparam=i2c_arm=on` in /boot/firmware/config.txt"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@io = File.open(@path, "r+b")
|
|
98
|
+
@io.ioctl(force ? I2C_SLAVE_FORCE : I2C_SLAVE, @address)
|
|
99
|
+
@closed = false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Integer] 7-bit device address
|
|
103
|
+
attr_reader :address
|
|
104
|
+
|
|
105
|
+
# @return [Integer] i2c-dev bus number
|
|
106
|
+
attr_reader :bus
|
|
107
|
+
|
|
108
|
+
# @return [String] path of the bus character device
|
|
109
|
+
attr_reader :path
|
|
110
|
+
|
|
111
|
+
# Write bytes in a single transaction.
|
|
112
|
+
# @param bytes [Array<Integer>, String] byte values, or a packed String
|
|
113
|
+
# @return [Integer] number of bytes written
|
|
114
|
+
def write(*bytes)
|
|
115
|
+
@io.syswrite(Bytes.pack(bytes))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Read bytes in a single transaction.
|
|
119
|
+
# @param count [Integer] how many bytes to read
|
|
120
|
+
# @return [Array<Integer>]
|
|
121
|
+
def read(count)
|
|
122
|
+
@io.sysread(count).unpack("C*")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Write, then read without releasing the bus (repeated START). Devices with
|
|
126
|
+
# an address pointer need this: a STOP between the two halves lets another
|
|
127
|
+
# master move the pointer in between.
|
|
128
|
+
# @param bytes [Array<Integer>, String] bytes to write first
|
|
129
|
+
# @param count [Integer] how many bytes to read back
|
|
130
|
+
# @return [Array<Integer>]
|
|
131
|
+
def write_read(bytes, count)
|
|
132
|
+
out = Bytes.pack(bytes)
|
|
133
|
+
raise ArgumentError, "write_read needs at least one byte to write" if out.empty?
|
|
134
|
+
raise ArgumentError, "count must be positive, got #{count}" unless count.positive?
|
|
135
|
+
|
|
136
|
+
wbuf = buffer(out)
|
|
137
|
+
rbuf = Fiddle::Pointer.malloc(count, Fiddle::RUBY_FREE)
|
|
138
|
+
msgs = buffer(self.class.pack_msg(@address, 0, out.bytesize, wbuf.to_i) +
|
|
139
|
+
self.class.pack_msg(@address, I2C_M_RD, count, rbuf.to_i))
|
|
140
|
+
@io.ioctl(I2C_RDWR, self.class.pack_rdwr(msgs.to_i, 2))
|
|
141
|
+
rbuf[0, count].unpack("C*")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Read `count` bytes from a register, addressing it with a repeated START.
|
|
145
|
+
# @return [Array<Integer>]
|
|
146
|
+
def read_register(register, count = 1)
|
|
147
|
+
write_read([register], count)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Write bytes to a register in one transaction.
|
|
151
|
+
# @return [Integer] number of bytes written
|
|
152
|
+
def write_register(register, *bytes)
|
|
153
|
+
write(register, *bytes)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def close
|
|
157
|
+
return if @closed
|
|
158
|
+
|
|
159
|
+
@closed = true
|
|
160
|
+
@io.close
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def closed?
|
|
164
|
+
@closed
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def buffer(str)
|
|
170
|
+
ptr = Fiddle::Pointer.malloc(str.bytesize, Fiddle::RUBY_FREE)
|
|
171
|
+
ptr[0, str.bytesize] = str
|
|
172
|
+
ptr
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# Holds an active kernel line request returned by Chip#request_lines.
|
|
3
|
+
# Must be released when done via #release (or the block form of Chip.open).
|
|
4
|
+
#
|
|
5
|
+
# Example — output:
|
|
6
|
+
# request = chip.request_lines(offsets: [17], direction: :output)
|
|
7
|
+
# request.set_value(17, :active)
|
|
8
|
+
# request.release
|
|
9
|
+
#
|
|
10
|
+
# Example — input with edge detection (blocking):
|
|
11
|
+
# request = chip.request_lines(offsets: [27], direction: :input,
|
|
12
|
+
# edge: :both, bias: :pull_up)
|
|
13
|
+
# loop do
|
|
14
|
+
# events = request.read_edge_events(timeout: 5.0)
|
|
15
|
+
# events.each { |e| puts "#{e[:type]} on offset #{e[:offset]}" }
|
|
16
|
+
# end
|
|
17
|
+
# request.release
|
|
18
|
+
class LineRequest
|
|
19
|
+
# @param request_ptr [Fiddle::Pointer] raw gpiod_line_request*
|
|
20
|
+
# @param offsets [Array<Integer>] offsets included in this request
|
|
21
|
+
def initialize(request_ptr, offsets)
|
|
22
|
+
@request_ptr = request_ptr
|
|
23
|
+
@offsets = offsets.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :offsets
|
|
27
|
+
|
|
28
|
+
# Read the current value of a single line.
|
|
29
|
+
#
|
|
30
|
+
# @param offset [Integer] GPIO line offset (must be in #offsets)
|
|
31
|
+
# @return [:active, :inactive]
|
|
32
|
+
# @raise [SystemCallError] on error
|
|
33
|
+
def get_value(offset)
|
|
34
|
+
assert_active!
|
|
35
|
+
result = Native.gpiod_line_request_get_value(@request_ptr, offset)
|
|
36
|
+
case result
|
|
37
|
+
when Native::LINE_VALUE_ACTIVE then :active
|
|
38
|
+
when Native::LINE_VALUE_INACTIVE then :inactive
|
|
39
|
+
else
|
|
40
|
+
raise SystemCallError.new("gpiod_line_request_get_value(offset=#{offset})", Native.errno)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Set the output value of a single line.
|
|
45
|
+
#
|
|
46
|
+
# @param offset [Integer] GPIO line offset
|
|
47
|
+
# @param value [:active, :inactive, 1, 0] desired output level
|
|
48
|
+
# @raise [SystemCallError] on error
|
|
49
|
+
def set_value(offset, value)
|
|
50
|
+
assert_active!
|
|
51
|
+
v = normalize_value(value)
|
|
52
|
+
ret = Native.gpiod_line_request_set_value(@request_ptr, offset, v)
|
|
53
|
+
raise SystemCallError.new("gpiod_line_request_set_value(offset=#{offset})", Native.errno) if ret == -1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Read several lines atomically in a single ioctl.
|
|
57
|
+
#
|
|
58
|
+
# @param offsets [Array<Integer>] offsets to read; defaults to every offset
|
|
59
|
+
# in this request. Must be a subset of #offsets.
|
|
60
|
+
# @return [Hash{Integer=>Symbol}] offset => :active / :inactive, in the
|
|
61
|
+
# order given
|
|
62
|
+
# @raise [SystemCallError] on error
|
|
63
|
+
def get_values(offsets = @offsets)
|
|
64
|
+
assert_active!
|
|
65
|
+
offs = Array(offsets)
|
|
66
|
+
return {} if offs.empty?
|
|
67
|
+
|
|
68
|
+
offsets_ptr = Native.uint32_buffer(offs)
|
|
69
|
+
values_ptr = Native.int_output_buffer(offs.size)
|
|
70
|
+
ret = Native.gpiod_line_request_get_values_subset(@request_ptr, offs.size, offsets_ptr, values_ptr)
|
|
71
|
+
raise SystemCallError.new("gpiod_line_request_get_values_subset", Native.errno) if ret == -1
|
|
72
|
+
|
|
73
|
+
raw = Native.read_int_buffer(values_ptr, offs.size)
|
|
74
|
+
offs.zip(raw).to_h { |offset, value| [offset, decode_value(value, offset)] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set several output lines atomically in a single ioctl.
|
|
78
|
+
#
|
|
79
|
+
# @param values [Hash{Integer=>Object}] offset => desired level
|
|
80
|
+
# (:active/:inactive/1/0/true/false). Offsets must be a subset
|
|
81
|
+
# of #offsets.
|
|
82
|
+
# @raise [ArgumentError] when values is not a Hash
|
|
83
|
+
# @raise [SystemCallError] on error
|
|
84
|
+
def set_values(values)
|
|
85
|
+
assert_active!
|
|
86
|
+
raise ArgumentError, "set_values expects a Hash of offset => value, got #{values.class}" unless values.is_a?(Hash)
|
|
87
|
+
return if values.empty?
|
|
88
|
+
|
|
89
|
+
offs = values.keys
|
|
90
|
+
vals = values.values.map { |v| normalize_value(v) }
|
|
91
|
+
offsets_ptr = Native.uint32_buffer(offs)
|
|
92
|
+
values_ptr = Native.int_buffer(vals)
|
|
93
|
+
ret = Native.gpiod_line_request_set_values_subset(@request_ptr, offs.size, offsets_ptr, values_ptr)
|
|
94
|
+
raise SystemCallError.new("gpiod_line_request_set_values_subset", Native.errno) if ret == -1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Wait for edge events on any line in this request.
|
|
98
|
+
#
|
|
99
|
+
# @param timeout [Float, nil] seconds to wait; nil = block indefinitely;
|
|
100
|
+
# 0 = non-blocking poll
|
|
101
|
+
# @return [Boolean] true if at least one event is ready
|
|
102
|
+
# @raise [SystemCallError] on error
|
|
103
|
+
def wait_edge_events(timeout: nil)
|
|
104
|
+
assert_active!
|
|
105
|
+
timeout_ns = if timeout.nil?
|
|
106
|
+
-1
|
|
107
|
+
else
|
|
108
|
+
(timeout * 1_000_000_000).to_i
|
|
109
|
+
end
|
|
110
|
+
ret = Native.gpiod_line_request_wait_edge_events(@request_ptr, timeout_ns)
|
|
111
|
+
raise SystemCallError.new("gpiod_line_request_wait_edge_events", Native.errno) if ret == -1
|
|
112
|
+
|
|
113
|
+
ret == 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Read pending edge events into an array.
|
|
117
|
+
# Typically called after #wait_edge_events returns true.
|
|
118
|
+
#
|
|
119
|
+
# @param timeout [Float, nil] wait up to this many seconds before reading
|
|
120
|
+
# @param capacity [Integer] maximum events to read per call
|
|
121
|
+
# @return [Array<Hash>] array of event hashes:
|
|
122
|
+
# { type: :rising | :falling, offset: Integer, timestamp_ns: Integer }
|
|
123
|
+
def read_edge_events(timeout: nil, capacity: 16)
|
|
124
|
+
assert_active!
|
|
125
|
+
return [] unless wait_edge_events(timeout: timeout)
|
|
126
|
+
|
|
127
|
+
buf = nil
|
|
128
|
+
begin
|
|
129
|
+
buf = Native.gpiod_edge_event_buffer_new(capacity)
|
|
130
|
+
raise Error, "gpiod_edge_event_buffer_new failed" if buf.null?
|
|
131
|
+
|
|
132
|
+
n = Native.gpiod_line_request_read_edge_events(@request_ptr, buf)
|
|
133
|
+
raise SystemCallError.new("gpiod_line_request_read_edge_events", Native.errno) if n == -1
|
|
134
|
+
|
|
135
|
+
Array.new(n) do |i|
|
|
136
|
+
ev = Native.gpiod_edge_event_buffer_get_event(buf, i)
|
|
137
|
+
type_int = Native.gpiod_edge_event_get_event_type(ev)
|
|
138
|
+
{
|
|
139
|
+
type: type_int == Native::EDGE_EVENT_RISING_EDGE ? :rising : :falling,
|
|
140
|
+
offset: Native.gpiod_edge_event_get_line_offset(ev),
|
|
141
|
+
timestamp_ns: Native.gpiod_edge_event_get_timestamp_ns(ev),
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
ensure
|
|
145
|
+
Native.gpiod_edge_event_buffer_free(buf) if buf && !buf.null?
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Release the kernel line request. Safe to call multiple times.
|
|
150
|
+
def release
|
|
151
|
+
return unless @request_ptr && !@request_ptr.null?
|
|
152
|
+
|
|
153
|
+
Native.gpiod_line_request_release(@request_ptr)
|
|
154
|
+
@request_ptr = nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def released?
|
|
158
|
+
@request_ptr.nil?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def assert_active!
|
|
164
|
+
raise Error, "LineRequest has already been released" if released?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def normalize_value(value)
|
|
168
|
+
case value
|
|
169
|
+
when :active, 1, true then Native::LINE_VALUE_ACTIVE
|
|
170
|
+
when :inactive, 0, false then Native::LINE_VALUE_INACTIVE
|
|
171
|
+
else raise ArgumentError, "Unknown line value: #{value.inspect}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def decode_value(int, offset)
|
|
176
|
+
case int
|
|
177
|
+
when Native::LINE_VALUE_ACTIVE then :active
|
|
178
|
+
when Native::LINE_VALUE_INACTIVE then :inactive
|
|
179
|
+
else
|
|
180
|
+
raise SystemCallError.new("gpiod_line_request_get_values_subset(offset=#{offset})", Native.errno)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|