From 911c0d053a635ed34d09066c3884ab108de1682a Mon Sep 17 00:00:00 2001 From: vickash Date: Wed, 7 Aug 2024 20:12:36 -0400 Subject: [PATCH] Rework and improve RotaryEncoder --- examples/digital_io/rotary_encoder.rb | 14 +++-- lib/denko/digital_io/rotary_encoder.rb | 85 ++++++++++++-------------- test/digital_io/rotary_encoder_test.rb | 83 ++++++++++++++----------- 3 files changed, 94 insertions(+), 88 deletions(-) diff --git a/examples/digital_io/rotary_encoder.rb b/examples/digital_io/rotary_encoder.rb index b16a8b8..d71c255 100644 --- a/examples/digital_io/rotary_encoder.rb +++ b/examples/digital_io/rotary_encoder.rb @@ -10,17 +10,21 @@ board = Denko::Board.new(Denko::Connection::Serial.new) encoder = Denko::DigitalIO::RotaryEncoder.new board: board, pins: { a: 4, b: 5 }, - divider: 1, # default, reads each pin every 1ms - steps_per_revolution: 30 # default + divider: 1, # Default. Applies only to Board. Read pin every 1ms. + debounce_time: 1, # Default. Applies only to PiBoard. Debounce filter set to 1 microsecond. + counts_per_revolution: 60 # Default # Reverse direction if needed. -# encoder.reverse +encoder.reverse -# Reset angle and steps to 0. +# Reset angle and count to 0. encoder.reset encoder.add_callback do |state| - puts "Encoder moved #{state[:change]} steps | CW step count: #{state[:steps]} | Current angle: #{state[:angle]}\xC2\xB0" + change_printable = state[:change].to_s + change_printable = "+#{change_printable}" if state[:change] > 0 + + puts "Encoder moved #{change_printable} | Count: #{state[:count]} | Angle: #{state[:angle]}\xC2\xB0" end sleep diff --git a/lib/denko/digital_io/rotary_encoder.rb b/lib/denko/digital_io/rotary_encoder.rb index e49dab4..4b9c19d 100644 --- a/lib/denko/digital_io/rotary_encoder.rb +++ b/lib/denko/digital_io/rotary_encoder.rb @@ -14,7 +14,7 @@ def initialize_pins(options={}) options[:pins][:b] = options[:pins][:dt] if options[:pins][:dt] options[:pins][:b] = options[:pins][:data] if options[:pins][:data] end - + # But always refer to them as a and b internally. [:clk, :clock, :dt, :data].each { |key| options[:pins].delete(key) } proxy_pin :a, DigitalIO::Input @@ -23,85 +23,76 @@ def initialize_pins(options={}) def after_initialize(options={}) super(options) - self.steps_per_revolution = options[:steps_per_revolution] || 30 - @reverse = false + @counts_per_revolution = options[:counts_per_revolution] || options[:cpr] || 60 + @reversed = false || options[:reversed] || options[:reverse] # Avoid repeated memory allocation. - self.state = { steps: 0, angle: 0 } - @reading = { steps: 0, angle: 0, change: 0} - - # DigitalInputs listen with default divider automatically. Override here. + self.state = { count: 0, angle: 0 } + @reading = { count: 0, angle: 0, change: 0} + + # PiBoard will use GPIO alerts, default to 1 microsecond debounce time. + @debounce_time = options[:debounce_time] || 1 + a.debounce_time = @debounce_time + b.debounce_time = @debounce_time + + # Board will default to 1ms digital listeners. @divider = options[:divider] || 1 a.listen(@divider) b.listen(@divider) - + + # Let initial state settle. + sleep 0.010 + observe_pins reset end - - attr_reader :reversed + + attr_reader :reversed, :counts_per_revolution, :divider, :debounce_time def reverse @reversed = !@reversed end - def steps_per_revolution - (360 / @degrees_per_step).to_i - end - - def steps_per_revolution=(step_count) - @degrees_per_step = 360.to_f / step_count + def degrees_per_count + @degrees_per_count ||= (360 / @counts_per_revolution.to_f) end - + def angle state[:angle] end - def steps - state[:steps] + def count + state[:count] end - + def reset - self.state = {steps: 0, angle: 0} + self.state = {count: 0, angle: 0} end - + private def observe_pins - # - # This is a quirk of listeners reading in numerical order. - # When observing the pins, attach a callback to the higher numbered pin (trailing), - # then read state of the lower numbered (leading). If not, direction will be reversed. - # - if a.pin > b.pin - trailing = a - leading = b - else - trailing = b - leading = a + a.add_callback do |a_state| + self.update((a_state == b.state) ? 1 : -1) end - - trailing.add_callback do |trailing_state| - change = (trailing_state == leading.state) ? 1 : -1 - change = -change if trailing == a - self.update(change) + + b.add_callback do |b_state| + self.update((b_state == a.state) ? -1 : 1) end end - + # # Take data (+/- 1 step change) and calculate new state. - # Return a hash with the new :steps and :angle. Pass through raw + # Return a hash with the new :count and :angle. Pass through raw # value in :change, so callbacks can use any of these. # def pre_callback_filter(step) step = -step if reversed + @state_mutex.synchronize { @reading[:count] = @state[:count] + step } @reading[:change] = step - @state_mutex.synchronize do - @reading[:steps] = @state[:steps] + step - end - @reading[:angle] = @reading[:steps] * @degrees_per_step % 360 - + @reading[:angle] = @reading[:count] * degrees_per_count % 360 + @reading end @@ -110,8 +101,8 @@ def pre_callback_filter(step) # def update_state(reading) @state_mutex.synchronize do - @state[:steps] = reading[:steps] - @state[:angle] = reading[:angle] + @state[:count] = reading[:count] + @state[:angle] = reading[:angle] end end end diff --git a/test/digital_io/rotary_encoder_test.rb b/test/digital_io/rotary_encoder_test.rb index fae8e1c..54f55e0 100644 --- a/test/digital_io/rotary_encoder_test.rb +++ b/test/digital_io/rotary_encoder_test.rb @@ -19,15 +19,29 @@ def test_alternate_pin_names assert_equal 6, long.a.pin end - def test_sets_steps_per_revolution - assert_equal 30, part.steps_per_revolution - assert_equal 12.to_f, part.instance_variable_get(:@degrees_per_step) + def test_counts_per_revolution + assert_equal 60, part.counts_per_revolution + assert_equal 6.to_f, part.degrees_per_count end def test_resets_on_initialize - assert_equal({steps: 0, angle: 0}, part.state) + assert_equal({count: 0, angle: 0}, part.state) end + def test_sets_debounce_time_for_both_pins + a_mock = Minitest::Mock.new.expect(:call, nil, [1]) + a_mock.expect(:call, nil, [2]) + b_mock = Minitest::Mock.new.expect(:call, nil, [1]) + b_mock.expect(:call, nil, [2]) + + part.a.stub(:debounce_time=, a_mock) do + part.b.stub(:debounce_time=, b_mock) do + part.send(:after_initialize) + part.send(:after_initialize, debounce_time: 2) + end + end + end + def test_calls_listen_on_both_pins_with_given_divider a_mock = Minitest::Mock.new.expect(:call, nil, [1]) a_mock.expect(:call, nil, [2]) @@ -49,56 +63,53 @@ def test_observes_on_initialize end end - def test_observes_the_right_pin - refute_empty part.a.callbacks - assert_empty part.b.callbacks - + def test_observes_the_pins part2 = Denko::DigitalIO::RotaryEncoder.new board: board, pins: {b: 6, a: 5} - refute_empty part2.b.callbacks - assert_empty part2.a.callbacks + refute_empty part2.a.callbacks end - def test_goes_the_right_direction - part.b.send(:update, 1) - part.a.send(:update, 1) - assert_equal({ steps: -1, angle: 348.0 }, part.state) - - part.reset - - part.b.send(:update, 1) - part.a.send(:update, 0) - assert_equal({ steps: 1, angle: 12.0 }, part.state) - end - def test_reverse part.reverse assert part.reversed - part.b.send(:update, 1) part.a.send(:update, 1) - assert_equal({ steps: 1, angle: 12.0 }, part.state) + part.b.send(:update, 1) + assert_equal({ count: 2, angle: 12.0 }, part.state) end - def test_swapped_pins - part2 = Denko::DigitalIO::RotaryEncoder.new board: board, pins: {b: 4, a: 3} - - part2.a.send(:update, 1) - part2.b.send(:update, 1) - assert_equal({ steps: 1, angle: 12.0 }, part2.state) - end - - def test_callback_prefilter - part.b.send(:update, 1) + def test_quadrature_decoding + part.b.send(:update, 0) part.a.send(:update, 0) callback_value = nil part.add_callback do |value| callback_value = value.dup end + + part.reset + part.a.send(:update, 1) + assert_equal({change: -1, count: -1, angle: 354.0}, callback_value) + part.b.send(:update, 1) + assert_equal({change: -1, count: -2, angle: 348.0}, callback_value) + part.a.send(:update, 0) - - assert_equal({change: 1, steps: 2, angle: 24.0}, callback_value) + assert_equal({change: -1, count: -3, angle: 342.0}, callback_value) + + part.b.send(:update, 0) + assert_equal({change: -1, count: -4, angle: 336.0}, callback_value) + + part.b.send(:update, 1) + assert_equal({change: 1, count: -3, angle: 342.0}, callback_value) + + part.a.send(:update, 1) + assert_equal({change: 1, count: -2, angle: 348.0}, callback_value) + + part.b.send(:update, 0) + assert_equal({change: 1, count: -1, angle: 354.0}, callback_value) + + part.a.send(:update, 0) + assert_equal({change: 1, count: 0, angle: 0.0}, callback_value) end def test_update_state_removes_change