diff --git a/Gemfile b/Gemfile index ec31b88..0c67494 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' # Specify your gem's dependencies in pi_piper-sysfs.gemspec -gemspec \ No newline at end of file +gemspec diff --git a/lib/pi_piper/sysfs.rb b/lib/pi_piper/sysfs.rb index 4c23e62..266ca43 100644 --- a/lib/pi_piper/sysfs.rb +++ b/lib/pi_piper/sysfs.rb @@ -1,6 +1,7 @@ -require "pi_piper/sysfs/version" -require "pi_piper/sysfs/sysfs" +require 'pi_piper' +require 'pi_piper/sysfs/version' +require 'pi_piper/sysfs/driver' module PiPiper - self.driver= PiPiper::Sysfs + self.driver = PiPiper::Sysfs::Driver end diff --git a/lib/pi_piper/sysfs/driver.rb b/lib/pi_piper/sysfs/driver.rb new file mode 100644 index 0000000..234b5dc --- /dev/null +++ b/lib/pi_piper/sysfs/driver.rb @@ -0,0 +1,110 @@ +module PiPiper + module Sysfs + class Driver < PiPiper::Driver + + GPIO_HIGH = 1 + GPIO_LOW = 0 + + def initialize + @exported_pins = Set.new + end + + def close + unexport_all + @exported_pins.empty? + end + + # Support GPIO pins + def pin_direction(pin, direction) + raise ArgumentError, "direction should be :in or :out" unless [:in, :out].include? direction + export(pin) + raise RuntimeError, "Pin #{pin} not exported" unless exported?(pin) + File.write(direction_file(pin), direction) + end + + def pin_read(pin) + raise ArgumentError, "Pin #{pin} not exported" unless exported?(pin) + File.read(value_file(pin)).to_i + end + + def pin_write(pin, value) + raise ArgumentError, "value should be GPIO_HIGH or GPIO_LOW" unless [GPIO_LOW, GPIO_HIGH].include? value + raise ArgumentError, "Pin #{pin} not exported" unless exported?(pin) + File.write(value_file(pin), value) + end + + def pin_set_pud(pin, value) + raise NotImplementedError, "Pull up/down not available with this driver. keep it on :off" unless value == :off + end + + def pin_set_trigger(pin, trigger) + raise ArgumentError, "trigger should be :falling, :rising, :both or :none" unless [:falling, :rising, :both, :none].include? trigger + raise ArgumentError, "Pin #{pin} not exported" unless exported?(pin) + File.write(edge_file(pin), trigger) + end + + def pin_wait_for(pin, trigger) + pin_set_trigger(pin, trigger) + fd = File.open(value_file(pin), 'r') + value = nil + + loop do + fd.read + IO.select(nil, nil, [fd], nil) + last_value = value + value = self.pin_read(pin) + if last_value != value + next if trigger == :rising and value == 0 + next if trigger == :falling and value == 1 + break + end + end + + end + + # Specific behaviours + + def unexport(pin) + File.write("/sys/class/gpio/unexport", pin) + @exported_pins.delete(pin) + end + + def unexport_all + @exported_pins.dup.each { |pin| unexport(pin) } + end + + def exported?(pin) + @exported_pins.include?(pin) + end + + private + + def export(pin) + raise RuntimeError, "pin #{pin} is already reserved by another Pin instance" if @exported_pins.include?(pin) + File.write("/sys/class/gpio/export", pin) + @exported_pins << pin + end + + def value_file(pin) + "/sys/class/gpio/gpio#{pin}/value" + end + + def edge_file(pin) + "/sys/class/gpio/gpio#{pin}/edge" + end + + def direction_file(pin) + "/sys/class/gpio/gpio#{pin}/direction" + end + + def pin_value_changed?(pin, trigger, value) + last_value = value + value = pin_read(pin) + return false if value == last_value + return false if trigger == :rising && value == 0 + return false if trigger == :falling && value == 1 + true + end + end + end +end diff --git a/lib/pi_piper/sysfs/sysfs.rb b/lib/pi_piper/sysfs/sysfs.rb deleted file mode 100644 index 2ef92ff..0000000 --- a/lib/pi_piper/sysfs/sysfs.rb +++ /dev/null @@ -1,73 +0,0 @@ -module PiPiper - class Sysfs < Driver - - GPIO_HIGH = 1 - GPIO_LOW = 0 - - def initialize - @exported_pins = Set.new - end - - def close - unexport_all - @exported_pins.empty? - end - -# Support GPIO pins - def pin_direction(pin, direction) - raise ArgumentError, "direction should be :in or :out" unless [:in, :out].include? direction - export(pin) - raise RuntimeError, "Pin #{pin} not exported" unless exported?(pin) - File.write("/sys/class/gpio/gpio#{pin}/direction", direction) - end - - def pin_read(pin) - raise ArgumentError, "Pin #{pin} not exported" unless exported?(pin) - File.read("/sys/class/gpio/gpio#{pin}/value").to_i - end - - def pin_write(pin, value) - raise ArgumentError, "value should be GPIO_HIGH or GPIO_LOW" unless [GPIO_LOW, GPIO_HIGH].include? value - raise ArgumentError, "Pin #{pin} not exported" unless exported?(pin) - File.write("/sys/class/gpio/gpio#{pin}/value", value) - end - - def pin_set_pud(pin, value) - raise NotImplementedError, "Pull up/down not avaliable with this driver. keep it on :off" unless value == :off - end - - def pin_set_trigger(pin, trigger) - raise ArgumentError, "trigger should be :falling, :rising, :both or :none" unless [:falling, :rising, :both, :none].include? trigger - raise ArgumentError, "Pin #{pin} not exported" unless exported?(pin) - File.write("/sys/class/gpio/gpio#{pin}/edge", trigger) - end - - def pin_wait_for(pin) - fd = File.open("/sys/class/gpio/gpio#{pin}/value", "r") - fd.read - IO.select(nil, nil, [fd], nil) - true - end - -# Specific behaviours - - def export(pin) - raise RuntimeError, "pin #{pin} is already reserved by another Pin instance" if @exported_pins.include?(pin) - File.write("/sys/class/gpio/export", pin) - @exported_pins << pin - end - - def unexport(pin) - File.write("/sys/class/gpio/unexport", pin) - @exported_pins.delete(pin) - end - - def unexport_all - @exported_pins.dup.each { |pin| unexport(pin) } - end - - def exported?(pin) - @exported_pins.include?(pin) - end - end -end diff --git a/lib/pi_piper/sysfs/version.rb b/lib/pi_piper/sysfs/version.rb index 5da3e42..33a5c38 100644 --- a/lib/pi_piper/sysfs/version.rb +++ b/lib/pi_piper/sysfs/version.rb @@ -1,6 +1,6 @@ module PiPiper class Driver; end - class Sysfs < Driver - VERSION = "0.1.0" + module Sysfs + VERSION = '0.1.0' end end diff --git a/pi_piper-sysfs.gemspec b/pi_piper-sysfs.gemspec index cd0a6d8..b4b3d4c 100644 --- a/pi_piper-sysfs.gemspec +++ b/pi_piper-sysfs.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |spec| spec.summary = %q{GPIO kernel driver library for the Raspberry Pi and PiPiper} spec.description = 'GPIO kernel driver library for the Raspberry Pi and other' \ ' boards that use the chipset. Commonly used with the' \ - ' PiPiper ruby library. it implements Pin (with events)' \ + ' PiPiper ruby library. It implements Pin (with events),' \ ' it reads from sysfs, and needs root UID to work' spec.homepage = "https://github.com/PiPiper/sysfs" diff --git a/spec/pi_piper/sysfs/driver_spec.rb b/spec/pi_piper/sysfs/driver_spec.rb new file mode 100644 index 0000000..47264cd --- /dev/null +++ b/spec/pi_piper/sysfs/driver_spec.rb @@ -0,0 +1,211 @@ +describe PiPiper::Sysfs::Driver do + let(:pins) { '@exported_pins' } + + before(:each) do + allow(File).to receive(:write).with('/sys/class/gpio/export', 4) + allow(File).to receive(:write).with('/sys/class/gpio/gpio4/direction', :in) + end + + describe '#initialize' do + it 'should load the driver' do + expect { PiPiper::Sysfs::Driver.new }.not_to raise_error + end + + it 'should not export any pins' do + expect(subject.instance_variable_get(pins)).to be_empty + end + end + + describe '#pin_direction' do + it 'should export the pin' do + allow(File).to receive(:write) + allow(subject).to receive(:exported?).with(4).and_return(true) + expect(subject).to receive(:export).with(4) + subject.pin_direction(4, :in) + end + + it 'should set the pin to :in when given :in' do + expect(File).to( + receive(:write).with('/sys/class/gpio/gpio4/direction', :in)) + + subject.pin_direction(4, :in) + end + + it 'should set the pin to :out when given :out' do + expect(File).to( + receive(:write).with('/sys/class/gpio/gpio4/direction', :out)) + + subject.pin_direction(4, :out) + end + + it 'should allow multiple pins to be exported' do + allow(File).to receive(:write).with('/sys/class/gpio/export', 5) + allow(File).to( + receive(:write).with('/sys/class/gpio/gpio5/direction', :in)) + + subject.pin_direction(4, :in) + subject.pin_direction(5, :in) + + expect(subject.instance_variable_get(pins)).to include(4, 5) + end + + it 'should raise an error if pin is already exported' do + subject.pin_direction(4, :in) + + expect { subject.pin_direction(4, :in) }.to raise_error(RuntimeError) + end + + it 'should raise an error on invalid directions' do + expect { subject.pin_direction(4, :bad) }.to raise_error(ArgumentError) + end + + it 'should raise an error if export fails' do + allow(subject).to receive(:exported?).with(4).and_return(false) + expect { subject.pin_direction(4, :in) }.to( + raise_error(RuntimeError, 'Pin 4 not exported')) + end + end + + describe '#pin_read' do + it 'should return the value of the pin' do + subject.pin_direction(4, :in) + expect(File).to receive(:read).with('/sys/class/gpio/gpio4/value') + subject.pin_read(4) + end + + it 'should raise an error if pin is not exported' do + expect { subject.pin_read(4) }.to( + raise_error(ArgumentError, 'Pin 4 not exported')) + end + end + + describe '#pin_write' do + it 'should write the value to the pin' do + allow(File).to( + receive(:write).with('/sys/class/gpio/gpio4/direction', :out)) + subject.pin_direction(4, :out) + expect(File).to receive(:write).with('/sys/class/gpio/gpio4/value', 1) + subject.pin_write(4, 1) + end + + it 'should raise an error if invalid value' do + expect { subject.pin_write(4, 25) }.to raise_error(ArgumentError) + end + + it 'should raise an error if pin is not exported' do + expect { subject.pin_write(4, 1) }.to( + raise_error(ArgumentError, 'Pin 4 not exported')) + end + end + + describe '#pin_set_pud' do + it 'should raise not implemented error because it is not implemented' do + expect { subject.pin_set_pud(4, :up) }.to raise_error(NotImplementedError) + end + end + + describe '#pin_set_trigger' do + before(:each) do + subject.pin_direction(4, :in) + end + + it 'should set the trigger for the pin to :both' do + expect(File).to receive(:write).with('/sys/class/gpio/gpio4/edge', :both) + subject.pin_set_trigger(4, :both) + end + + it 'should set the trigger for the pin to :none' do + expect(File).to receive(:write).with('/sys/class/gpio/gpio4/edge', :none) + subject.pin_set_trigger(4, :none) + end + + it 'should set the trigger for the pin to :falling' do + expect(File).to( + receive(:write).with('/sys/class/gpio/gpio4/edge', :falling)) + subject.pin_set_trigger(4, :falling) + end + + it 'should set the trigger for the pin to :rising' do + expect(File).to( + receive(:write).with('/sys/class/gpio/gpio4/edge', :rising)) + subject.pin_set_trigger(4, :rising) + end + + it 'should raise an error for invalid triggers' do + expect { subject.pin_set_trigger(4, :invalid) }.to( + raise_error(ArgumentError)) + end + + it 'should raise an error if pin is not exported' do + expect { subject.pin_set_trigger(5, :rising) }.to( + raise_error(ArgumentError, 'Pin 5 not exported')) + end + end + + xdescribe '#pin_wait_for' do + + end + + describe '#close' do + it 'should unexport all exported pins' do + subject.pin_direction(4, :in) + allow(File).to receive(:write).with('/sys/class/gpio/unexport', 4) + + subject.close + expect(subject.instance_variable_get(pins)).to be_empty + end + end + + describe '#unexport' do + before(:each) do + subject.pin_direction(4, :in) + end + + it 'should unexport the pin' do + allow(File).to receive(:write).with('/sys/class/gpio/unexport', 4) + subject.unexport(4) + expect(subject.instance_variable_get(pins)).to be_empty + end + + it 'should not export other pins' do + allow(File).to receive(:write).with('/sys/class/gpio/unexport', 4) + allow(File).to receive(:write).with('/sys/class/gpio/export', 5) + allow(File).to( + receive(:write).with('/sys/class/gpio/gpio5/direction', :in)) + subject.pin_direction(5, :in) + + subject.unexport(4) + + exported_pins = subject.instance_variable_get(pins) + expect(exported_pins).to include(5) + expect(exported_pins).not_to include(4) + end + end + + describe '#unexport_all' do + it 'should unexport all pins' do + allow(File).to receive(:write).with('/sys/class/gpio/export', 5) + allow(File).to( + receive(:write).with('/sys/class/gpio/gpio5/direction', :in)) + allow(File).to receive(:write).with('/sys/class/gpio/unexport', 4) + allow(File).to receive(:write).with('/sys/class/gpio/unexport', 5) + + subject.pin_direction(4, :in) + subject.pin_direction(5, :in) + + subject.unexport_all + expect(subject.instance_variable_get(pins)).to be_empty + end + end + + describe '#exported?' do + it 'should return true if pin is exported' do + subject.pin_direction(4, :in) + expect(subject.exported?(4)).to be(true) + end + + it 'should return false if pin is not exported' do + expect(subject.exported?(4)).to be(false) + end + end +end diff --git a/spec/pi_piper/sysfs_spec.rb b/spec/pi_piper/sysfs_spec.rb index 356c46f..198fe88 100644 --- a/spec/pi_piper/sysfs_spec.rb +++ b/spec/pi_piper/sysfs_spec.rb @@ -1,121 +1,5 @@ -require 'spec_helper' - describe PiPiper::Sysfs do - it 'has a version number' do - expect(PiPiper::Sysfs::VERSION).not_to be nil + it 'should have a version' do + expect(PiPiper::Sysfs::VERSION).not_to be_nil end - - context 'init & close' do - it 'should load the driver' do - expect{ PiPiper::Sysfs.new }.not_to raise_error - end - - it 'should unexport every pin at close' do - expect(subject).to receive(:unexport_all) - subject.close - end - end - - let(:file_like_object) { double("file like object") } - - before :example do - allow(File).to receive(:read).and_return("1") - allow(File).to receive(:write).and_return("1") - allow(File).to receive(:open).and_return(file_like_object) - end - - describe 'Specific behaviours' do - it '#export(pin)' do - expect(File).to receive(:write).with("/sys/class/gpio/export", 4) - expect(subject.instance_variable_get('@exported_pins')).not_to include(4) - subject.export(4) - expect(subject.instance_variable_get('@exported_pins')).to include(4) - end - - it '#unexport(pin)' do - expect(File).to receive(:write).with("/sys/class/gpio/unexport", 4) - - subject.export(4) - expect(subject.instance_variable_get('@exported_pins')).to include(4) - subject.unexport(4) - expect(subject.instance_variable_get('@exported_pins')).not_to include(4) - end - - it '#unexport_all' do - subject.export(4) - subject.export(18) - subject.export(27) - expect(subject.instance_variable_get('@exported_pins')).to eq Set.new([4,18,27]) - subject.unexport_all - expect(subject.instance_variable_get('@exported_pins')).to eq Set.new([]) - end - - it '#exported?(pin)' do - subject.export(4) - expect(subject.exported?(4)).to be true - expect(subject.exported?(112)).to be false - end - - context "when a pin is not exported" do - it 'should stop RW access to pin after unexport' do - subject.unexport(4) - expect { subject.pin_read(4) }.to raise_error ArgumentError, "Pin 4 not exported" - expect { subject.pin_write(4, 1) }.to raise_error ArgumentError, "Pin 4 not exported" - end - end - - context "when a pin is already exported" do - it 'should raise_error when a pin is already in use' do - subject.export(4) - expect { subject.export(4) }.to raise_error RuntimeError - end - end - end - - context 'API for Pin' do - it '#pin_direction(pin, direction)' do - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/direction", :in) - subject.pin_direction(5, :in) - expect(File).to receive(:write).with("/sys/class/gpio/gpio6/direction", :out) - subject.pin_direction(6, :out) - - expect{ subject.pin_direction(7, :inout) }.to raise_error ArgumentError, "direction should be :in or :out" - end - - it '#pin_write(pin, value)' do - subject.export(5) - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/value", 1) - subject.pin_write(5, 1) - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/value", 0) - subject.pin_write(5, 0) - expect { subject.pin_write(5, 99) }.to raise_error ArgumentError, "value should be GPIO_HIGH or GPIO_LOW" - end - - it '#pin_read(pin)' do - subject.export(5) - expect(File).to receive(:read).with("/sys/class/gpio/gpio5/value") - subject.pin_read(5) - end - - it '#pin_set_pud(pin, value)' do - expect {subject.pin_set_pud(5, :up)}.to raise_error NotImplementedError - end - - it '#pin_set_trigger(pin, trigger)' do - subject.export(5) - - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/edge", :none) - subject.pin_set_trigger(5, :none) - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/edge", :both) - subject.pin_set_trigger(5, :both) - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/edge", :falling) - subject.pin_set_trigger(5, :falling) - expect(File).to receive(:write).with("/sys/class/gpio/gpio5/edge", :rising) - subject.pin_set_trigger(5, :rising) - - expect { subject.pin_set_trigger(5, :not_a_trigger) }.to raise_error ArgumentError, "trigger should be :falling, :rising, :both or :none" - end - - it '#pin_wait_for(pin)' - end -end \ No newline at end of file +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 19f0f6c..2488316 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,9 @@ +module PiPiper + def self.driver=(driver) + # Placeholder for testing, PiPiper 3.0 contains this so we can remove + end +end + require 'pi_piper' require 'simplecov' SimpleCov.start