Skip to content

Commit

Permalink
Rework and improve RotaryEncoder
Browse files Browse the repository at this point in the history
  • Loading branch information
vickash committed Aug 8, 2024
1 parent 13220df commit 911c0d0
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 88 deletions.
14 changes: 9 additions & 5 deletions examples/digital_io/rotary_encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
85 changes: 38 additions & 47 deletions lib/denko/digital_io/rotary_encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
83 changes: 47 additions & 36 deletions test/digital_io/rotary_encoder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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
Expand Down

0 comments on commit 911c0d0

Please sign in to comment.