lgpio 0.1.3 → 0.1.5
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 +4 -4
- data/.gitignore +1 -0
- data/README.md +27 -16
- data/examples/i2c_aht10.rb +1 -1
- data/examples/i2c_aht10_zip.rb +1 -1
- data/examples/i2c_ssd1306_bench.rb +1 -1
- data/examples/servo.rb +36 -0
- data/examples/spi_ws2812.rb +4 -34
- data/examples/spi_ws2812_bounce.rb +44 -0
- data/ext/lgpio/lgpio.c +37 -0
- data/lib/lgpio/hardware_pwm.rb +70 -0
- data/lib/lgpio/positional_servo.rb +28 -0
- data/lib/lgpio/version.rb +1 -1
- data/lib/lgpio.rb +3 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 984afd62543a10d3fc0b6d2a9a581b05b4d9bf7c159d2747f990b2247b59aef1
|
4
|
+
data.tar.gz: d2431adb24613fdd42b9f9f06a428c399fe48be2e215a13463237a798113630c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f32ceb3af8218d26f1f5713302a9891365af498fe76e2a750037f2ae10a437b747b40167e9fd9c5170fc28696d90c9244ce5d9a36269eba0d0c827a79be47fcd
|
7
|
+
data.tar.gz: b167ef512729754b46dd3cd14750dd715c8541987014a254465c72619f1ead5d4ae8ff74bcbb6c9122b331c5f65a159e0f0c9890f9e0ad7a5b582dafe2763e54
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,18 +1,29 @@
|
|
1
1
|
# lgpio
|
2
2
|
|
3
|
-
Ruby bindings for the [lgpio (lg)](https://github.com/joan2937/lg)
|
4
|
-
|
5
|
-
## Features
|
6
|
-
- GPIO Read/Write
|
7
|
-
- GPIO Group Read/Write
|
8
|
-
- GPIO Alerts / Callbacks
|
9
|
-
- lg generates alerts at high speed
|
10
|
-
- PWM
|
11
|
-
- Software timed on any pin
|
12
|
-
-
|
3
|
+
Ruby bindings for the [lgpio (lg)](https://github.com/joan2937/lg) Linux library, running on single board computers (SBCs), like Raspberry Pi.
|
4
|
+
|
5
|
+
## Mapped LGPIO Features
|
6
|
+
- [x] GPIO Read/Write
|
7
|
+
- [x] GPIO Group Read/Write
|
8
|
+
- [x] GPIO Alerts / Callbacks
|
9
|
+
- lg generates alerts at high speed in a separate thread. You can read them from a queue in Ruby, as part of your application loop.
|
10
|
+
- [x] Software PWM Out
|
11
|
+
- Software timed (not 100% precise), on any pin
|
12
|
+
- Not recommended for servo motors
|
13
|
+
- [x] Wave
|
13
14
|
- Software timed on any pin, as with PWM.
|
14
|
-
- I2C
|
15
|
-
- SPI
|
15
|
+
- [x] I2C
|
16
|
+
- [x] SPI
|
17
|
+
|
18
|
+
## Extra Features, based on LGPIO
|
19
|
+
- [x] WS2812 over SPI
|
20
|
+
- Only outputs on a SPI MOSI pin. Must be able to set SPI clock frequency to 2.4 MHz.
|
21
|
+
- [ ] Bit Bang SPI
|
22
|
+
- [ ] Bit Bang I2C
|
23
|
+
|
24
|
+
## Sysfs PWM Interface Features
|
25
|
+
- [x] Hardware PWM Out (specific pins per chip)
|
26
|
+
- [x] Servo
|
16
27
|
|
17
28
|
## Installation
|
18
29
|
On Debian-based Linuxes (RaspberryPi OS, Armbian, DietPi etc.):
|
@@ -33,9 +44,9 @@ gem install lgpio
|
|
33
44
|
```
|
34
45
|
|
35
46
|
## Enabling Hardware & Permissions
|
36
|
-
Depending on your SBC and Linux distro/version, you may need to manually enable I2C and SPI hardware. You
|
47
|
+
Depending on your SBC and Linux distro/version, you may need to manually enable I2C and SPI hardware. You should use the setup or config tool that came with your distro for that.
|
37
48
|
|
38
|
-
You may also not have permission to access
|
49
|
+
You may also not have permission to access the GPIO and peripherals. To run without `sudo`, you need read+write permission to some or all of the following devices:
|
39
50
|
```
|
40
51
|
/dev/gpiochip* (For GPIO, example: /dev/gpiochip0)
|
41
52
|
/dev/i2c-* (For I2C, example: /dev/i2c-1)
|
@@ -44,11 +55,11 @@ You may also not have permission to access some or all of the GPIO and periphera
|
|
44
55
|
|
45
56
|
## Documentation
|
46
57
|
- See examples folder for demos of every implemented interface.
|
47
|
-
- Development was done on an Orange Pi Zero 2W. Your GPIO numbers
|
58
|
+
- Development was done on an Orange Pi Zero 2W. Your GPIO numbers may be different, so change them.
|
48
59
|
- For more info, see the [lgpio C API docs.](https://abyz.me.uk/lg/lgpio.html)
|
49
60
|
- As much as possible, the Ruby methods closely follow the C API functions, except:
|
50
61
|
- Snake case instead of camel case names for methods.
|
51
62
|
- Method names have the leading `lg` removed, since inside the `LGPIO` class.
|
52
63
|
- Constants have leading `LG_` removed, as above.
|
53
64
|
- "count" or "length" arguments associated with array args are not needed.
|
54
|
-
- Check the return values of your method calls. On failure, they return negative values
|
65
|
+
- Check the return values of your method calls. On failure, they return negative values, matching the `LG_` error codes at the bottom of the C API doc page.
|
data/examples/i2c_aht10.rb
CHANGED
data/examples/i2c_aht10_zip.rb
CHANGED
data/examples/servo.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'lgpio'
|
2
|
+
#
|
3
|
+
# Writing directly to a hardware PWM channel to control a servo.
|
4
|
+
# Arguments in order are:
|
5
|
+
# pwmchip index (X in /sys/class/pwm/pwmchipX/)
|
6
|
+
# PWM channel on the chip (Y in /sys/class/pwm/pwmchipX/pwmY)
|
7
|
+
# period: given in nanoseconds
|
8
|
+
# OR frequency: given in Hz
|
9
|
+
#
|
10
|
+
servo = LGPIO::HardwarePWM.new(0, 1, period: 20_000_000)
|
11
|
+
#
|
12
|
+
# Duty cycle is given in nanoseconds by default. Extra setter methods
|
13
|
+
# are provided for microseconds, and percent.
|
14
|
+
#
|
15
|
+
# servo.duty_percent = 2.5
|
16
|
+
# servo.duty_us = 500
|
17
|
+
servo.duty = 500_000
|
18
|
+
|
19
|
+
#
|
20
|
+
# Using the Servo class instead.
|
21
|
+
# Arguments in order are:
|
22
|
+
# pwmchip index (X in /sys/class/pwm/pwmchipX/)
|
23
|
+
# PWM channel on the chip (Y in /sys/class/pwm/pwmchipX/pwmY)
|
24
|
+
# Minimum servo duty cycle in microseconds
|
25
|
+
# Maximum servo duty cicle in microseconds
|
26
|
+
# Minimum servo angle
|
27
|
+
# Maximum servo angle
|
28
|
+
#
|
29
|
+
servo = LGPIO::PositionalServo.new(0, 1, 500, 2500, 0, 180)
|
30
|
+
|
31
|
+
angles = [0, 30, 60, 90, 120, 150, 180, 150, 120, 90, 60, 30]
|
32
|
+
|
33
|
+
angles.cycle do |angle|
|
34
|
+
servo.angle = angle
|
35
|
+
sleep 1
|
36
|
+
end
|
data/examples/spi_ws2812.rb
CHANGED
@@ -1,42 +1,15 @@
|
|
1
1
|
#
|
2
|
-
# 2.4 MHz SPI method for writing WS2812 addressable LEDs
|
3
|
-
# Based on:https://learn.adafruit.com/dma-driven-neopixels/overview
|
2
|
+
# 2.4 MHz SPI method for writing WS2812 addressable LEDs.
|
3
|
+
# Based on: https://learn.adafruit.com/dma-driven-neopixels/overview
|
4
4
|
#
|
5
5
|
require 'lgpio'
|
6
6
|
|
7
|
-
# WS2812 Constants
|
8
|
-
NP_ONE = 0b110
|
9
|
-
NP_ZERO = 0b100
|
10
|
-
NP_START = [0]
|
11
|
-
NP_END = Array.new(90) { 0 }
|
12
|
-
BITS = (0..7).to_a.reverse
|
13
|
-
|
14
7
|
# SPI Config
|
15
8
|
SPI_DEV = 1
|
16
9
|
SPI_CHAN = 0
|
17
10
|
SPI_MODE = 0
|
18
11
|
SPI_BAUD = 2_400_000
|
19
12
|
|
20
|
-
# Map array of pixel values (3 bytes each) to raw SPI bytes (1bit -> 3 bits).
|
21
|
-
def pixel_to_raw_spi_bytes(pixel_arr)
|
22
|
-
raw_spi_bytes = []
|
23
|
-
pixel_arr.flatten.each do |byte|
|
24
|
-
long = 0b0
|
25
|
-
for i in BITS do
|
26
|
-
long = long << 3
|
27
|
-
if byte[i] == 0
|
28
|
-
long |= NP_ZERO
|
29
|
-
else
|
30
|
-
long |= NP_ONE
|
31
|
-
end
|
32
|
-
end
|
33
|
-
# Pack as big-endian uint32, then unpack to bytes, taking lowest 24 bits only.
|
34
|
-
long = [long].pack('L>')
|
35
|
-
raw_spi_bytes << long.unpack('C*')[1..3]
|
36
|
-
end
|
37
|
-
return raw_spi_bytes.flatten
|
38
|
-
end
|
39
|
-
|
40
13
|
# 4 pixels: RGBW. Data order per pixel is GRB.
|
41
14
|
pixels_on = [
|
42
15
|
0, 255, 0,
|
@@ -44,17 +17,14 @@ pixels_on = [
|
|
44
17
|
0, 0, 255,
|
45
18
|
255, 255, 255,
|
46
19
|
]
|
47
|
-
data_on = NP_START + pixel_to_raw_spi_bytes(pixels_on) + NP_END
|
48
|
-
|
49
20
|
# 4 pixels, all off.
|
50
21
|
pixels_off = Array.new(12) { 0 }
|
51
|
-
data_off = NP_START + pixel_to_raw_spi_bytes(pixels_off) + NP_END
|
52
22
|
|
53
23
|
spi_handle = LGPIO.spi_open(SPI_DEV, SPI_CHAN, SPI_BAUD, SPI_MODE)
|
54
24
|
|
55
25
|
loop do
|
56
|
-
LGPIO.
|
26
|
+
LGPIO.spi_ws2812_write(spi_handle, pixels_on)
|
57
27
|
sleep 0.5
|
58
|
-
LGPIO.
|
28
|
+
LGPIO.spi_ws2812_write(spi_handle, pixels_off)
|
59
29
|
sleep 0.5
|
60
30
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#
|
2
|
+
# 2.4 MHz SPI method for writing WS2812 addressable LEDs.
|
3
|
+
# Based on: https://learn.adafruit.com/dma-driven-neopixels/overview
|
4
|
+
#
|
5
|
+
require 'lgpio'
|
6
|
+
|
7
|
+
# SPI config
|
8
|
+
SPI_DEV = 1
|
9
|
+
SPI_CHAN = 0
|
10
|
+
SPI_MODE = 0
|
11
|
+
SPI_BAUD = 2_400_000
|
12
|
+
|
13
|
+
PIXEL_COUNT = 8
|
14
|
+
COLORS = [
|
15
|
+
[255, 255, 255],
|
16
|
+
[0, 255, 0],
|
17
|
+
[255, 0, 0],
|
18
|
+
[0, 0, 255],
|
19
|
+
[255, 255, 0],
|
20
|
+
[255, 0, 255],
|
21
|
+
[0, 255, 255]
|
22
|
+
]
|
23
|
+
|
24
|
+
# Move along the strip and back, one pixel at a time.
|
25
|
+
POSITIONS = (0..PIXEL_COUNT-1).to_a + (1..PIXEL_COUNT-2).to_a.reverse
|
26
|
+
|
27
|
+
pixels = Array.new(PIXEL_COUNT) { [0, 0, 0] }
|
28
|
+
spi_handle = LGPIO.spi_open(SPI_DEV, SPI_CHAN, SPI_BAUD, SPI_MODE)
|
29
|
+
|
30
|
+
loop do
|
31
|
+
COLORS.each do |color|
|
32
|
+
POSITIONS.each do |index|
|
33
|
+
# Clear and write.
|
34
|
+
(0..PIXEL_COUNT-1).each { |i| pixels[i] = [0, 0, 0] }
|
35
|
+
LGPIO.spi_ws2812_write(spi_handle, pixels.flatten)
|
36
|
+
|
37
|
+
# Set one pixel and write.
|
38
|
+
pixels[index] = color
|
39
|
+
LGPIO.spi_ws2812_write(spi_handle, pixels.flatten)
|
40
|
+
|
41
|
+
sleep 0.05
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/ext/lgpio/lgpio.c
CHANGED
@@ -302,6 +302,42 @@ static VALUE spi_xfer(VALUE self, VALUE handle, VALUE txArray){
|
|
302
302
|
return retArray;
|
303
303
|
}
|
304
304
|
|
305
|
+
static VALUE spi_ws2812_write(VALUE self, VALUE handle, VALUE pixelArray){
|
306
|
+
int count = RARRAY_LEN(pixelArray);
|
307
|
+
|
308
|
+
// Pull low for at least one byte at 2.4 Mhz before data, and 90 after.
|
309
|
+
int zeroesBefore = 1;
|
310
|
+
int zeroesAfter = 90;
|
311
|
+
int txBufLength = zeroesBefore + (count*3) + zeroesAfter;
|
312
|
+
uint8_t txBuf[txBufLength];
|
313
|
+
for (int i=0; i<txBufLength; i++) { txBuf[i] = 0; }
|
314
|
+
|
315
|
+
VALUE currentByte_rb;
|
316
|
+
uint8_t currentByte;
|
317
|
+
uint8_t currentBit;
|
318
|
+
uint32_t temp;
|
319
|
+
|
320
|
+
for (int i=0; i<count; i++){
|
321
|
+
temp = 0;
|
322
|
+
currentByte_rb = rb_ary_entry(pixelArray, i);
|
323
|
+
Check_Type(currentByte_rb, T_FIXNUM);
|
324
|
+
currentByte = NUM2CHR(currentByte_rb);
|
325
|
+
|
326
|
+
for (int i=7; i>=0; i--) {
|
327
|
+
currentBit = (currentByte & (1 << i));
|
328
|
+
temp = temp << 3;
|
329
|
+
temp = (currentBit == 0) ? (temp | 0b100) : (temp | 0b110);
|
330
|
+
}
|
331
|
+
|
332
|
+
txBuf[zeroesBefore+(i*3)] = (temp >> 16) & 0xFF;
|
333
|
+
txBuf[zeroesBefore+(i*3)+1] = (temp >> 8) & 0xFF;
|
334
|
+
txBuf[zeroesBefore+(i*3)+2] = temp & 0xFF;
|
335
|
+
}
|
336
|
+
|
337
|
+
int result = lgSpiWrite(NUM2INT(handle), txBuf, txBufLength);
|
338
|
+
return INT2NUM(result);
|
339
|
+
}
|
340
|
+
|
305
341
|
void Init_lgpio(void) {
|
306
342
|
// Modules
|
307
343
|
VALUE mLGPIO = rb_define_module("LGPIO");
|
@@ -360,4 +396,5 @@ void Init_lgpio(void) {
|
|
360
396
|
rb_define_singleton_method(mLGPIO, "spi_read", spi_read, 2);
|
361
397
|
rb_define_singleton_method(mLGPIO, "spi_write", spi_write, 2);
|
362
398
|
rb_define_singleton_method(mLGPIO, "spi_xfer", spi_xfer, 2);
|
399
|
+
rb_define_singleton_method(mLGPIO, "spi_ws2812_write", spi_ws2812_write, 2);
|
363
400
|
}
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module LGPIO
|
2
|
+
class HardwarePWM
|
3
|
+
NS_PER_S = 1_000_000_000
|
4
|
+
NS_PER_US = 1_000
|
5
|
+
SYS_FS_PWM_PATH = "/sys/class/pwm/"
|
6
|
+
|
7
|
+
attr_reader :period, :duty, :enabled
|
8
|
+
|
9
|
+
def initialize(chip, channel, frequency: nil, period: nil)
|
10
|
+
@chip = chip
|
11
|
+
@channel = channel
|
12
|
+
|
13
|
+
# Accept either frequency (in Hz) or period in nanoseconds.
|
14
|
+
if (frequency && period) || (!frequency && !period)
|
15
|
+
raise "either period: or frequency: is required, but not both"
|
16
|
+
end
|
17
|
+
|
18
|
+
period ? self.period = period : self.frequency = frequency
|
19
|
+
enable
|
20
|
+
end
|
21
|
+
|
22
|
+
def path
|
23
|
+
@path ||= "#{SYS_FS_PWM_PATH}pwmchip#{@chip}/pwm#{@channel}/"
|
24
|
+
end
|
25
|
+
|
26
|
+
def frequency=(freq)
|
27
|
+
self.period = (NS_PER_S / freq.to_f).round
|
28
|
+
end
|
29
|
+
|
30
|
+
def period=(p)
|
31
|
+
old_period = File.read("#{path}period").strip.to_i
|
32
|
+
unless (old_period == 0)
|
33
|
+
File.open("#{path}duty_cycle", 'w') { |f| f.write("0") }
|
34
|
+
end
|
35
|
+
File.open("#{path}period", 'w') { |f| f.write(p) }
|
36
|
+
@period = p
|
37
|
+
end
|
38
|
+
|
39
|
+
def duty_percent
|
40
|
+
(duty / period.to_f) * 100.0
|
41
|
+
end
|
42
|
+
|
43
|
+
def duty_percent=(d)
|
44
|
+
raise "duty_cycle: #{d} % cannot be more than 100%" if d > 100
|
45
|
+
d_ns = ((d / 100.0) * @period.to_i).round
|
46
|
+
self.duty = d_ns
|
47
|
+
end
|
48
|
+
|
49
|
+
def duty_us=(d_us)
|
50
|
+
d_ns = (d_us * NS_PER_US).round
|
51
|
+
self.duty = d_ns
|
52
|
+
end
|
53
|
+
|
54
|
+
def duty=(d_ns)
|
55
|
+
raise "duty cycle: #{d_ns} ns cannot be longer than period: #{period} ns" if d_ns > period
|
56
|
+
File.open("#{path}duty_cycle", 'w') { |f| f.write(d_ns) }
|
57
|
+
@duty = d_ns
|
58
|
+
end
|
59
|
+
|
60
|
+
def disable
|
61
|
+
File.open("#{path}enable", 'w') { |f| f.write("0") }
|
62
|
+
@enabled = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def enable
|
66
|
+
File.open("#{path}enable", 'w') { |f| f.write("1") }
|
67
|
+
@enabled = true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module LGPIO
|
2
|
+
class PositionalServo < HardwarePWM
|
3
|
+
FREQUENCY = 50
|
4
|
+
|
5
|
+
attr_reader :angle
|
6
|
+
|
7
|
+
def initialize(chip, channel, min_us, max_us, min_angle, max_angle)
|
8
|
+
super(chip, channel, frequency: FREQUENCY)
|
9
|
+
|
10
|
+
raise "min_us: #{min_us} cannot be lower than max_us: #{max_us}" if max_us < min_us
|
11
|
+
@min_us = min_us
|
12
|
+
@max_us = max_us
|
13
|
+
@us_range = @max_us - @min_us
|
14
|
+
|
15
|
+
@min_angle = min_angle
|
16
|
+
@max_angle = max_angle
|
17
|
+
end
|
18
|
+
|
19
|
+
def angle=(a)
|
20
|
+
ratio = (a - @min_angle).to_f / (@max_angle - @min_angle)
|
21
|
+
raise "angle: #{a} outside servo range" if (ratio < 0) || (ratio > 1)
|
22
|
+
|
23
|
+
d_us = (@us_range * ratio) + @min_us
|
24
|
+
self.duty_us = d_us
|
25
|
+
@angle = a
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/lgpio/version.rb
CHANGED
data/lib/lgpio.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lgpio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- vickash
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
11
|
+
date: 2024-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Use GPIO / PWM / I2C / SPI / UART on Linux SBCs in Ruby
|
14
14
|
email: mail@vickash.com
|
@@ -34,14 +34,18 @@ files:
|
|
34
34
|
- examples/reports.rb
|
35
35
|
- examples/rotary_encoder.rb
|
36
36
|
- examples/rotary_encoder_led.rb
|
37
|
+
- examples/servo.rb
|
37
38
|
- examples/spi_loopback.rb
|
38
39
|
- examples/spi_read.rb
|
39
40
|
- examples/spi_ws2812.rb
|
41
|
+
- examples/spi_ws2812_bounce.rb
|
40
42
|
- examples/wave.rb
|
41
43
|
- ext/lgpio/extconf.rb
|
42
44
|
- ext/lgpio/lgpio.c
|
43
45
|
- lgpio.gemspec
|
44
46
|
- lib/lgpio.rb
|
47
|
+
- lib/lgpio/hardware_pwm.rb
|
48
|
+
- lib/lgpio/positional_servo.rb
|
45
49
|
- lib/lgpio/version.rb
|
46
50
|
homepage: https://github.com/denko-rb/lgpio
|
47
51
|
licenses:
|