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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +103 -0
  3. data/LICENSE +21 -0
  4. data/PLAN.md +347 -0
  5. data/README.md +969 -0
  6. data/examples/adc.rb +60 -0
  7. data/examples/adc_led.rb +44 -0
  8. data/examples/button.rb +33 -0
  9. data/examples/lcd.rb +47 -0
  10. data/examples/lcd_thermometer.rb +57 -0
  11. data/examples/led.rb +31 -0
  12. data/examples/lowlevel/blink.rb +46 -0
  13. data/examples/lowlevel/button.rb +69 -0
  14. data/examples/lowlevel/servo.rb +76 -0
  15. data/examples/motion_sensor.rb +70 -0
  16. data/examples/motor.rb +38 -0
  17. data/examples/pwm_info.rb +68 -0
  18. data/examples/pwm_jitter.rb +139 -0
  19. data/examples/pwm_led.rb +56 -0
  20. data/examples/rgb_balance.rb +65 -0
  21. data/examples/rgb_led.rb +72 -0
  22. data/examples/servo.rb +70 -0
  23. data/examples/temperature.rb +53 -0
  24. data/lib/rgpio/bytes.rb +12 -0
  25. data/lib/rgpio/chip.rb +271 -0
  26. data/lib/rgpio/devices/adt7410.rb +116 -0
  27. data/lib/rgpio/devices/device.rb +41 -0
  28. data/lib/rgpio/devices/input_device.rb +150 -0
  29. data/lib/rgpio/devices/mcp3208.rb +104 -0
  30. data/lib/rgpio/devices/motor.rb +51 -0
  31. data/lib/rgpio/devices/output_device.rb +66 -0
  32. data/lib/rgpio/devices/pwm_channel.rb +25 -0
  33. data/lib/rgpio/devices/pwm_output_device.rb +109 -0
  34. data/lib/rgpio/devices/rgb_led.rb +175 -0
  35. data/lib/rgpio/devices/servo.rb +161 -0
  36. data/lib/rgpio/devices/st7032.rb +234 -0
  37. data/lib/rgpio/i2c.rb +175 -0
  38. data/lib/rgpio/line_request.rb +184 -0
  39. data/lib/rgpio/native.rb +238 -0
  40. data/lib/rgpio/pwm.rb +321 -0
  41. data/lib/rgpio/software_pwm.rb +290 -0
  42. data/lib/rgpio/spi.rb +208 -0
  43. data/lib/rgpio/version.rb +3 -0
  44. data/lib/rgpio.rb +99 -0
  45. metadata +152 -0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Measure how accurate a PWM waveform really is, using the kernel's own edge
4
+ # timestamps. A diagnostic, not a demo — nothing here is part of the gem's API.
5
+ #
6
+ # Wiring: one jumper between two header pins.
7
+ # GPIO23 (pin 16, output) ---- GPIO24 (pin 18, input)
8
+ #
9
+ # Driving a GPIO output straight into a GPIO input is safe; no resistor needed.
10
+ #
11
+ # Run (Ruby SoftwarePWM, generated in a forked process so the measuring loop
12
+ # does not compete with it for the GVL):
13
+ # ruby examples/pwm_jitter.rb --hz 50 --duty 0.075 --seconds 5
14
+ #
15
+ # Run (measure something else driving the pin — e.g. Python gpiozero, or
16
+ # HardwarePWM wired from GPIO12):
17
+ # ruby examples/pwm_jitter.rb --external --seconds 5
18
+ #
19
+ # What the numbers mean: `pulse` is the high time a servo reads as its position
20
+ # (1 us is about 0.09 degrees on a 180-degree servo), `period` is the frame
21
+ # rate. Spread matters more than the mean — a mean that is 3 us off is a fixed
22
+ # offset you can calibrate out, while a 200 us spread is visible twitching.
23
+
24
+ require_relative "../lib/rgpio"
25
+
26
+ options = {
27
+ out: 23, in: 24, hz: 50.0, duty: 0.075, seconds: 5.0,
28
+ spin: Rgpio::SoftwarePWM::DEFAULT_SPIN_US, external: false,
29
+ }
30
+
31
+ ARGV.each_with_index do |arg, i|
32
+ case arg
33
+ when "--out" then options[:out] = ARGV[i + 1].to_i
34
+ when "--in" then options[:in] = ARGV[i + 1].to_i
35
+ when "--hz" then options[:hz] = ARGV[i + 1].to_f
36
+ when "--duty" then options[:duty] = ARGV[i + 1].to_f
37
+ when "--seconds" then options[:seconds] = ARGV[i + 1].to_f
38
+ when "--spin" then options[:spin] = ARGV[i + 1].to_i
39
+ when "--external" then options[:external] = true
40
+ when "--help", "-h"
41
+ puts File.read(__FILE__).lines.grep(/^#/).join
42
+ exit 0
43
+ end
44
+ end
45
+
46
+ def stats(samples)
47
+ return nil if samples.empty?
48
+
49
+ sorted = samples.sort
50
+ mean = samples.sum / samples.size.to_f
51
+ variance = samples.sum { |v| (v - mean)**2 } / samples.size
52
+ {
53
+ n: samples.size, mean: mean, sd: Math.sqrt(variance),
54
+ min: sorted.first, p50: sorted[sorted.size / 2],
55
+ p99: sorted[(sorted.size * 0.99).floor], max: sorted.last,
56
+ }
57
+ end
58
+
59
+ def report(label, target_us, samples)
60
+ s = stats(samples)
61
+ return puts("#{label}: no samples") unless s
62
+
63
+ errors = samples.map { |v| (v - target_us).abs }
64
+ e = stats(errors)
65
+ puts format("%-7s target %8.1f us n=%d", label, target_us, s[:n])
66
+ puts format(" measured mean %8.1f sd %6.1f min %8.1f p50 %8.1f max %8.1f",
67
+ s[:mean], s[:sd], s[:min], s[:p50], s[:max])
68
+ puts format(" |error| mean %8.1f p50 %6.1f p99 %8.1f max %8.1f",
69
+ e[:mean], e[:p50], e[:p99], e[:max])
70
+ end
71
+
72
+ # Collect edge events for `seconds`, then report pulse widths and periods.
73
+ def measure(gpio, seconds, target_period_us, target_pulse_us)
74
+ events = []
75
+ Rgpio::Chip.open do |chip|
76
+ request = chip.request_lines(offsets: [gpio], direction: :input, edge: :both,
77
+ bias: :disabled, consumer: "pwm_jitter")
78
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
79
+ while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
80
+ events.concat(request.read_edge_events(timeout: 0.05, capacity: 64))
81
+ end
82
+ request.release
83
+ end
84
+
85
+ pulses = []
86
+ periods = []
87
+ last_rise = nil
88
+ events.each do |event|
89
+ if event[:type] == :rising
90
+ periods << ((event[:timestamp_ns] - last_rise) / 1000.0) if last_rise
91
+ last_rise = event[:timestamp_ns]
92
+ elsif last_rise
93
+ pulses << ((event[:timestamp_ns] - last_rise) / 1000.0)
94
+ end
95
+ end
96
+
97
+ puts "edges: #{events.size}"
98
+ report("pulse", target_pulse_us, pulses)
99
+ report("period", target_period_us, periods)
100
+
101
+ dropped = periods.count { |p| p > target_period_us * 1.5 }
102
+ puts format("dropped/late cycles (period > 1.5x target): %d of %d", dropped, periods.size)
103
+ end
104
+
105
+ period_us = 1_000_000.0 / options[:hz]
106
+ pulse_us = period_us * options[:duty]
107
+
108
+ puts "libgpiod #{Rgpio.version} | measuring GPIO#{options[:in]}, #{options[:seconds]} s"
109
+
110
+ if options[:external]
111
+ puts "generator: external (not driven by this script)"
112
+ measure(options[:in], options[:seconds], period_us, pulse_us)
113
+ else
114
+ puts format("generator: Rgpio::SoftwarePWM on GPIO%d, %g Hz, duty %.4f, spin_us %d (forked)",
115
+ options[:out], options[:hz], options[:duty], options[:spin])
116
+ ready_r, ready_w = IO.pipe
117
+
118
+ pid = fork do
119
+ ready_r.close
120
+ pwm = Rgpio::SoftwarePWM.new(options[:out], frequency: options[:hz], duty_cycle: options[:duty],
121
+ spin_us: options[:spin], consumer: "pwm_jitter_gen")
122
+ pwm.enable
123
+ ready_w.puts "ready"
124
+ ready_w.close
125
+ sleep options[:seconds] + 1.5
126
+ pwm.close
127
+ end
128
+
129
+ ready_w.close
130
+ ready_r.gets
131
+ ready_r.close
132
+ sleep 0.2 # let the first few cycles settle before sampling
133
+
134
+ begin
135
+ measure(options[:in], options[:seconds], period_us, pulse_us)
136
+ ensure
137
+ Process.waitpid(pid)
138
+ end
139
+ end
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Fade an LED up and down with PWM, on any GPIO line.
4
+ #
5
+ # Wiring (the same as examples/led.rb):
6
+ # GPIO4 (pin 7) -- 1k resistor -- LED anode (long leg)
7
+ # LED cathode (short leg) -- GND (pin 6)
8
+ #
9
+ # No dtoverlay and no config.txt entry: the brightness comes from
10
+ # Rgpio::SoftwarePWM, which drives any line. On GPIO12/13/18/19 you can pass
11
+ # pwm: :hardware instead for a peripheral-timed waveform.
12
+ #
13
+ # Run:
14
+ # ruby examples/pwm_led.rb
15
+
16
+ require_relative "../lib/rgpio"
17
+
18
+ LED_GPIO = 4
19
+ STEPS = 50
20
+ STEP_DELAY = 0.02
21
+ HOLD = 3
22
+
23
+ # Say what the LED is doing as it happens, so the terminal and the bench agree.
24
+ $stdout.sync = true
25
+
26
+ led = Rgpio::PWMLED.new(LED_GPIO)
27
+ puts "LED on GPIO#{LED_GPIO}, #{led.frequency} Hz. Ctrl-C to stop."
28
+
29
+ begin
30
+ puts "1) fading up and down, three times"
31
+ 3.times do
32
+ (0..STEPS).each do |i|
33
+ led.value = i / STEPS.to_f
34
+ sleep STEP_DELAY
35
+ end
36
+ (0..STEPS).reverse_each do |i|
37
+ led.value = i / STEPS.to_f
38
+ sleep STEP_DELAY
39
+ end
40
+ end
41
+
42
+ puts "2) holding fixed levels for #{HOLD} s each — one call per level, no loop"
43
+ [0.05, 0.25, 0.5, 1.0, 0.5].each do |level|
44
+ puts format(" value = %.2f (%.0f%% duty)", level, level * 100)
45
+ led.value = level
46
+ sleep HOLD
47
+ end
48
+
49
+ puts "3) off"
50
+ led.off
51
+ sleep 1
52
+ rescue Interrupt
53
+ puts "\nStopped."
54
+ ensure
55
+ led.close
56
+ end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Find the per-channel balance that makes an RGB LED's white look white.
4
+ #
5
+ # The three dies are not equally bright for equal duty — red drops about 1.9 V
6
+ # against 3.1 V for green and blue, so on a 3.3 V line with equal resistors the
7
+ # channels get very different currents, and the white ends up tinted. This walks
8
+ # through candidate scales with the LED showing white, prints each one, and waits
9
+ # for Enter so you can look before it moves on.
10
+ #
11
+ # Wiring: the same as examples/rgb_led.rb.
12
+ # GPIO17 (pin 11) -- 1k -- red, GPIO27 (pin 13) -- 1k -- green,
13
+ # GPIO22 (pin 15) -- 1k -- blue, common leg -- GND (pin 9)
14
+ #
15
+ # Run:
16
+ # ruby examples/rgb_balance.rb
17
+ #
18
+ # Note the scale that looks neutral and pass it from then on:
19
+ # Rgpio::RGBLED.new(red: 17, green: 27, blue: 22, balance: [0.7, 1.0, 0.6])
20
+ #
21
+ # Only reducing is possible: whichever channel is weakest stays at 1.0 and the
22
+ # others come down to meet it, so a tinted white costs some brightness.
23
+
24
+ require_relative "../lib/rgpio"
25
+
26
+ RED_GPIO = 17
27
+ GREEN_GPIO = 27
28
+ BLUE_GPIO = 22
29
+
30
+ # Each step keeps the weakest-looking channel at full and pulls one or two of the
31
+ # others down. Green is usually the weak one on 3.3 V, which reads as a blue or
32
+ # magenta white; the later rows dim red and blue further.
33
+ CANDIDATES = [
34
+ [1.0, 1.0, 1.0],
35
+ [0.8, 1.0, 0.8],
36
+ [0.7, 1.0, 0.7],
37
+ [0.6, 1.0, 0.6],
38
+ [0.5, 1.0, 0.5],
39
+ [0.7, 1.0, 0.5],
40
+ [0.5, 1.0, 0.7],
41
+ [1.0, 0.8, 0.8],
42
+ ].freeze
43
+
44
+ $stdout.sync = true
45
+
46
+ led = Rgpio::RGBLED.new(red: RED_GPIO, green: GREEN_GPIO, blue: BLUE_GPIO)
47
+ led.color = :white
48
+
49
+ puts "The LED is showing white. Press Enter to try the next balance, Ctrl-C to stop."
50
+ puts
51
+
52
+ begin
53
+ CANDIDATES.each_with_index do |balance, i|
54
+ led.balance = balance
55
+ puts format("%d/%d balance = %s", i + 1, CANDIDATES.size, balance.inspect)
56
+ $stdin.gets
57
+ end
58
+
59
+ puts "Through all of them. Re-run to compare the ones you liked."
60
+ rescue Interrupt
61
+ puts "\nStopped."
62
+ ensure
63
+ led.off
64
+ led.close
65
+ end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Cycle a full-colour LED through the corners of the colour cube, then mix.
4
+ #
5
+ # Wiring (common cathode: the long leg is the common one and goes to GND):
6
+ # GPIO17 (pin 11) -- 1k resistor -- red leg
7
+ # GPIO27 (pin 13) -- 1k resistor -- green leg
8
+ # GPIO22 (pin 15) -- 1k resistor -- blue leg
9
+ # common leg -- GND (pin 9)
10
+ #
11
+ # For a common-anode LED, tie the common leg to 3.3 V and pass active_low: true.
12
+ #
13
+ # White comes out tinted unless the channels are scaled to match — see BALANCE
14
+ # below and examples/rgb_balance.rb.
15
+ #
16
+ # GPIO2/3/4 work just as well (the pins the book uses) as long as the I2C bus is
17
+ # not enabled, since GPIO2/3 are SDA/SCL. Three software PWM channels need no
18
+ # dtoverlay; the hardware PWM peripheral could not do this at all, because the
19
+ # 40-pin header exposes only two of its channels.
20
+ #
21
+ # Run:
22
+ # ruby examples/rgb_led.rb
23
+
24
+ require_relative "../lib/rgpio"
25
+
26
+ RED_GPIO = 17
27
+ GREEN_GPIO = 27
28
+ BLUE_GPIO = 22
29
+ HOLD = 2
30
+
31
+ # Per-channel scale that makes white look white. This is the value the LED used
32
+ # for verification wanted — red at full, green and blue trimmed 20% — and it is
33
+ # a property of the part, not of the gem: run examples/rgb_balance.rb to find
34
+ # yours. [1.0, 1.0, 1.0] is the gem's default, i.e. no correction.
35
+ BALANCE = [1.0, 0.8, 0.8].freeze
36
+
37
+ # Say what the device is doing as it happens, so the terminal and the bench agree.
38
+ $stdout.sync = true
39
+
40
+ # Walk from one colour to another, a step at a time.
41
+ def fade(led, from, to, steps: 50, delay: 0.02)
42
+ (0..steps).each do |i|
43
+ position = i / steps.to_f
44
+ led.color = from.zip(to).map { |a, b| a + ((b - a) * position) }
45
+ sleep delay
46
+ end
47
+ end
48
+
49
+ led = Rgpio::RGBLED.new(red: RED_GPIO, green: GREEN_GPIO, blue: BLUE_GPIO, balance: BALANCE)
50
+ puts "RGB LED on GPIO#{RED_GPIO} (red) / #{GREEN_GPIO} (green) / #{BLUE_GPIO} (blue). Ctrl-C to stop."
51
+ puts "1) each named colour for #{HOLD} s"
52
+
53
+ begin
54
+ Rgpio::RGBLED::COLORS.each_key do |name|
55
+ next if name == :off
56
+
57
+ puts " #{name}"
58
+ led.color = name
59
+ sleep HOLD
60
+ end
61
+
62
+ puts "2) fading red into blue and back, twice"
63
+ 2.times do
64
+ fade(led, Rgpio::RGBLED::COLORS[:red], Rgpio::RGBLED::COLORS[:blue])
65
+ fade(led, Rgpio::RGBLED::COLORS[:blue], Rgpio::RGBLED::COLORS[:red])
66
+ end
67
+ rescue Interrupt
68
+ puts "\nStopped."
69
+ ensure
70
+ led.off
71
+ led.close
72
+ end
data/examples/servo.rb ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Sweep an RC servo, positioned by value (-1..1) and by angle.
4
+ #
5
+ # Wiring:
6
+ # GPIO4 (pin 7) -- servo signal (usually yellow or orange)
7
+ # 5 V (pin 2 or 4) -- servo power (red)
8
+ # GND (pin 6 or any GND) -- servo ground (brown or black)
9
+ #
10
+ # A servo under load draws more than the Pi's 5 V rail likes to give; if the Pi
11
+ # reboots mid-sweep, power the servo from its own supply with a common ground.
12
+ #
13
+ # No dtoverlay and no config.txt entry: the pulses come from Rgpio::SoftwarePWM,
14
+ # which measured 6 us of spread at this frame rate on an idle Pi 5 — about half a
15
+ # degree. On GPIO12/13/18/19 you can pass pwm: :hardware for a peripheral-timed
16
+ # pulse instead. examples/lowlevel/servo.rb drives the peripheral directly.
17
+ #
18
+ # Pulse widths vary by servo. 1000..2000 us is the range every hobby servo
19
+ # understands; widen it only as far as the datasheet allows, since a servo driven
20
+ # past its travel buzzes and heats up.
21
+ #
22
+ # Run:
23
+ # ruby examples/servo.rb
24
+
25
+ require_relative "../lib/rgpio"
26
+
27
+ SERVO_GPIO = 4
28
+
29
+ # Say what the device is doing as it happens, so the terminal and the bench agree.
30
+ $stdout.sync = true
31
+
32
+ servo = Rgpio::Servo.new(SERVO_GPIO, min_pulse_us: 1000, max_pulse_us: 2000)
33
+ puts "Servo on GPIO#{SERVO_GPIO}, #{servo.pwm.frequency} Hz frames. Ctrl-C to stop."
34
+
35
+ begin
36
+ puts "centre (#{servo.pulse_width_us.round} us)"
37
+ sleep 1
38
+
39
+ 2.times do
40
+ puts "one end"
41
+ servo.min
42
+ sleep 1
43
+ puts "the other"
44
+ servo.max
45
+ sleep 1
46
+ end
47
+
48
+ servo.mid
49
+ sleep 0.5
50
+
51
+ puts "sweeping by angle"
52
+ 2.times do
53
+ (-90..90).step(2) do |degrees|
54
+ servo.angle = degrees
55
+ sleep 0.01
56
+ end
57
+ (-90..90).step(2).reverse_each do |degrees|
58
+ servo.angle = degrees
59
+ sleep 0.01
60
+ end
61
+ end
62
+
63
+ puts "detaching — the horn goes limp and the servo stops drawing current"
64
+ servo.detach
65
+ sleep 1
66
+ rescue Interrupt
67
+ puts "\nStopped."
68
+ ensure
69
+ servo.close
70
+ end
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Read the ambient temperature from an ADT7410 I2C sensor once a second.
4
+ #
5
+ # Wiring (the sensor breakout runs on 3.3 V):
6
+ # 3.3V (pin 1) -- VDD
7
+ # GND (pin 9) -- GND
8
+ # GPIO2 (pin 3, SDA) -- SDA
9
+ # GPIO3 (pin 5, SCL) -- SCL
10
+ #
11
+ # Prerequisite — the header I2C bus must be enabled:
12
+ # sudo raspi-config nonint do_i2c 0 # or: dtparam=i2c_arm=on in config.txt
13
+ # sudo reboot
14
+ #
15
+ # Verify:
16
+ # ls /dev/i2c-1
17
+ # i2cdetect -y 1 # the sensor answers at 0x48 (0x49..0x4b if A0/A1 are high)
18
+ #
19
+ # Run:
20
+ # ruby examples/temperature.rb
21
+
22
+ require_relative "../lib/rgpio"
23
+
24
+ ADDRESS = 0x48
25
+ INTERVAL = 1.0
26
+
27
+ $stdout.sync = true # so the readings still appear when piped to a file
28
+
29
+ puts "I2C buses: #{Rgpio::I2C.buses.inspect}"
30
+
31
+ sensor = Rgpio::ADT7410.new(address: ADDRESS)
32
+
33
+ unless sensor.detected?
34
+ warn format("No ADT7410 at 0x%02x (ID register read back 0x%02x).", ADDRESS, sensor.id)
35
+ warn "Check the wiring and `i2cdetect -y 1`."
36
+ exit 1
37
+ end
38
+
39
+ puts format("ADT7410 at 0x%02x, ID 0x%02x, %d-bit mode. Ctrl-C to stop.", ADDRESS, sensor.id, sensor.resolution)
40
+
41
+ # The first conversion after power-up is still running for ~240 ms.
42
+ sleep Rgpio::ADT7410::CONVERSION_TIME
43
+
44
+ begin
45
+ loop do
46
+ puts format("%.4f degC", sensor.temperature)
47
+ sleep INTERVAL
48
+ end
49
+ rescue Interrupt
50
+ puts "\nStopped."
51
+ ensure
52
+ sensor.close
53
+ end
@@ -0,0 +1,12 @@
1
+ module Rgpio
2
+ # Turning argument lists into the bytes that go on a bus. Shared by {I2C} and
3
+ # {SPI} so that `write(0x40, "Hi")` and `write(0x40, 0x48, 0x69)` mean the same
4
+ # thing on both.
5
+ module Bytes
6
+ # @param bytes [Array<Integer, String, Array>]
7
+ # @return [String] binary string
8
+ def self.pack(bytes)
9
+ Array(bytes).flatten.map { |b| b.is_a?(String) ? b.b : [b].pack("C") }.join
10
+ end
11
+ end
12
+ end