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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e24e221cb39d27714d3fdd9486c47dd01e33115980d29e5104a455e14fd40890
4
- data.tar.gz: 4ffd6e35644bc8dcde49486cd43d8e7ca73ebbfe9b46955a8de89a4ecff49d33
3
+ metadata.gz: 984afd62543a10d3fc0b6d2a9a581b05b4d9bf7c159d2747f990b2247b59aef1
4
+ data.tar.gz: d2431adb24613fdd42b9f9f06a428c399fe48be2e215a13463237a798113630c
5
5
  SHA512:
6
- metadata.gz: 5060a2fd7a5ef20a1de29f01a87aa2ff2639c9d0e9005594de672d7fa011df48e7a65b893ad4310d07d780f9677c1544ccc80018a5bb4794e014513f4864fae4
7
- data.tar.gz: 16e6a32af1a358ffcf18c7864d2748527f635a83f42734fc3191ecb6eed7fb50cef3ffe4370651b063fde6e4a28bd2075a12b6adb8d04b3e4646353d973515c3
6
+ metadata.gz: f32ceb3af8218d26f1f5713302a9891365af498fe76e2a750037f2ae10a437b747b40167e9fd9c5170fc28696d90c9244ce5d9a36269eba0d0c827a79be47fcd
7
+ data.tar.gz: b167ef512729754b46dd3cd14750dd715c8541987014a254465c72619f1ead5d4ae8ff74bcbb6c9122b331c5f65a159e0f0c9890f9e0ad7a5b582dafe2763e54
data/.gitignore CHANGED
@@ -23,3 +23,4 @@ tmp
23
23
  .ruby-version
24
24
  .rvmrc
25
25
  .zed
26
+ ._*
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) C library, for Linux on single board computers (SBCs), like the Raspberry Pi and its competitors.
4
-
5
- ## Features
6
- - GPIO Read/Write
7
- - GPIO Group Read/Write
8
- - GPIO Alerts / Callbacks
9
- - lg generates alerts at high speed, in a separate thread. In Ruby they can be read from a queue as part of your application loop.
10
- - PWM Output
11
- - Software timed on any pin. No interface for hardware PWM yet.
12
- - Wave
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 may use the setup or config tool that came with your distro for that.
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 some or all of the GPIO and peripherals. To run without `sudo`, you will need read+write permission to some or all of the following devices:
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 will be different, so change them.
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 values, matching the `LG_` error codes at the bottom of the C API doc page.
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.
@@ -1,6 +1,6 @@
1
1
  require 'lgpio'
2
2
 
3
- I2C_DEV = 2
3
+ I2C_DEV = 3
4
4
  POWER_ON_DELAY = 0.100
5
5
  RESET_DELAY = 0.020
6
6
  COMMAND_DELAY = 0.010
@@ -1,6 +1,6 @@
1
1
  require 'lgpio'
2
2
 
3
- I2C_DEV = 2
3
+ I2C_DEV = 3
4
4
  POWER_ON_DELAY = 0.100
5
5
  RESET_DELAY = 0.020
6
6
  COMMAND_DELAY = 0.010
@@ -1,6 +1,6 @@
1
1
  require 'lgpio'
2
2
 
3
- I2C_DEV = 2
3
+ I2C_DEV = 3
4
4
  INIT_ARRAY = [0, 168, 63, 211, 0, 64, 161, 200, 218, 18, 164, 166, 213, 128, 219, 32, 217, 241, 141, 20, 32, 0, 175]
5
5
  START_ARRAY = [0, 33, 0, 127, 34, 0, 7]
6
6
  BLANK_ARRAY = [64] + Array.new(1024) { 0 }
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
@@ -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.spi_write(spi_handle, data_on)
26
+ LGPIO.spi_ws2812_write(spi_handle, pixels_on)
57
27
  sleep 0.5
58
- LGPIO.spi_write(spi_handle, data_off)
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
@@ -1,3 +1,3 @@
1
1
  module LGPIO
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.5"
3
3
  end
data/lib/lgpio.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  require_relative 'lgpio/lgpio'
2
+ require_relative 'lgpio/version'
3
+ require_relative 'lgpio/hardware_pwm'
4
+ require_relative 'lgpio/positional_servo'
2
5
 
3
6
  module LGPIO
4
7
  LOW = 0
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.3
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-03 00:00:00.000000000 Z
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: