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,51 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# A DC motor behind a two-input driver such as the DRV8835 or SN754410:
|
|
3
|
+
# one line drives it forward, the other backward.
|
|
4
|
+
#
|
|
5
|
+
# motor = Rgpio::Motor.new(forward: 2, backward: 14)
|
|
6
|
+
# motor.forward
|
|
7
|
+
# sleep 5
|
|
8
|
+
# motor.backward
|
|
9
|
+
# sleep 5
|
|
10
|
+
# motor.stop
|
|
11
|
+
# motor.close
|
|
12
|
+
#
|
|
13
|
+
# Speed control needs PWM on both lines and is not supported yet; the motor
|
|
14
|
+
# runs at full speed in either direction.
|
|
15
|
+
class Motor < Device
|
|
16
|
+
# @param forward [Integer] GPIO line that drives the motor forward
|
|
17
|
+
# @param backward [Integer] GPIO line that drives the motor backward
|
|
18
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
19
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
20
|
+
def initialize(forward:, backward:, chip: nil, consumer: "rgpio")
|
|
21
|
+
super(chip: chip)
|
|
22
|
+
@forward = OutputDevice.new(forward, chip: @chip, consumer: consumer)
|
|
23
|
+
@backward = OutputDevice.new(backward, chip: @chip, consumer: consumer)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Both directions are dropped before one is raised, so the driver is never
|
|
27
|
+
# asked to source and sink the same output at once.
|
|
28
|
+
def forward
|
|
29
|
+
@backward.off
|
|
30
|
+
@forward.on
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def backward
|
|
34
|
+
@forward.off
|
|
35
|
+
@backward.on
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def stop
|
|
39
|
+
@forward.off
|
|
40
|
+
@backward.off
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def release_resources
|
|
46
|
+
stop
|
|
47
|
+
@forward.close
|
|
48
|
+
@backward.close
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# A single GPIO line driven as an output.
|
|
3
|
+
#
|
|
4
|
+
# out = Rgpio::OutputDevice.new(17)
|
|
5
|
+
# out.on
|
|
6
|
+
# out.off
|
|
7
|
+
# out.close
|
|
8
|
+
class OutputDevice < Device
|
|
9
|
+
# @param gpio [Integer] GPIO line offset (BCM numbering)
|
|
10
|
+
# @param active_low [Boolean] when true, #on drives the line low
|
|
11
|
+
# @param initial_value [Boolean] level to drive as soon as the line is claimed
|
|
12
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
13
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
14
|
+
def initialize(gpio, active_low: false, initial_value: false, chip: nil, consumer: "rgpio")
|
|
15
|
+
super(chip: chip)
|
|
16
|
+
@gpio = gpio
|
|
17
|
+
@request = @chip.request_lines(
|
|
18
|
+
offsets: [gpio],
|
|
19
|
+
direction: :output,
|
|
20
|
+
active_low: active_low,
|
|
21
|
+
initial_value: initial_value ? :active : :inactive,
|
|
22
|
+
consumer: consumer
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Integer] the GPIO line offset this device drives
|
|
27
|
+
attr_reader :gpio
|
|
28
|
+
|
|
29
|
+
def on
|
|
30
|
+
self.value = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def off
|
|
34
|
+
self.value = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def toggle
|
|
38
|
+
self.value = !value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] true when the line is at its active level
|
|
42
|
+
def value
|
|
43
|
+
@request.get_value(@gpio) == :active
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def value=(level)
|
|
47
|
+
@request.set_value(@gpio, level ? :active : :inactive)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
alias on? value
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def release_resources
|
|
55
|
+
@request.release
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# An LED on a GPIO line.
|
|
60
|
+
#
|
|
61
|
+
# led = Rgpio::LED.new(4)
|
|
62
|
+
# 5.times { led.on; sleep 1; led.off; sleep 1 }
|
|
63
|
+
# led.close
|
|
64
|
+
class LED < OutputDevice
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# Shared by the PWM-backed devices ({PWMOutputDevice}, {Servo}): turns the
|
|
3
|
+
# `pwm:` option into a channel to drive the line with.
|
|
4
|
+
#
|
|
5
|
+
# A symbol means the device opens — and later closes — a channel of its own.
|
|
6
|
+
# Anything else is a channel the caller owns and keeps, so that several devices
|
|
7
|
+
# can share one, or a {HardwarePWM} already configured the way they want it.
|
|
8
|
+
module PWMChannel
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# @return [Array(Object, Boolean)] the channel, and whether it is ours to close
|
|
12
|
+
def resolve_pwm(pwm, gpio:, frequency:, chip:, consumer:)
|
|
13
|
+
return [pwm, false] unless pwm.is_a?(Symbol)
|
|
14
|
+
|
|
15
|
+
case pwm
|
|
16
|
+
when :software
|
|
17
|
+
[SoftwarePWM.new(gpio, frequency: frequency, chip: chip, consumer: consumer), true]
|
|
18
|
+
when :hardware
|
|
19
|
+
[HardwarePWM.new(gpio: gpio).tap { |channel| channel.frequency = frequency }, true]
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "pwm must be :software, :hardware or a PWM channel, got #{pwm.inspect}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# A GPIO line driven by a PWM channel, so it has a level between off and on
|
|
3
|
+
# rather than just the two.
|
|
4
|
+
#
|
|
5
|
+
# The channel is {SoftwarePWM} unless asked otherwise. It needs no dtoverlay
|
|
6
|
+
# and works on every line, where {HardwarePWM} needs a config.txt entry and
|
|
7
|
+
# reaches two header pins at a time — see PLAN.md for the measurements behind
|
|
8
|
+
# that default. Pass `pwm: :hardware` on GPIO12/13/18/19 to use the peripheral,
|
|
9
|
+
# or pass a channel object to share one.
|
|
10
|
+
class PWMOutputDevice
|
|
11
|
+
include PWMChannel
|
|
12
|
+
|
|
13
|
+
# Fast enough that an LED does not visibly flicker, and gpiozero's default.
|
|
14
|
+
DEFAULT_FREQUENCY = 100
|
|
15
|
+
|
|
16
|
+
# @param gpio [Integer] GPIO line offset (BCM numbering)
|
|
17
|
+
# @param frequency [Numeric] Hz; ignored when `pwm:` is a channel object,
|
|
18
|
+
# which the caller has already configured
|
|
19
|
+
# @param initial_value [Float] 0.0..1.0, applied before the channel starts
|
|
20
|
+
# @param active_low [Boolean] when true, 1.0 drives the line low — the
|
|
21
|
+
# wiring of a common-anode LED
|
|
22
|
+
# @param pwm [:software, :hardware, Object] channel to drive the
|
|
23
|
+
# line with, or one to borrow
|
|
24
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
25
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
26
|
+
def initialize(gpio, frequency: DEFAULT_FREQUENCY, initial_value: 0.0, active_low: false,
|
|
27
|
+
pwm: :software, chip: nil, consumer: "rgpio")
|
|
28
|
+
@gpio = gpio
|
|
29
|
+
@active_low = active_low
|
|
30
|
+
@closed = false
|
|
31
|
+
@pwm, @owns_pwm = resolve_pwm(pwm, gpio: gpio, frequency: frequency, chip: chip, consumer: consumer)
|
|
32
|
+
self.value = initial_value
|
|
33
|
+
@pwm.enable
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Integer] the GPIO line this device drives
|
|
37
|
+
attr_reader :gpio
|
|
38
|
+
|
|
39
|
+
# @return [Object] the PWM channel behind this device
|
|
40
|
+
attr_reader :pwm
|
|
41
|
+
|
|
42
|
+
# @return [Float] current level, 0.0..1.0
|
|
43
|
+
attr_reader :value
|
|
44
|
+
|
|
45
|
+
# @param ratio [Float] 0.0 = off, 1.0 = full on
|
|
46
|
+
def value=(ratio)
|
|
47
|
+
raise Error, "#{self.class} on GPIO#{@gpio} is closed" if @closed
|
|
48
|
+
unless ratio.is_a?(Numeric) && (0.0..1.0).cover?(ratio)
|
|
49
|
+
raise ArgumentError, "value must be in 0.0..1.0, got #{ratio.inspect}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@value = ratio.to_f
|
|
53
|
+
@pwm.duty_cycle = @active_low ? 1.0 - @value : @value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def on
|
|
57
|
+
self.value = 1.0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def off
|
|
61
|
+
self.value = 0.0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Invert the level, so a half-lit LED stays half-lit the other way about.
|
|
65
|
+
def toggle
|
|
66
|
+
self.value = 1.0 - @value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [Boolean] true when the line is not fully off
|
|
70
|
+
def on?
|
|
71
|
+
@value.positive?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
alias active? on?
|
|
75
|
+
|
|
76
|
+
# @return [Numeric] the channel's frequency in Hz
|
|
77
|
+
def frequency
|
|
78
|
+
@pwm.frequency
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def frequency=(hz)
|
|
82
|
+
@pwm.frequency = hz
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Stop the channel, and release it if this device opened it.
|
|
86
|
+
# Safe to call multiple times.
|
|
87
|
+
def close
|
|
88
|
+
return if @closed
|
|
89
|
+
|
|
90
|
+
@closed = true
|
|
91
|
+
@owns_pwm ? @pwm.close : @pwm.disable
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def closed?
|
|
95
|
+
@closed
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# An LED whose brightness can be set, not just its state.
|
|
100
|
+
#
|
|
101
|
+
# led = Rgpio::PWMLED.new(4)
|
|
102
|
+
# led.value = 0.25 # a quarter bright
|
|
103
|
+
# 10.times { |i| led.value = i / 9.0; sleep 0.1 }
|
|
104
|
+
# led.close
|
|
105
|
+
class PWMLED < PWMOutputDevice
|
|
106
|
+
alias brightness value
|
|
107
|
+
alias brightness= value=
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# A full-colour LED: three {PWMLED} channels behind one object.
|
|
3
|
+
#
|
|
4
|
+
# led = Rgpio::RGBLED.new(red: 2, green: 3, blue: 4)
|
|
5
|
+
# led.color = :magenta
|
|
6
|
+
# led.color = [1.0, 0.4, 0.0] # amber
|
|
7
|
+
# led.off
|
|
8
|
+
# led.close
|
|
9
|
+
#
|
|
10
|
+
# The default wiring is common cathode: each line sources current through its
|
|
11
|
+
# own resistor and the common leg goes to GND. For a common-anode part, tie the
|
|
12
|
+
# common leg to 3.3 V and pass `active_low: true`.
|
|
13
|
+
#
|
|
14
|
+
# Three software PWM channels means three generating threads, each spinning
|
|
15
|
+
# briefly around its edges (see {SoftwarePWM}). At the default 100 Hz that is
|
|
16
|
+
# some 16% of one core, and the threads occasionally collide, which shifts an
|
|
17
|
+
# edge by microseconds — invisible in an LED's brightness.
|
|
18
|
+
class RGBLED
|
|
19
|
+
# Corners of the colour cube, for the common case of naming a colour.
|
|
20
|
+
COLORS = {
|
|
21
|
+
off: [0.0, 0.0, 0.0],
|
|
22
|
+
red: [1.0, 0.0, 0.0],
|
|
23
|
+
green: [0.0, 1.0, 0.0],
|
|
24
|
+
blue: [0.0, 0.0, 1.0],
|
|
25
|
+
yellow: [1.0, 1.0, 0.0],
|
|
26
|
+
cyan: [0.0, 1.0, 1.0],
|
|
27
|
+
magenta: [1.0, 0.0, 1.0],
|
|
28
|
+
white: [1.0, 1.0, 1.0],
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# @param red [Integer] GPIO line of the red channel
|
|
32
|
+
# @param green [Integer] GPIO line of the green channel
|
|
33
|
+
# @param blue [Integer] GPIO line of the blue channel
|
|
34
|
+
# @param frequency [Numeric] Hz, for all three channels
|
|
35
|
+
# @param active_low [Boolean] true for a common-anode LED
|
|
36
|
+
# @param balance [Array<Float>] per-channel scale, 0.0..1.0 each, applied
|
|
37
|
+
# on top of every level asked for
|
|
38
|
+
# @param pwm [:software, :hardware, Hash] channel kind, or a
|
|
39
|
+
# {red:, green:, blue:} hash of channels to borrow
|
|
40
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
41
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
42
|
+
def initialize(red:, green:, blue:, frequency: PWMOutputDevice::DEFAULT_FREQUENCY,
|
|
43
|
+
active_low: false, balance: [1.0, 1.0, 1.0], pwm: :software, chip: nil, consumer: "rgpio")
|
|
44
|
+
@owns_chip = chip.nil? && pwm == :software
|
|
45
|
+
@chip = @owns_chip ? Chip.new : chip
|
|
46
|
+
@closed = false
|
|
47
|
+
@balance = validate_triple(balance, "balance")
|
|
48
|
+
@color = [0.0, 0.0, 0.0]
|
|
49
|
+
lines = { red: red, green: green, blue: blue }
|
|
50
|
+
# One channel per colour: handing the same channel object to all three
|
|
51
|
+
# would have them overwrite each other's duty cycle.
|
|
52
|
+
channels = pwm.is_a?(Hash) ? pwm : {}
|
|
53
|
+
@channels = lines.to_h do |colour, gpio|
|
|
54
|
+
channel = PWMLED.new(gpio,
|
|
55
|
+
frequency: frequency, active_low: active_low,
|
|
56
|
+
pwm: channels.fetch(colour, pwm), chip: @chip, consumer: consumer)
|
|
57
|
+
[colour, channel]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Hash{Symbol=>PWMLED}] the three channels, by colour
|
|
62
|
+
attr_reader :channels
|
|
63
|
+
|
|
64
|
+
# The colour that was asked for, not the duty cycles it became — {#balance}
|
|
65
|
+
# and `active_low` both sit between the two.
|
|
66
|
+
# @return [Array<Float>] red, green, blue in 0.0..1.0
|
|
67
|
+
attr_reader :color
|
|
68
|
+
|
|
69
|
+
# @param value [Array<Float>, Symbol] an r,g,b triple, or a name from {COLORS}
|
|
70
|
+
def color=(value)
|
|
71
|
+
triple = value.is_a?(Symbol) ? named_color(value) : value
|
|
72
|
+
unless triple.is_a?(Array) && triple.size == 3
|
|
73
|
+
raise ArgumentError, "color must be a three-element array or one of #{COLORS.keys.inspect}, " \
|
|
74
|
+
"got #{value.inspect}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@color = validate_triple(triple, "color")
|
|
78
|
+
apply
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Array<Float>] the per-channel scale in force
|
|
82
|
+
attr_reader :balance
|
|
83
|
+
|
|
84
|
+
# Re-scale the channels, keeping the colour that was asked for. Setting this
|
|
85
|
+
# while white is showing is how the calibration example works.
|
|
86
|
+
def balance=(triple)
|
|
87
|
+
@balance = validate_triple(triple, "balance")
|
|
88
|
+
apply
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def red
|
|
92
|
+
@color[0]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def green
|
|
96
|
+
@color[1]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def blue
|
|
100
|
+
@color[2]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def red=(level)
|
|
104
|
+
self.color = [level, green, blue]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def green=(level)
|
|
108
|
+
self.color = [red, level, blue]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def blue=(level)
|
|
112
|
+
self.color = [red, green, level]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Full brightness on all three channels, which is white.
|
|
116
|
+
def on
|
|
117
|
+
self.color = :white
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def off
|
|
121
|
+
self.color = :off
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Invert every channel, so a dimmed colour comes back as its complement.
|
|
125
|
+
def toggle
|
|
126
|
+
self.color = @color.map { |level| 1.0 - level }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Boolean] true when any channel is lit
|
|
130
|
+
def on?
|
|
131
|
+
@color.any?(&:positive?)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
alias active? on?
|
|
135
|
+
|
|
136
|
+
# Stop all three channels, and close the chip if this LED opened it.
|
|
137
|
+
# Safe to call multiple times.
|
|
138
|
+
def close
|
|
139
|
+
return if @closed
|
|
140
|
+
|
|
141
|
+
@closed = true
|
|
142
|
+
@channels.each_value(&:close)
|
|
143
|
+
@chip.close if @owns_chip
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def closed?
|
|
147
|
+
@closed
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# Push the requested colour out through the balance. The channels hold the
|
|
153
|
+
# scaled levels; #color keeps reporting what was asked for.
|
|
154
|
+
def apply
|
|
155
|
+
@channels.values.each_with_index do |channel, i|
|
|
156
|
+
channel.value = @color[i] * @balance[i]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_triple(triple, what)
|
|
161
|
+
unless triple.is_a?(Array) && triple.size == 3 &&
|
|
162
|
+
triple.all? { |v| v.is_a?(Numeric) && (0.0..1.0).cover?(v) }
|
|
163
|
+
raise ArgumentError, "#{what} must be three numbers in 0.0..1.0, got #{triple.inspect}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
triple.map(&:to_f)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def named_color(name)
|
|
170
|
+
COLORS.fetch(name) do
|
|
171
|
+
raise ArgumentError, "unknown colour #{name.inspect}; known: #{COLORS.keys.inspect}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Rgpio
|
|
2
|
+
# An RC servo, positioned by the width of a pulse repeated every 20 ms.
|
|
3
|
+
#
|
|
4
|
+
# servo = Rgpio::Servo.new(12)
|
|
5
|
+
# servo.max # one end of its travel
|
|
6
|
+
# servo.mid # centre
|
|
7
|
+
# servo.angle = 45 # or by angle
|
|
8
|
+
# servo.detach # stop holding the position
|
|
9
|
+
# servo.close
|
|
10
|
+
#
|
|
11
|
+
# Pulse widths vary by servo: 1000..2000 us is the safe range every hobby
|
|
12
|
+
# servo understands, and many reach further (500..2500 us). A servo driven past
|
|
13
|
+
# its travel buzzes and heats up, so widen the range only as far as the part's
|
|
14
|
+
# datasheet allows, and by measuring rather than by trusting.
|
|
15
|
+
#
|
|
16
|
+
# Accuracy: the default {SoftwarePWM} channel places the pulse within about
|
|
17
|
+
# 6 us on an idle Pi 5, which is half a degree of travel; a busy machine can
|
|
18
|
+
# push an occasional pulse 30-80 us out (see PLAN.md). `pwm: :hardware` on
|
|
19
|
+
# GPIO12/13/18/19 removes that entirely, at the cost of a config.txt entry.
|
|
20
|
+
class Servo
|
|
21
|
+
include PWMChannel
|
|
22
|
+
|
|
23
|
+
# The frame rate every hobby servo expects.
|
|
24
|
+
DEFAULT_FREQUENCY = 50
|
|
25
|
+
|
|
26
|
+
DEFAULT_MIN_PULSE_US = 1000
|
|
27
|
+
DEFAULT_MAX_PULSE_US = 2000
|
|
28
|
+
|
|
29
|
+
# @param gpio [Integer] GPIO line offset (BCM numbering)
|
|
30
|
+
# @param min_pulse_us [Numeric] pulse width at value -1.0
|
|
31
|
+
# @param max_pulse_us [Numeric] pulse width at value +1.0
|
|
32
|
+
# @param frequency [Numeric] frame rate in Hz; ignored when `pwm:` is a
|
|
33
|
+
# channel object, which the caller has already configured
|
|
34
|
+
# @param min_angle [Numeric] angle reported at value -1.0
|
|
35
|
+
# @param max_angle [Numeric] angle reported at value +1.0
|
|
36
|
+
# @param initial_value [Float, nil] -1.0..1.0 to start there, nil to start
|
|
37
|
+
# detached (no pulses, so the horn is free)
|
|
38
|
+
# @param pwm [:software, :hardware, Object] channel to drive with
|
|
39
|
+
# @param chip [Chip, nil] chip to share, or nil to open one
|
|
40
|
+
# @param consumer [String] name shown in the kernel's request list
|
|
41
|
+
def initialize(gpio, min_pulse_us: DEFAULT_MIN_PULSE_US, max_pulse_us: DEFAULT_MAX_PULSE_US,
|
|
42
|
+
frequency: DEFAULT_FREQUENCY, min_angle: -90, max_angle: 90,
|
|
43
|
+
initial_value: 0.0, pwm: :software, chip: nil, consumer: "rgpio")
|
|
44
|
+
raise ArgumentError, "max_pulse_us must exceed min_pulse_us" unless max_pulse_us > min_pulse_us
|
|
45
|
+
|
|
46
|
+
@gpio = gpio
|
|
47
|
+
@min_pulse_us = min_pulse_us.to_f
|
|
48
|
+
@max_pulse_us = max_pulse_us.to_f
|
|
49
|
+
@min_angle = min_angle.to_f
|
|
50
|
+
@max_angle = max_angle.to_f
|
|
51
|
+
@value = nil
|
|
52
|
+
@closed = false
|
|
53
|
+
@pwm, @owns_pwm = resolve_pwm(pwm, gpio: gpio, frequency: frequency, chip: chip, consumer: consumer)
|
|
54
|
+
self.value = initial_value
|
|
55
|
+
@pwm.enable
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Integer] the GPIO line this servo is driven from
|
|
59
|
+
attr_reader :gpio
|
|
60
|
+
|
|
61
|
+
# @return [Object] the PWM channel behind this servo
|
|
62
|
+
attr_reader :pwm
|
|
63
|
+
|
|
64
|
+
# @return [Float, nil] -1.0..1.0, or nil when detached
|
|
65
|
+
attr_reader :value
|
|
66
|
+
|
|
67
|
+
# @return [Float] pulse width at value -1.0
|
|
68
|
+
attr_reader :min_pulse_us
|
|
69
|
+
|
|
70
|
+
# @return [Float] pulse width at value +1.0
|
|
71
|
+
attr_reader :max_pulse_us
|
|
72
|
+
|
|
73
|
+
# @param position [Float, nil] -1.0..1.0, or nil to detach
|
|
74
|
+
def value=(position)
|
|
75
|
+
raise Error, "Servo on GPIO#{@gpio} is closed" if @closed
|
|
76
|
+
|
|
77
|
+
if position.nil?
|
|
78
|
+
detach
|
|
79
|
+
else
|
|
80
|
+
unless position.is_a?(Numeric) && (-1.0..1.0).cover?(position)
|
|
81
|
+
raise ArgumentError, "value must be in -1.0..1.0 or nil, got #{position.inspect}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@value = position.to_f
|
|
85
|
+
@pwm.pulse_width_us = @min_pulse_us + ((@max_pulse_us - @min_pulse_us) * ((@value + 1.0) / 2.0))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def min
|
|
90
|
+
self.value = -1.0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def mid
|
|
94
|
+
self.value = 0.0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def max
|
|
98
|
+
self.value = 1.0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @return [Float, nil] the current position in degrees, or nil when detached
|
|
102
|
+
def angle
|
|
103
|
+
return nil if @value.nil?
|
|
104
|
+
|
|
105
|
+
@min_angle + ((@max_angle - @min_angle) * ((@value + 1.0) / 2.0))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param degrees [Numeric, nil] between min_angle and max_angle
|
|
109
|
+
def angle=(degrees)
|
|
110
|
+
if degrees.nil?
|
|
111
|
+
detach
|
|
112
|
+
else
|
|
113
|
+
low, high = [@min_angle, @max_angle].minmax
|
|
114
|
+
unless degrees.is_a?(Numeric) && (low..high).cover?(degrees)
|
|
115
|
+
raise ArgumentError, "angle must be in #{low}..#{high} degrees or nil, got #{degrees.inspect}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
self.value = (((degrees - @min_angle) / (@max_angle - @min_angle)) * 2.0) - 1.0
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Float, nil] the pulse width currently being sent, or nil when detached
|
|
123
|
+
def pulse_width_us
|
|
124
|
+
@value.nil? ? nil : @pwm.pulse_width_us
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Drive a pulse width directly, for calibrating a servo's real travel.
|
|
128
|
+
def pulse_width_us=(us)
|
|
129
|
+
raise ArgumentError, "pulse width must be positive, got #{us}" unless us.is_a?(Numeric) && us.positive?
|
|
130
|
+
|
|
131
|
+
@pwm.pulse_width_us = us
|
|
132
|
+
# The position no longer follows from #value, so stop claiming it does.
|
|
133
|
+
@value = nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Stop sending pulses. The servo stops holding its position and can be
|
|
137
|
+
# turned by hand — and stops drawing current fighting a load.
|
|
138
|
+
def detach
|
|
139
|
+
@pwm.duty_cycle = 0.0
|
|
140
|
+
@value = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Boolean] true while pulses are being sent
|
|
144
|
+
def attached?
|
|
145
|
+
!@value.nil?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Stop the channel, and release it if this servo opened it.
|
|
149
|
+
# Safe to call multiple times.
|
|
150
|
+
def close
|
|
151
|
+
return if @closed
|
|
152
|
+
|
|
153
|
+
@closed = true
|
|
154
|
+
@owns_pwm ? @pwm.close : @pwm.disable
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def closed?
|
|
158
|
+
@closed
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|