denko-piboard 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +165 -0
- data/Rakefile +5 -0
- data/denko_piboard.gemspec +21 -0
- data/examples/pi_system_monitor.rb +58 -0
- data/ext/gpiod/extconf.rb +9 -0
- data/ext/gpiod/gpiod.c +179 -0
- data/lib/denko/piboard.rb +11 -0
- data/lib/denko/piboard_base.rb +81 -0
- data/lib/denko/piboard_core.rb +171 -0
- data/lib/denko/piboard_i2c.rb +119 -0
- data/lib/denko/piboard_infrared.rb +51 -0
- data/lib/denko/piboard_map.rb +49 -0
- data/lib/denko/piboard_pulse.rb +82 -0
- data/lib/denko/piboard_servo.rb +18 -0
- data/lib/denko/piboard_spi.rb +100 -0
- data/lib/denko/piboard_tone.rb +39 -0
- data/lib/denko/piboard_version.rb +5 -0
- data/lib/gpiod.rb +6 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7bd44c89b18336b4d540b81e6fd7e74cf3dd51a733f148e9acd8278795942249
|
4
|
+
data.tar.gz: 02d317de93789676488866e9560ca4d5da91014f47509029991b4be31ab20c9f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c8bb78c2b3e62273b21ec520d35ba54465d3edf8f2e93811f7aaca0aca1e089152a7a0479715754d77301bc8ad938a39086a8bb1cb127566b5ac9c126946956d
|
7
|
+
data.tar.gz: d305d28242a97dd14c546516c5ba4b5853e249822981c59ed55e1fb949573524c3744e1a1b2f7889658de8afcedeb469b018acc5dc233fb0e29798a923827e33
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 denko-rb
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# denko-piboard 0.13.0
|
2
|
+
|
3
|
+
### Raspberry Pi GPIO in Ruby
|
4
|
+
|
5
|
+
This gem adds support for the Raspberry Pi GPIO interface to the [`denko`](https://github.com/denko-rb/denko) gem. Unlike the main gem, which requires an external microcontroller, this lets you to connect peripherals directly to the Pi.
|
6
|
+
|
7
|
+
`Denko::PiBoard` is a drop-in replacement for `Denko::Board`, which would represent a connected micrcontroller. Everything maps to the Pi's built-in GPIO pins instead.
|
8
|
+
|
9
|
+
**Note:** This is not for the Raspberry Pi Pico (W) / RP2040. That microcontroller works with the main gem.
|
10
|
+
|
11
|
+
## Example
|
12
|
+
Create a script, `led_button.rb`:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'denko/piboard'
|
16
|
+
|
17
|
+
# Board instance for the Pi.
|
18
|
+
board = Denko::PiBoard.new
|
19
|
+
|
20
|
+
# LED connected to GPIO4.
|
21
|
+
led = Denko::LED.new(board: board, pin: 4)
|
22
|
+
|
23
|
+
# Momentary button connected to GPIO17, using internal pullup.
|
24
|
+
button = Denko::DigitalIO::Button.new(board: board, pin: 17, pullup: true)
|
25
|
+
|
26
|
+
# Led on when button is down (0)
|
27
|
+
button.down do
|
28
|
+
puts "Button down"
|
29
|
+
led.on
|
30
|
+
end
|
31
|
+
|
32
|
+
# Led is off when button is up (1)
|
33
|
+
button.up do
|
34
|
+
puts "Button up"
|
35
|
+
led.off
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sleep main thread. Ctrl+C to quit.
|
39
|
+
sleep
|
40
|
+
```
|
41
|
+
|
42
|
+
Run it:
|
43
|
+
```shell
|
44
|
+
ruby led_button.rb
|
45
|
+
```
|
46
|
+
#### More Examples
|
47
|
+
Some Pi-specific code is shown in this gem's [examples](examples) folder, but most examples are in the [main gem](https://github.com/denko-rb/denko/tree/master/examples). They must be modified to work with the Pi's GPIO:
|
48
|
+
|
49
|
+
1. Replace setup code:
|
50
|
+
```ruby
|
51
|
+
# Replace this:
|
52
|
+
require 'bundler/setup'
|
53
|
+
require 'denko'
|
54
|
+
# With this:
|
55
|
+
require 'denko/piboard'
|
56
|
+
|
57
|
+
# Replace this:
|
58
|
+
connection = Denko::Connection::Serial.new()
|
59
|
+
board = Denko::Board.new()
|
60
|
+
# With this:
|
61
|
+
board = Denko::PiBoard.new
|
62
|
+
```
|
63
|
+
|
64
|
+
2. Update GPIO/pin numbers as needed. Raspberry Pi pinouts can be found [here](https://pinout.xyz/).
|
65
|
+
|
66
|
+
**Note:** Not all features from all examples are implemented yet, nor can be implemented. See [Features](#features) below.
|
67
|
+
|
68
|
+
## Installation
|
69
|
+
This gem depends on the [pigpio library](https://github.com/joan2937/pigpio) and [pigpio gem](https://github.com/nak1114/ruby-extension-pigpio), which provides Ruby bindings, as well as [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git).
|
70
|
+
|
71
|
+
#### 1. Install pigpio and libgpiod packages
|
72
|
+
```shell
|
73
|
+
sudo apt install pigpio libgpiod-dev
|
74
|
+
```
|
75
|
+
|
76
|
+
#### 2. Install pigpio gem
|
77
|
+
The `pigpio` gem has a couple bugs. Until fixes are merged, please install from [this fork](https://github.com/vickash/ruby-extension-pigpio):
|
78
|
+
```shell
|
79
|
+
git clone https://github.com/vickash/ruby-extension-pigpio.git
|
80
|
+
cd ruby-extension-pigpio
|
81
|
+
gem build
|
82
|
+
gem install ruby-extension-pigpio-0.1.11.gem
|
83
|
+
```
|
84
|
+
|
85
|
+
#### 3. Install denko gem
|
86
|
+
This gem is very new. It __will not__ work with the version of `denko` (0.11.3) currently available from RubyGems. Install the latest version (future 0.13.0) from the master branch instead:
|
87
|
+
```shell
|
88
|
+
git clone https://github.com/denko-rb/denko.git
|
89
|
+
cd denko
|
90
|
+
git submodule init
|
91
|
+
git submodule update
|
92
|
+
gem build
|
93
|
+
gem install denko-0.13.0.gem
|
94
|
+
```
|
95
|
+
|
96
|
+
#### 4. Install denko/piboard gem
|
97
|
+
Again, since this gem is so new, install from the latest master branch:
|
98
|
+
```shell
|
99
|
+
git clone https://github.com/denko-rb/denko-piboard.git
|
100
|
+
cd denko-piboard
|
101
|
+
gem build
|
102
|
+
gem install denko-piboard-0.13.0.gem
|
103
|
+
```
|
104
|
+
|
105
|
+
**Note:** `sudo` may be needed before `gem install` if using the preinstalled Ruby on a Raspberry Pi.
|
106
|
+
|
107
|
+
## Pi Setup
|
108
|
+
Depending on your Pi setup, libgpiod may limit GPIO access to the `root` user. If this is the case, Ruby scripts will fail with a `libgpiod` error. To give your user account permission to access GPIO, add it to the `gpio` group.
|
109
|
+
```
|
110
|
+
sudo usermod -a -G gpio YOUR_USERNAME
|
111
|
+
```
|
112
|
+
|
113
|
+
I2C, SPI and the hardware UART are disabled on the Pi by default. Enable them with the built in utility:
|
114
|
+
```shell
|
115
|
+
sudo raspi-config
|
116
|
+
```
|
117
|
+
|
118
|
+
Select "Interfacing Options" from the menu and enable as needed. More info in the [Features](#features) section.
|
119
|
+
|
120
|
+
#### pigpiod
|
121
|
+
The `pigpio` package includes `pigpiod`, which runs in the background as root, providing GPIO access. Ruby scripts won't work if it isn't running. You should only need to start it once per boot. You can automate it, or start manually with:
|
122
|
+
```shell
|
123
|
+
sudo pigpiod -s 10
|
124
|
+
```
|
125
|
+
|
126
|
+
**Note:** `-s 10` sets `pigpiod` to tick every 10 microseconds, lowering CPU use. Valid values are: 1, 2, 4, 5, 8, 10 (5 default).
|
127
|
+
|
128
|
+
## Features
|
129
|
+
|
130
|
+
### Already Implemented
|
131
|
+
- Internal Pull Down/Up
|
132
|
+
- Digital Out
|
133
|
+
- Digital In
|
134
|
+
- PWM Out (use on any pin disables PCM out, cancels Servo on same pin)
|
135
|
+
- Servo (use on any pin disables PCM out, cancels PWM Out on same pin)
|
136
|
+
- ToneOut (uses waves, one at a time per board, cancels any Infrared Out)
|
137
|
+
- Infrared Out (uses waves, one at a time per board, cancels any Tone Out)
|
138
|
+
- DHT Class Temperature + Humidity Sensors
|
139
|
+
- I2C
|
140
|
+
- Must enable with `raspi-config` before use. Instructions [here](https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-i2c).
|
141
|
+
- I2C hardware clock cannot be set dynamically, like a microcontroller. Must set in `/boot/config.txt`. Default is 100 kHz. 400 kHz recommended if transferring a lot of data, like with SSD1306 OLED. See [here](https://www.raspberrypi-spy.co.uk/2018/02/change-raspberry-pi-i2c-bus-speed/) for instructions.
|
142
|
+
|
143
|
+
### Partially Implemented
|
144
|
+
- SPI
|
145
|
+
- Must enable with `raspi-config` before use. Insturctions [here](https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-spi).
|
146
|
+
- Only Uses SPI1 interface, not SPI0.
|
147
|
+
- Does not bind CE pins according to GPIO pinout. Any pin can be used for chip enable.
|
148
|
+
- SPI modes 1 and 3 may not work.
|
149
|
+
- No listeners yet.
|
150
|
+
|
151
|
+
### To Be Implemented
|
152
|
+
- OneWire
|
153
|
+
- Hardware UART
|
154
|
+
- BitBang I2C
|
155
|
+
- BitBang SPI
|
156
|
+
- BitBang UART
|
157
|
+
- WS2812
|
158
|
+
|
159
|
+
### Differences
|
160
|
+
- Listeners are still polled in a thread, but always at 1ms.
|
161
|
+
- pigpio has very fast native input callbacks available, but events are not received in order on a global basis, only per pin. This creates issues where event order between pins is important (like a RotaryEncoder). May expose this functionality for SinglePin components later.
|
162
|
+
|
163
|
+
### Incompatible
|
164
|
+
- EEPROM (Use the filesystem for persistence instead)
|
165
|
+
- Analog IO (No analog pins on Raspberry Pi. Use ADC or DAC over I2C or SPI)
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'lib/denko/piboard_version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'denko-piboard'
|
5
|
+
s.version = Denko::PiBoard::VERSION
|
6
|
+
s.licenses = ['MIT']
|
7
|
+
s.summary = "Use Raspberry Pi built-in GPIO as a Board class with the denko gem"
|
8
|
+
s.description = "Denko::PiBoard is a drop-in replacement for Denko::Board. Use denko features and component classes to be used directly on a Raspberry Pi."
|
9
|
+
|
10
|
+
s.authors = ["vickash"]
|
11
|
+
s.email = 'mail@vickash.com'
|
12
|
+
s.files = Dir['**/*'].reject { |f| f.match /.gem\z/}
|
13
|
+
s.homepage = 'https://github.com/denko-rb/denko-piboard'
|
14
|
+
s.metadata = { "source_code_uri" => "https://github.com/denko-rb/denko-piboard" }
|
15
|
+
|
16
|
+
# libgpio C extension
|
17
|
+
s.extensions = %w[ext/gpiod/extconf.rb]
|
18
|
+
|
19
|
+
s.add_dependency 'pigpio', '~> 0.1'
|
20
|
+
s.add_dependency 'denko', '~> 0.13'
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#
|
2
|
+
# This example uses an SSD1306 OLED display, connected to the Pi's I2C1 interface.
|
3
|
+
# I2C1 pins are GPIO2 (SDA) and GPIO3 (SCL).
|
4
|
+
#
|
5
|
+
# CPU usage is measured using `mpstat`. To install it:
|
6
|
+
# sudo apt install sysstat
|
7
|
+
#
|
8
|
+
# RAM usage is measured using `free`.
|
9
|
+
#
|
10
|
+
# Each loop, the OLED refreshes, showing date, time, CPU usage and RAM usage.
|
11
|
+
#
|
12
|
+
require 'denko/piboard'
|
13
|
+
|
14
|
+
# Special character that fills a 4x6 rectangle.
|
15
|
+
# Used to make bar graphs for CPU and RAM usage.
|
16
|
+
BAR_ELEMENT = [0x00, 0x7E, 0x7E, 0x7E, 0x00]
|
17
|
+
|
18
|
+
board = Denko::PiBoard.new
|
19
|
+
i2c = Denko::I2C::Bus.new(board: board, pin: :SDA)
|
20
|
+
|
21
|
+
oled = Denko::Display::SSD1306.new(bus: i2c, rotate: true)
|
22
|
+
canvas = oled.canvas
|
23
|
+
|
24
|
+
# Only do this once since total RAM won't change.
|
25
|
+
total_ram = `free -h | awk 'NR==2 {gsub("Mi", "", $2); print $2}'`
|
26
|
+
total_ram = total_ram.to_i
|
27
|
+
ram_bar_factor = total_ram / 25.0
|
28
|
+
|
29
|
+
loop do
|
30
|
+
# CPU Usage (automatically delays for 1 second)
|
31
|
+
mpstat_result = `mpstat -P ALL 1 1 | awk '/^Average:/ && ++count == 2 {print 100 - $12"%"}'`
|
32
|
+
cpu_percent = mpstat_result.chop.to_f
|
33
|
+
|
34
|
+
canvas.clear
|
35
|
+
canvas.text_cursor = [0, 16]
|
36
|
+
canvas.print "CPU Usage: #{('%.3f' % cpu_percent).rjust(8, ' ')}%"
|
37
|
+
|
38
|
+
canvas.text_cursor = [0, 24]
|
39
|
+
(cpu_percent / 4).ceil.times { canvas.raw_char(BAR_ELEMENT) }
|
40
|
+
|
41
|
+
# RAM Usage
|
42
|
+
ram_usage = `free -h | awk 'NR==2 {gsub("Mi", "", $3); print $3}'`
|
43
|
+
ram_usage = ram_usage.to_i
|
44
|
+
|
45
|
+
canvas.text_cursor = [0, 40]
|
46
|
+
ram_string = "#{ram_usage}/#{total_ram}MB"
|
47
|
+
canvas.print "RAM Usage:#{ram_string.rjust(11, ' ')}"
|
48
|
+
|
49
|
+
canvas.text_cursor = [0, 48]
|
50
|
+
(ram_usage / ram_bar_factor).ceil.times { canvas.raw_char(BAR_ELEMENT) }
|
51
|
+
|
52
|
+
# Date and time just before write.
|
53
|
+
canvas.text_cursor = [0,0]
|
54
|
+
canvas.print(Time.now.strftime('%a %b %d %-l:%M %p'))
|
55
|
+
|
56
|
+
# Only refresh the area in use.
|
57
|
+
oled.draw(0, 127, 0, 56)
|
58
|
+
end
|
data/ext/gpiod/gpiod.c
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <gpiod.h>
|
3
|
+
|
4
|
+
#define GPIO_CHIP_NAME "gpiochip0"
|
5
|
+
|
6
|
+
static struct gpiod_chip *chip;
|
7
|
+
|
8
|
+
// Save mapping of lowest 32 GPIOs to gpiod_line structs.
|
9
|
+
static struct gpiod_line *lines[32] = { NULL };
|
10
|
+
|
11
|
+
// Input and output values.
|
12
|
+
static int gpio_number;
|
13
|
+
static int gpio_value;
|
14
|
+
static int return_value;
|
15
|
+
|
16
|
+
static VALUE open_chip(VALUE self) {
|
17
|
+
chip = gpiod_chip_open_by_name(GPIO_CHIP_NAME);
|
18
|
+
if (!chip) {
|
19
|
+
rb_raise(rb_eRuntimeError, "libgpiod error: Could not open GPIO chip");
|
20
|
+
return Qnil;
|
21
|
+
}
|
22
|
+
return Qnil;
|
23
|
+
}
|
24
|
+
|
25
|
+
static VALUE close_chip(VALUE self) {
|
26
|
+
gpiod_chip_close(chip);
|
27
|
+
return Qnil;
|
28
|
+
}
|
29
|
+
|
30
|
+
static void validate_gpio_number(int gpio_number) {
|
31
|
+
if ((gpio_number < 0) || (gpio_number > 31)) {
|
32
|
+
VALUE error_message = rb_sprintf("libgpiod error: GPIO line (%d) out of range. Valid range is 0..31", gpio_number);
|
33
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
static void validate_gpio_value(int value) {
|
38
|
+
if (!((value == 0) || (value == 1))) {
|
39
|
+
VALUE error_message = rb_sprintf("libgpiod error: GPIO value (%d) out of range. Valid values are 0 or 1", value);
|
40
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
static VALUE open_line_output(VALUE self, VALUE gpio) {
|
45
|
+
gpio_number = NUM2INT(gpio);
|
46
|
+
validate_gpio_number(gpio_number);
|
47
|
+
|
48
|
+
lines[gpio_number] = gpiod_chip_get_line(chip, gpio_number);
|
49
|
+
if (!lines[gpio_number]) {
|
50
|
+
gpiod_chip_close(chip);
|
51
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not get GPIO line %d", gpio_number);
|
52
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
53
|
+
return Qnil;
|
54
|
+
}
|
55
|
+
|
56
|
+
return_value = gpiod_line_request_output(lines[gpio_number], "GPIOD_RB", 0);
|
57
|
+
if (return_value < 0) {
|
58
|
+
gpiod_chip_close(chip);
|
59
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not request output for GPIO line %d", gpio_number);
|
60
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
61
|
+
return Qnil;
|
62
|
+
}
|
63
|
+
|
64
|
+
return Qnil;
|
65
|
+
}
|
66
|
+
|
67
|
+
static VALUE set_value(VALUE self, VALUE gpio, VALUE value) {
|
68
|
+
gpio_number = NUM2INT(gpio);
|
69
|
+
validate_gpio_number(gpio_number);
|
70
|
+
gpio_value = NUM2INT(value);
|
71
|
+
validate_gpio_value(gpio_value);
|
72
|
+
|
73
|
+
return_value = gpiod_line_set_value(lines[gpio_number], gpio_value);
|
74
|
+
|
75
|
+
if (return_value < 0) {
|
76
|
+
gpiod_chip_close(chip);
|
77
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not set value %d on GPIO line %d", gpio_value, gpio_number);
|
78
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
79
|
+
return Qnil;
|
80
|
+
}
|
81
|
+
|
82
|
+
return value;
|
83
|
+
}
|
84
|
+
|
85
|
+
static VALUE open_line_input(VALUE self, VALUE gpio) {
|
86
|
+
gpio_number = NUM2INT(gpio);
|
87
|
+
validate_gpio_number(gpio_number);
|
88
|
+
|
89
|
+
lines[gpio_number] = gpiod_chip_get_line(chip, gpio_number);
|
90
|
+
if (!lines[gpio_number]) {
|
91
|
+
gpiod_chip_close(chip);
|
92
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not get GPIO line %d", gpio_number);
|
93
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
94
|
+
return Qnil;
|
95
|
+
}
|
96
|
+
|
97
|
+
return_value = gpiod_line_request_input(lines[gpio_number], "GPIOD_RB");
|
98
|
+
if (return_value < 0) {
|
99
|
+
gpiod_chip_close(chip);
|
100
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not request input for GPIO line %d", gpio_number);
|
101
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
102
|
+
return Qnil;
|
103
|
+
}
|
104
|
+
|
105
|
+
return Qnil;
|
106
|
+
}
|
107
|
+
|
108
|
+
static VALUE get_value(VALUE self, VALUE gpio) {
|
109
|
+
gpio_number = NUM2INT(gpio);
|
110
|
+
validate_gpio_number(gpio_number);
|
111
|
+
|
112
|
+
return_value = gpiod_line_get_value(lines[gpio_number]);
|
113
|
+
|
114
|
+
if (return_value < 0) {
|
115
|
+
gpiod_chip_close(chip);
|
116
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not get value from GPIO line %d", gpio_number);
|
117
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
118
|
+
return Qnil;
|
119
|
+
}
|
120
|
+
|
121
|
+
return INT2NUM(return_value);
|
122
|
+
}
|
123
|
+
|
124
|
+
static VALUE close_line(VALUE self, VALUE gpio) {
|
125
|
+
gpio_number = NUM2INT(gpio);
|
126
|
+
validate_gpio_number(gpio_number);
|
127
|
+
|
128
|
+
// Only try to close the line if it was opened before.
|
129
|
+
if (lines[gpio_number] == NULL) return Qnil;
|
130
|
+
|
131
|
+
gpiod_line_release(lines[gpio_number]);
|
132
|
+
lines[gpio_number] = NULL;
|
133
|
+
return Qnil;
|
134
|
+
}
|
135
|
+
|
136
|
+
static VALUE set_value_raw(VALUE self, VALUE gpio, VALUE value) {
|
137
|
+
gpio_number = NUM2INT(gpio);
|
138
|
+
gpio_value = NUM2INT(value);
|
139
|
+
|
140
|
+
return_value = gpiod_line_set_value(lines[gpio_number], gpio_value);
|
141
|
+
|
142
|
+
if (return_value < 0) {
|
143
|
+
gpiod_chip_close(chip);
|
144
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not set value %d on GPIO line %d", gpio_value, gpio_number);
|
145
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
146
|
+
return Qnil;
|
147
|
+
}
|
148
|
+
return value;
|
149
|
+
}
|
150
|
+
|
151
|
+
static VALUE get_value_raw(VALUE self, VALUE gpio) {
|
152
|
+
gpio_number = NUM2INT(gpio);
|
153
|
+
|
154
|
+
return_value = gpiod_line_get_value(lines[gpio_number]);
|
155
|
+
|
156
|
+
if (return_value < 0) {
|
157
|
+
gpiod_chip_close(chip);
|
158
|
+
VALUE error_message = rb_sprintf("libgpiod error: Could not get value from GPIO line %d", gpio_number);
|
159
|
+
rb_raise(rb_eRuntimeError, "%s", StringValueCStr(error_message));
|
160
|
+
return Qnil;
|
161
|
+
}
|
162
|
+
return INT2NUM(return_value);
|
163
|
+
}
|
164
|
+
|
165
|
+
void Init_gpiod(void) {
|
166
|
+
VALUE mDenko = rb_define_module("Denko");
|
167
|
+
VALUE mGPIOD = rb_define_module_under(mDenko, "GPIOD");
|
168
|
+
rb_define_singleton_method(mGPIOD, "open_chip", open_chip, 0);
|
169
|
+
rb_define_singleton_method(mGPIOD, "close_chip", close_chip, 0);
|
170
|
+
rb_define_singleton_method(mGPIOD, "open_line_output", open_line_output, 1);
|
171
|
+
rb_define_singleton_method(mGPIOD, "set_value", set_value, 2);
|
172
|
+
rb_define_singleton_method(mGPIOD, "open_line_input", open_line_input, 1);
|
173
|
+
rb_define_singleton_method(mGPIOD, "get_value", get_value, 1);
|
174
|
+
rb_define_singleton_method(mGPIOD, "close_line", close_line, 1);
|
175
|
+
|
176
|
+
// These do no validation.
|
177
|
+
rb_define_singleton_method(mGPIOD, "set_value_raw", set_value_raw, 2);
|
178
|
+
rb_define_singleton_method(mGPIOD, "get_value_raw", get_value_raw, 1);
|
179
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative '../gpiod'
|
2
|
+
require_relative 'piboard_version'
|
3
|
+
require_relative 'piboard_map'
|
4
|
+
require_relative 'piboard_base'
|
5
|
+
require_relative 'piboard_core'
|
6
|
+
require_relative 'piboard_pulse'
|
7
|
+
require_relative 'piboard_servo'
|
8
|
+
require_relative 'piboard_tone'
|
9
|
+
require_relative 'piboard_infrared'
|
10
|
+
require_relative 'piboard_i2c'
|
11
|
+
require_relative 'piboard_spi'
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'denko'
|
2
|
+
require 'pigpio'
|
3
|
+
|
4
|
+
module Denko
|
5
|
+
class PiBoard
|
6
|
+
include Pigpio::Constant
|
7
|
+
|
8
|
+
attr_reader :high, :low, :pwm_high
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
# On 64-bit systems there's a pigpio bug where wavelengths are doubled.
|
12
|
+
@aarch64 = RUBY_PLATFORM.match(/aarch64/)
|
13
|
+
|
14
|
+
# Pin state
|
15
|
+
@pins = []
|
16
|
+
@pwms = []
|
17
|
+
|
18
|
+
# Listener state
|
19
|
+
@pin_listeners = []
|
20
|
+
@listen_mutex = Mutex.new
|
21
|
+
@listen_states = Array.new(32) { 0 }
|
22
|
+
@listen_thread = nil
|
23
|
+
@listen_reading = 0
|
24
|
+
|
25
|
+
# PiGPIO callback state. Unused for now.
|
26
|
+
@pin_callbacks = []
|
27
|
+
|
28
|
+
# Logic levels
|
29
|
+
@low = 0
|
30
|
+
@high = 1
|
31
|
+
@pwm_high = 255
|
32
|
+
|
33
|
+
# Use the pigpiod interface directly.
|
34
|
+
@pi_handle = Pigpio::IF.pigpio_start
|
35
|
+
exit(-1) if @pi_handle < 0
|
36
|
+
|
37
|
+
# Open the libgpiod interface too.
|
38
|
+
Denko::GPIOD.open_chip
|
39
|
+
end
|
40
|
+
|
41
|
+
def finish_write
|
42
|
+
Pigpio::IF.pigpio_stop(@pi_handle)
|
43
|
+
Denko::GPIOD.close_chip
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Use standard Subcomponents behavior.
|
48
|
+
#
|
49
|
+
include Behaviors::Subcomponents
|
50
|
+
|
51
|
+
def update(pin, message, time=nil)
|
52
|
+
if single_pin_components[pin]
|
53
|
+
single_pin_components[pin].update(message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :pi_handle, :wave
|
60
|
+
|
61
|
+
def get_gpio(pin)
|
62
|
+
@pins[pin] ||= Pigpio::UserGPIO.new(@pi_handle, pin)
|
63
|
+
end
|
64
|
+
|
65
|
+
def pwm_clear(pin)
|
66
|
+
@pwms[pin] = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def new_wave
|
70
|
+
stop_wave
|
71
|
+
@wave = Pigpio::Wave.new(pi_handle)
|
72
|
+
end
|
73
|
+
|
74
|
+
def stop_wave
|
75
|
+
return unless wave
|
76
|
+
wave.tx_stop
|
77
|
+
wave.clear
|
78
|
+
@wave = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
# CMD = 0
|
4
|
+
def set_pin_mode(pin, mode=:input, glitch_time=nil)
|
5
|
+
# Close the line in libgpiod, if was already open.
|
6
|
+
Denko::GPIOD.close_line(pin)
|
7
|
+
|
8
|
+
pwm_clear(pin)
|
9
|
+
gpio = get_gpio(pin)
|
10
|
+
|
11
|
+
# Output
|
12
|
+
if mode.to_s.match /output/
|
13
|
+
gpio.mode = PI_OUTPUT
|
14
|
+
|
15
|
+
# Use pigpiod for setup, but still open line in libgpiod.
|
16
|
+
Denko::GPIOD.open_line_output(pin)
|
17
|
+
|
18
|
+
# Input
|
19
|
+
else
|
20
|
+
gpio.mode = PI_INPUT
|
21
|
+
|
22
|
+
# State change valid only if steady for this many microseconds.
|
23
|
+
# Only applies to callbacks hooked through pigpiod.
|
24
|
+
if glitch_time
|
25
|
+
gpio.glitch_filter(glitch_time)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Pull down/up/none
|
29
|
+
if mode.to_s.match /pulldown/
|
30
|
+
gpio.pud = PI_PUD_DOWN
|
31
|
+
elsif mode.to_s.match /pullup/
|
32
|
+
gpio.pud = PI_PUD_UP
|
33
|
+
else
|
34
|
+
gpio.pud = PI_PUD_OFF
|
35
|
+
end
|
36
|
+
|
37
|
+
# Use pigpiod for setup, but still open line in libgpiod.
|
38
|
+
Denko::GPIOD.open_line_input(pin)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# CMD = 1
|
43
|
+
def digital_write(pin, value)
|
44
|
+
pwm_clear(pin)
|
45
|
+
Denko::GPIOD.set_value(pin, value)
|
46
|
+
end
|
47
|
+
|
48
|
+
# CMD = 2
|
49
|
+
def digital_read(pin)
|
50
|
+
unless @pwms[pin]
|
51
|
+
state = Denko::GPIOD.get_value(pin)
|
52
|
+
self.update(pin, state)
|
53
|
+
return state
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# CMD = 3
|
58
|
+
def pwm_write(pin, value)
|
59
|
+
# Disable servo if necessary.
|
60
|
+
pwm_clear(pin) if @pwms[pin] == :servo
|
61
|
+
|
62
|
+
unless @pwms[pin]
|
63
|
+
@pwms[pin] = get_gpio(pin).pwm
|
64
|
+
@pwms[pin].frequency = 1000
|
65
|
+
end
|
66
|
+
@pwms[pin].dutycycle = value
|
67
|
+
end
|
68
|
+
|
69
|
+
# PiGPIO native callbacks. Unused now.
|
70
|
+
def set_alert(pin, state=:off, options={})
|
71
|
+
# Listener on
|
72
|
+
if state == :on && !@pin_callbacks[pin]
|
73
|
+
callback = get_gpio(pin).callback(EITHER_EDGE) do |tick, level, pin_cb|
|
74
|
+
update(pin_cb, level, tick)
|
75
|
+
end
|
76
|
+
@pin_callbacks[pin] = callback
|
77
|
+
|
78
|
+
# Listener off
|
79
|
+
else
|
80
|
+
@pin_callbacks[pin].cancel if @pin_callbacks[pin]
|
81
|
+
@pin_callbacks[pin] = nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# CMD = 6
|
86
|
+
def set_listener(pin, state=:off, options={})
|
87
|
+
# Listener on
|
88
|
+
if state == :on && !@pin_listeners.include?(pin)
|
89
|
+
add_listener(pin)
|
90
|
+
else
|
91
|
+
remove_listener(pin)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def digital_listen(pin, divider=4)
|
96
|
+
set_listener(pin, :on, {})
|
97
|
+
end
|
98
|
+
|
99
|
+
def stop_listener(pin)
|
100
|
+
set_listener(pin, :off)
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_listener(pin)
|
104
|
+
@listen_mutex.synchronize do
|
105
|
+
@pin_listeners |= [pin]
|
106
|
+
@pin_listeners.sort!
|
107
|
+
@listen_states[pin] = Denko::GPIOD.get_value(pin)
|
108
|
+
end
|
109
|
+
start_listen_thread
|
110
|
+
end
|
111
|
+
|
112
|
+
def remove_listener(pin)
|
113
|
+
@listen_mutex.synchronize do
|
114
|
+
@pin_listeners.delete(pin)
|
115
|
+
@listen_states[pin] = nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def start_listen_thread
|
120
|
+
return if @listen_thread
|
121
|
+
|
122
|
+
@listen_thread = Thread.new do
|
123
|
+
#
|
124
|
+
# @listen_monitor_thread will adjust sleep time dyanmically,
|
125
|
+
# targeting even timing of 1 millisecond between loops.
|
126
|
+
#
|
127
|
+
@listen_count = 0
|
128
|
+
@listen_sleep = 0.001
|
129
|
+
start_time = Time.now
|
130
|
+
|
131
|
+
loop do
|
132
|
+
@listen_mutex.synchronize do
|
133
|
+
@pin_listeners.each do |pin|
|
134
|
+
@listen_reading = Denko::GPIOD.get_value(pin)
|
135
|
+
self.update(pin, @listen_reading) if (@listen_reading != @listen_states[pin])
|
136
|
+
@listen_states[pin] = @listen_reading
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@listen_count += 1
|
140
|
+
sleep(@listen_sleep)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
@listen_monitor_thread = Thread.new do
|
145
|
+
loop do
|
146
|
+
# Sample the listen rate over 5 seconds.
|
147
|
+
time1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
148
|
+
count1 = @listen_count
|
149
|
+
sleep(5)
|
150
|
+
time2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
151
|
+
count2 = @listen_count
|
152
|
+
|
153
|
+
# Quick maths.
|
154
|
+
loops = count2 - count1
|
155
|
+
time = time2 - time1
|
156
|
+
active_time_per_loop = (time - (loops * @listen_sleep)) / loops
|
157
|
+
|
158
|
+
# Target 1 millisecond.
|
159
|
+
@listen_sleep = 0.001 - active_time_per_loop
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def stop_listen_thread
|
165
|
+
@listen_monitor_thread.kill
|
166
|
+
@listen_monitor_thread = nil
|
167
|
+
@listen_thread.kill
|
168
|
+
@listen_thread = nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
# Maximum amount of bytes that can be read or written in a single I2C operation.
|
4
|
+
def i2c_limit
|
5
|
+
65535
|
6
|
+
end
|
7
|
+
|
8
|
+
def i2c_mutex
|
9
|
+
@i2c_mutex ||= Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# CMD = 33
|
13
|
+
def i2c_search
|
14
|
+
i2c_mutex.synchronize do
|
15
|
+
found_string = ""
|
16
|
+
|
17
|
+
# Address ranges 0..7 and 120..127 are reserved.
|
18
|
+
# Try each address in 8..19 (0x08 to 0x77).
|
19
|
+
(0x08..0x77).each do |address|
|
20
|
+
i2c_open(1, address)
|
21
|
+
byte = Pigpio::IF.i2c_read_byte(pi_handle, i2c_handle)
|
22
|
+
# Add to the string colon separated if byte was valid.
|
23
|
+
found_string << "#{address}:" if byte >= 0
|
24
|
+
i2c_close
|
25
|
+
end
|
26
|
+
|
27
|
+
# Remove trailing colon.
|
28
|
+
found_string.chop! unless found_string.empty?
|
29
|
+
|
30
|
+
# Update the bus as if message came from microcontroller.
|
31
|
+
self.update(2, found_string)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# CMD = 34
|
36
|
+
def i2c_write(address, bytes, frequency=100000, repeated_start=false)
|
37
|
+
i2c_mutex.synchronize do
|
38
|
+
raise ArgumentError, "can't write more than #{i2c_limit} bytes to I2C" if bytes.length > i2c_limit
|
39
|
+
|
40
|
+
# Pack length as a 16-bit uint, then unpack it into 2 litle endian bytes.
|
41
|
+
length = [bytes.length].pack("S").unpack("C*")
|
42
|
+
|
43
|
+
# Prepend length to the bytes.
|
44
|
+
bytes = length + bytes
|
45
|
+
|
46
|
+
# Prepend write command and escape character (necessary for double byte write length).
|
47
|
+
bytes = [0x01, 0x07] + bytes
|
48
|
+
|
49
|
+
# Enable and (re)disable repeated start as needed.
|
50
|
+
bytes = [0x02] + bytes + [0x03] if repeated_start
|
51
|
+
|
52
|
+
# Null terminate the command sequence.
|
53
|
+
bytes = bytes + [0x00]
|
54
|
+
|
55
|
+
# Send the command to the I2C1 interface, packed as uint8 string.
|
56
|
+
i2c_open(1, address)
|
57
|
+
Pigpio::IF.i2c_zip(pi_handle, i2c_handle, bytes.pack("C*"), 0)
|
58
|
+
i2c_close
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# CMD = 35
|
63
|
+
def i2c_read(address, register, read_length, frequency=100000, repeated_start=false)
|
64
|
+
i2c_mutex.synchronize do
|
65
|
+
raise ArgumentError, "can't read more than #{i2c_limit} bytes to I2C" if read_length > i2c_limit
|
66
|
+
|
67
|
+
# Start with number of bytes to read (16-bit number) represented as 2 little endian bytes.
|
68
|
+
buffer = [read_length].pack("S").unpack("C*")
|
69
|
+
|
70
|
+
# Prepend read command and escape character (necessary for double byte write length).
|
71
|
+
buffer = [0x01, 0x06] + buffer
|
72
|
+
|
73
|
+
# If a start register was given, write it first.
|
74
|
+
if register
|
75
|
+
register = [register].flatten
|
76
|
+
raise ArgumentError, "can't pre-write a register address > 4 bytes" if register.length > 4
|
77
|
+
buffer = [0x07, register.length] + register + buffer
|
78
|
+
end
|
79
|
+
|
80
|
+
# Enable and (re)disable repeated start as needed.
|
81
|
+
buffer = [0x02] + buffer + [0x03] if repeated_start
|
82
|
+
|
83
|
+
# Null terminate the command sequence.
|
84
|
+
buffer = buffer + [0x00]
|
85
|
+
|
86
|
+
# Send the command to the I2C1 interface, packed as uint8 string.
|
87
|
+
i2c_open(1, address)
|
88
|
+
read_bytes = Pigpio::IF.i2c_zip(pi_handle, i2c_handle, buffer.pack("C*"), read_length)
|
89
|
+
i2c_close
|
90
|
+
|
91
|
+
# Pigpio returned an error. Denko expects blank message after address.
|
92
|
+
if read_bytes.class == Integer
|
93
|
+
message = "#{address}-"
|
94
|
+
else
|
95
|
+
# Format the bytes like denko expects from a microcontroller.
|
96
|
+
message = read_bytes.split("").map { |byte| byte.ord.to_s }.join(",")
|
97
|
+
message = "#{address}-#{message}"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Call update with the message, as if it came from pin 2 (I2C1 SDA pin).
|
101
|
+
self.update(2, message)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
attr_reader :i2c_handle
|
108
|
+
|
109
|
+
def i2c_open(bus_index, address)
|
110
|
+
@i2c_handle = Pigpio::IF.i2c_open(pi_handle, bus_index, address, 0)
|
111
|
+
raise StandardError, "I2C error, code #{@i2c_handle}" if @i2c_handle < 0
|
112
|
+
end
|
113
|
+
|
114
|
+
def i2c_close
|
115
|
+
Pigpio::IF.i2c_close(pi_handle, i2c_handle)
|
116
|
+
@i2c_handle = nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
def infrared_emit(pin, frequency, pulses)
|
4
|
+
# 32-bit mask where only the bit corresponding to the GPIO in use is set.
|
5
|
+
pin_mask = 1 << pin
|
6
|
+
|
7
|
+
# IR frequency given in kHz. Find half wave time in microseconds.
|
8
|
+
if @aarch64
|
9
|
+
# Compensate for pigpio doubling wave times on 64-bit systems.
|
10
|
+
half_wave_time = (250.0 / frequency)
|
11
|
+
else
|
12
|
+
# True half wave time.
|
13
|
+
half_wave_time = (500.0 / frequency)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Standard wave setup.
|
17
|
+
new_wave
|
18
|
+
wave.tx_stop
|
19
|
+
wave.clear
|
20
|
+
wave.add_new
|
21
|
+
|
22
|
+
# Build an array of pulses to add to the wave.
|
23
|
+
wave_pulses = []
|
24
|
+
pulses.each_with_index do |pulse, index|
|
25
|
+
# Even indices send the carrier wave.
|
26
|
+
if (index % 2 == 0)
|
27
|
+
cycles = (pulse / (half_wave_time * 2)).round
|
28
|
+
cycles.times do
|
29
|
+
wave_pulses << wave.pulse(pin_mask, 0x00, half_wave_time)
|
30
|
+
wave_pulses << wave.pulse(0x00, pin_mask, half_wave_time)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Odd indices are idle.
|
34
|
+
else
|
35
|
+
if @aarch64
|
36
|
+
# Half idle times for 64-bit systems.
|
37
|
+
wave_pulses << wave.pulse(0x00, pin_mask, pulse / 2)
|
38
|
+
else
|
39
|
+
wave_pulses << wave.pulse(0x00, pin_mask, pulse)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
wave.add_generic(wave_pulses)
|
44
|
+
wave_id = wave.create
|
45
|
+
|
46
|
+
# Temporary workaround while Wave#send_once gets fixed.
|
47
|
+
Pigpio::IF.wave_send_once(@pi_handle, wave_id)
|
48
|
+
# wave.send_once(wave_id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
MAP = {
|
4
|
+
# Only use SPI1 interface.
|
5
|
+
SS: 18,
|
6
|
+
MISO: 19,
|
7
|
+
MOSI: 20,
|
8
|
+
SCK: 21,
|
9
|
+
|
10
|
+
# Only use I2C1 interface.
|
11
|
+
SDA: 2,
|
12
|
+
SCL: 3,
|
13
|
+
|
14
|
+
# Single UART
|
15
|
+
TX: 14,
|
16
|
+
RX: 15,
|
17
|
+
}
|
18
|
+
|
19
|
+
def map
|
20
|
+
MAP
|
21
|
+
end
|
22
|
+
|
23
|
+
def convert_pin(pin)
|
24
|
+
# Convert non numerical strings to symbols.
|
25
|
+
pin = pin.to_sym if (pin.class == String) && !(pin.match (/\A\d+\.*\d*/))
|
26
|
+
|
27
|
+
# Handle symbols.
|
28
|
+
if (pin.class == Symbol)
|
29
|
+
if map && map[pin]
|
30
|
+
return map[pin]
|
31
|
+
elsif map
|
32
|
+
raise ArgumentError, "error in pin: #{pin.inspect}. Make sure that pin is defined for this board by calling Board#map"
|
33
|
+
else
|
34
|
+
raise ArgumentError, "error in pin: #{pin.inspect}. Given a Symbol, but board has no map. Try using GPIO integer instead"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Handle integers.
|
39
|
+
return pin if pin.class == Integer
|
40
|
+
|
41
|
+
# Try #to_i on anyting else. Will catch numerical strings.
|
42
|
+
begin
|
43
|
+
return pin.to_i
|
44
|
+
rescue
|
45
|
+
raise ArgumentError, "error in pin: #{pin.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Denko
|
4
|
+
class PiBoard
|
5
|
+
def pulse_read(pin, reset: false, reset_time: 0, pulse_limit: 100, timeout: 200)
|
6
|
+
# Validation
|
7
|
+
raise ArgumentError, "error in reset: #{reset}. Should be either #{high} or #{low}" if reset && ![high, low].include?(reset)
|
8
|
+
raise ArgumentError, "errror in reset_time: #{reset_time}. Should be 0..65535 microseconds" if (reset_time < 0) || (reset_time > 0xFFFF)
|
9
|
+
raise ArgumentError, "errror in pulse_limit: #{pulse_limit}. Should be 0..255 pulses" if (pulse_limit < 0) || (pulse_limit > 0xFF)
|
10
|
+
raise ArgumentError, "errror in timeout: #{timeout}. Should be 0..65535 milliseconds" if (timeout < 0) || (timeout > 0xFFFF)
|
11
|
+
|
12
|
+
if reset
|
13
|
+
# Reset pulse will be captured as the first 2 edges.
|
14
|
+
expected_edges = pulse_limit + 2
|
15
|
+
else
|
16
|
+
# First edge is a starting reference, so store 1 extra.
|
17
|
+
expected_edges = pulse_limit + 1
|
18
|
+
end
|
19
|
+
|
20
|
+
# Storage for absolute tick time of each edge received from pigpio.
|
21
|
+
edges = Array.new(expected_edges) {0}
|
22
|
+
edge_index = 0
|
23
|
+
|
24
|
+
# Switch to input mode immediately if no reset.
|
25
|
+
set_pin_mode(pin, :input) unless reset
|
26
|
+
|
27
|
+
# Add callback to catch edges.
|
28
|
+
callback = get_gpio(pin).callback(EITHER_EDGE) do |tick, level, pin_cb|
|
29
|
+
edges[edge_index] = tick
|
30
|
+
edge_index += 1
|
31
|
+
callback.cancel if edge_index == expected_edges
|
32
|
+
end
|
33
|
+
|
34
|
+
# If using reset pulse, do it, and the mode switch, while the callback is active.
|
35
|
+
if reset
|
36
|
+
set_pin_mode(pin, :output)
|
37
|
+
Denko::GPIOD.set_value(pin, reset)
|
38
|
+
sleep(reset_time / 1000000.0)
|
39
|
+
|
40
|
+
# Set pull to opposite direction of reset.
|
41
|
+
if (reset == low)
|
42
|
+
set_pin_mode(pin, :input_pullup)
|
43
|
+
else
|
44
|
+
set_pin_mode(pin, :input_pulldown)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Wait for pulses or timeout.
|
49
|
+
begin
|
50
|
+
Timeout::timeout(timeout / 1000.0) do
|
51
|
+
loop do
|
52
|
+
edge_index == expected_edges ? break : sleep(0.001)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue Timeout::Error
|
56
|
+
# Allow less than pulse_limit to be read.
|
57
|
+
end
|
58
|
+
callback.cancel
|
59
|
+
|
60
|
+
# Ignore the first 2 edges (enable pulse) if reset used, 1 edge (starting reference) if not.
|
61
|
+
pulse_offset = reset ? 2 : 1
|
62
|
+
pulse_count = edge_index - pulse_offset
|
63
|
+
|
64
|
+
# Handle no pulses read.
|
65
|
+
if pulse_count < 1
|
66
|
+
self.update(pin, "")
|
67
|
+
return nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# Convert from edge times to pulses.
|
71
|
+
i = 0
|
72
|
+
pulses = Array.new(pulse_count) {0}
|
73
|
+
while i < pulse_count
|
74
|
+
pulses[i] = edges[i+pulse_offset] - edges[i+pulse_offset-1]
|
75
|
+
i += 1
|
76
|
+
end
|
77
|
+
|
78
|
+
# Format pulses as if coming from a microcontroller, and update the component.
|
79
|
+
self.update(pin, pulses.join(","))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
# CMD = 10
|
4
|
+
def servo_toggle(pin, value=:off, options={})
|
5
|
+
if value == :off
|
6
|
+
pwm_clear(pin)
|
7
|
+
digital_write(pin, 0)
|
8
|
+
else
|
9
|
+
@pwms[pin] = :servo
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# CMD = 11
|
14
|
+
def servo_write(pin, value=0)
|
15
|
+
Pigpio::IF.set_servo_pulsewidth(pi_handle, pin, value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
|
4
|
+
def spi_config(mode, bit_order)
|
5
|
+
# Config is a 32-bit mask, where bits 0 and 1 are a 2-bit number equal to the SPI mode.
|
6
|
+
# Default to SPI mode 0 when none given.
|
7
|
+
config = mode || 0
|
8
|
+
raise ArgumentError, "invalid SPI mode #{config}" unless (0..3).include? config
|
9
|
+
|
10
|
+
# Default to :msbfirst when bit_order not given.
|
11
|
+
bit_order ||= :msbfirst
|
12
|
+
unless (bit_order == :msbfirst) || (bit_order == :lsbfirst)
|
13
|
+
raise ArgumentError, "invalid bit order #{bitorder}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Bits 14 and 15 control MSBFIRST (0) or LSBFIRST (1) for MOSI and MISO respectively.
|
17
|
+
# Use same order for both directions like Arduino does.
|
18
|
+
config |= (0b11 << 14) if bit_order == :lsbfirst
|
19
|
+
|
20
|
+
# Use SPI1 interface instead of SPI0.
|
21
|
+
# Setting bit 8 means we're using SPI1.
|
22
|
+
config |= (0b1 << 8)
|
23
|
+
|
24
|
+
# Bits 5-7 set leaves all CE pins free, and allows any GPIO to be used for chip enable.
|
25
|
+
# We toggle separately in #spi_transfer.
|
26
|
+
config |= (0b111 << 5)
|
27
|
+
|
28
|
+
return config
|
29
|
+
end
|
30
|
+
|
31
|
+
# CMD = 26
|
32
|
+
def spi_transfer(select_pin, write: [], read: 0, frequency: nil, mode: nil, bit_order: nil)
|
33
|
+
# Default to 1MHz SPI frequency.
|
34
|
+
frequency ||= 1000000
|
35
|
+
|
36
|
+
# Make SPI config mask.
|
37
|
+
config = spi_config(mode, bit_order)
|
38
|
+
|
39
|
+
# Open SPI handle.
|
40
|
+
spi_open(frequency, config)
|
41
|
+
|
42
|
+
# Chip enable low. select_pin == 255 means no chip enable (mostly for APA102 LEDs).
|
43
|
+
digital_write(select_pin, 0) unless select_pin == 255
|
44
|
+
|
45
|
+
# Do the SPI transfer.
|
46
|
+
write_bytes = write.pack("C*")
|
47
|
+
read_bytes = Pigpio::IF.spi_xfer(pi_handle, spi_handle, write_bytes)
|
48
|
+
|
49
|
+
# Close SPI handle.
|
50
|
+
spi_close
|
51
|
+
|
52
|
+
# Chip enable high. select_pin == 255 means no chip enable (mostly for APA102 LEDs).
|
53
|
+
digital_write(select_pin, 1) unless select_pin == 255
|
54
|
+
|
55
|
+
# Handle spi_xfer errors.
|
56
|
+
raise StandardError, "spi_xfer error, code #{read_bytes}" if read_bytes.class == Integer
|
57
|
+
|
58
|
+
# Handle read bytes.
|
59
|
+
if read > 0
|
60
|
+
message = ""
|
61
|
+
|
62
|
+
# Format like a microcontrolelr would. Limit to number of bytes requested.
|
63
|
+
i = 0
|
64
|
+
while i < read
|
65
|
+
message = "#{message},#{read_bytes[i].ord}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Call update with the message as if coming from select_pin.
|
69
|
+
self.update(select_pin, message)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# CMD = 27
|
74
|
+
def spi_listen
|
75
|
+
end
|
76
|
+
|
77
|
+
# CMD = 28
|
78
|
+
def spi_stop
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
attr_reader :spi_handle
|
84
|
+
|
85
|
+
def spi_open(frequency, config)
|
86
|
+
# Give SPI channel as 0 (SPI CE0), even though we are toggling chip enable separately.
|
87
|
+
@spi_handle = Pigpio::IF.spi_open(pi_handle, 0, frequency, config)
|
88
|
+
raise StandardError, "SPI error, code #{@spi_handle}" if @spi_handle < 0
|
89
|
+
end
|
90
|
+
|
91
|
+
def spi_close
|
92
|
+
Pigpio::IF.spi_close(pi_handle, spi_handle)
|
93
|
+
@spi_handle = nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def spi_listeners
|
97
|
+
@spi_listeners ||= Array.new
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Denko
|
2
|
+
class PiBoard
|
3
|
+
# CMD = 17
|
4
|
+
def tone(pin, frequency, duration=nil)
|
5
|
+
# 32-bit mask where only the bit corresponding to the GPIO in use is set.
|
6
|
+
pin_mask = 1 << pin
|
7
|
+
|
8
|
+
if @aarch64
|
9
|
+
# pigpio doubles wave times on 64-bit systems for some reason. Halve it to compensate.
|
10
|
+
half_wave_time = (250000.0 / frequency).round
|
11
|
+
else
|
12
|
+
# This is the true halve wave time.
|
13
|
+
half_wave_time = (500000.0 / frequency).round
|
14
|
+
end
|
15
|
+
|
16
|
+
# Standard wave setup.
|
17
|
+
new_wave
|
18
|
+
wave.tx_stop
|
19
|
+
wave.clear
|
20
|
+
wave.add_new
|
21
|
+
|
22
|
+
# Build wave with a single cycle that will repeat.
|
23
|
+
wave.add_generic [
|
24
|
+
wave.pulse(pin_mask, 0x00, half_wave_time),
|
25
|
+
wave.pulse(0x00, pin_mask, half_wave_time)
|
26
|
+
]
|
27
|
+
wave_id = wave.create
|
28
|
+
|
29
|
+
# Temporary workaround while Wave#send_repeat gets fixed.
|
30
|
+
Pigpio::IF.wave_send_repeat(@pi_handle, wave_id)
|
31
|
+
# wave.send_repeat(wave_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
# CMD = 18
|
35
|
+
def no_tone(pin)
|
36
|
+
stop_wave
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/gpiod.rb
ADDED
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: denko-piboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.13.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- vickash
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-06-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pigpio
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: denko
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.13'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.13'
|
41
|
+
description: Denko::PiBoard is a drop-in replacement for Denko::Board. Use denko features
|
42
|
+
and component classes to be used directly on a Raspberry Pi.
|
43
|
+
email: mail@vickash.com
|
44
|
+
executables: []
|
45
|
+
extensions:
|
46
|
+
- ext/gpiod/extconf.rb
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- denko_piboard.gemspec
|
53
|
+
- examples/pi_system_monitor.rb
|
54
|
+
- ext/gpiod/extconf.rb
|
55
|
+
- ext/gpiod/gpiod.c
|
56
|
+
- lib/denko/piboard.rb
|
57
|
+
- lib/denko/piboard_base.rb
|
58
|
+
- lib/denko/piboard_core.rb
|
59
|
+
- lib/denko/piboard_i2c.rb
|
60
|
+
- lib/denko/piboard_infrared.rb
|
61
|
+
- lib/denko/piboard_map.rb
|
62
|
+
- lib/denko/piboard_pulse.rb
|
63
|
+
- lib/denko/piboard_servo.rb
|
64
|
+
- lib/denko/piboard_spi.rb
|
65
|
+
- lib/denko/piboard_tone.rb
|
66
|
+
- lib/denko/piboard_version.rb
|
67
|
+
- lib/gpiod.rb
|
68
|
+
homepage: https://github.com/denko-rb/denko-piboard
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata:
|
72
|
+
source_code_uri: https://github.com/denko-rb/denko-piboard
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubygems_version: 3.4.13
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: Use Raspberry Pi built-in GPIO as a Board class with the denko gem
|
92
|
+
test_files: []
|