diff --git a/examples/saa1064-two-digits.py b/examples/saa1064-two-digits.py new file mode 100644 index 0000000..e42de4f --- /dev/null +++ b/examples/saa1064-two-digits.py @@ -0,0 +1,13 @@ +import quick2wire.i2c as i2c +from quick2wire.parts.saa1064 import SAA1064, STATIC_MODE +from time import sleep + +saa1064 = SAA1064(i2c.I2CMaster(), digits=2) +saa1064.brightness=0b01100000 + +for y in range(10): + saa1064.digit(0).value(y) + for x in range(10): + saa1064.digit(1).value(x) + saa1064.write() + sleep(0.1) \ No newline at end of file diff --git a/examples/seven_segment_display_with_two_digits.py b/examples/seven_segment_display_with_two_digits.py new file mode 100644 index 0000000..4002c59 --- /dev/null +++ b/examples/seven_segment_display_with_two_digits.py @@ -0,0 +1,17 @@ +__author__ = 'stuartervine' + +from quick2wire import i2c +from quick2wire.parts.saa1064 import SAA1064 +from quick2wire.parts.seven_segment_display import SevenSegmentDisplay +from time import sleep + +saa1064 = SAA1064(i2c.I2CMaster(), digits=4) +sevenSegmentDisplay=SevenSegmentDisplay(saa1064) + +sevenSegmentDisplay.display('1.5') +sleep(1) +sevenSegmentDisplay.display('100R') +sleep(1) + +for i in range(9999): + sevenSegmentDisplay.display(i) diff --git a/quick2wire/gpio.py b/quick2wire/gpio.py index fe8e3a8..98d021f 100644 --- a/quick2wire/gpio.py +++ b/quick2wire/gpio.py @@ -6,6 +6,7 @@ import subprocess from contextlib import contextmanager from quick2wire.board_revision import revision +from quick2wire.pin import PinAPI, PinBankAPI from quick2wire.selector import EDGE @@ -27,46 +28,6 @@ def gpio_admin(subcommand, pin, pull=None): PullUp = "pullup" - -class PinAPI(object): - def __init__(self, bank, index): - self._bank = bank - self._index = index - - @property - def index(self): - return self._index - - @property - def bank(self): - return self._bank - - def __enter__(self): - self.open() - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - value = property(lambda p: p.get(), - lambda p,v: p.set(v), - doc="""The value of the pin: 1 if the pin is high, 0 if the pin is low.""") - - -class PinBankAPI(object): - def __getitem__(self, n): - if 0 < n < len(self): - raise ValueError("no pin index {n} out of range", n=n) - return self.pin(n) - - def write(self): - pass - - def read(self): - pass - - - class Pin(PinAPI): """Controls a GPIO pin.""" @@ -204,9 +165,6 @@ def __str__(self): index=self.index) - - - class PinBank(PinBankAPI): def __init__(self, index_to_soc_fn, count=None): super(PinBank,self).__init__() diff --git a/quick2wire/parts/fake_i2c.py b/quick2wire/parts/fake_i2c.py new file mode 100644 index 0000000..be014e9 --- /dev/null +++ b/quick2wire/parts/fake_i2c.py @@ -0,0 +1,56 @@ +__author__ = 'stuart' + +class FakeI2CMaster: + def __init__(self): + self._requests = [] + self._responses = [] + self._next_response = 0 + self.message_precondition = lambda m: True + + def all_messages_must(self, p): + self.message_precondition + + def clear(self): + self.__init__() + + def transaction(self, *messages): + for m in messages: + self.message_precondition(m) + + self._requests.append(messages) + return [] + + def add_response(self, *messages): + self._responses.append(messages) + + + @property + def request_count(self): + return len(self._requests) + + def request(self, n): + return self._requests[n] + + def request_at(self, n): + return I2CRequestWrapper(self._requests[n]) + + def message(self, message_index): + request = self.request(0) + return I2CMessageWrapper(request[message_index]) + +class I2CRequestWrapper: + def __init__(self, i2c_request): + self._i2c_request = i2c_request + + def message(self, index): + return I2CMessageWrapper(self._i2c_request[index]) + + +class I2CMessageWrapper: + def __init__(self, i2_message): + self._i2c_message = i2_message + self.len = i2_message.len + + def byte(self, index): + return self._i2c_message.buf[index][0] + diff --git a/quick2wire/parts/saa1064.py b/quick2wire/parts/saa1064.py new file mode 100644 index 0000000..944b59d --- /dev/null +++ b/quick2wire/parts/saa1064.py @@ -0,0 +1,248 @@ +""" +API for the SAA1064 seven segment display driver chip. + +The SAA1064 can drive up to 4 seven segment displays simultaneously. +There are 2 modes the chip can run in: + +STATIC mode - drives 2 seven segment displays. +Where no multiplexing is required, and the outputs of the pins 3-11 +and 14-22 can be connected directly to the LED displays. +[URL] + +DYNAMIC mode - drives 4 seven segment displays. +The chip multiplexes the output of displays 1+3 and 2+4. The pairs +of displays are connected to the same output pins as above. The circuit +requires 2 transistors to be connected to the multiplex outputs to control +which display to trigger. + +Applications talk to the chip via objects of the SAA1064 class. A +SAA1064 object is created with an I2CMaster and a number of digits between 1-4. + +The display brightness can be supplied using the brightness property. +This supports values between 0-7. + +The displays can then be controlled individually by setting valid values +on digits and then calling write(). + +For example: + + with I2CMaster() as i2c: + saa1064 = SAA1064(i2c, digits=2) + + saa1064.digit(0).value('1') + saa1064.digit(1).value('2') + saa1064.write() + +The current values supported for a digit are: 0-9, A-F, r + +Specifying a decimal point after the value will light the point in the display. + +For example: + with I2CMaster() as i2c: + saa1064 = SAA1064(i2c, digits=2) + + saa1064.digit(0).value('1.') + +The displays are configured using the following value conversions for the display segments + + --64-- + | | + 2 32 + | | + -- 1-- + | | + 4 16 + | | + -- 8-- DP-128 + +""" +from quick2wire.pin import PinAPI, PinBankAPI + + +__author__ = 'stuartervine' + +from functools import reduce +from operator import or_ +from quick2wire.i2c import writing_bytes + +displayController = 0x38 +STATIC_MODE = 0b00000000 +DYNAMIC_MODE = 0b00000001 +CONTINUOUS_DISPLAY = 0b00000110 + +DECIMAL_POINT = 0b10000000 + +digit_map = { + '0':126, + '1':48, + '2':109, + '3':121, + '4':51, + '5':91, + '6':95, + '7':112, + '8':127, + '9':123, + 'A':119, + 'B':31, + 'C':78, + 'D':61, + 'E':79, + 'F':71, + 'R':5 +} + +class SAA1064(object): + def create_pin_bank(self, i): + return _PinBank(i) + + def __init__(self, master, digits=1, brightness=7): + self.master = master + self.brightness = brightness + self.configured = False + + if digits <= 0 or digits > 4: + raise ValueError('SAA1064 only supports driving 1 to 4 digits') + elif digits <= 2: + self._mode = STATIC_MODE + else: + self._mode = DYNAMIC_MODE + + self._pin_bank = tuple(self.create_pin_bank(i) for i in range(digits)) + + def configure(self): + """Writes the configured control byte to the chip.""" + self.write_control(self.mode|self._brightness|CONTINUOUS_DISPLAY) + + def reset(self): + for pin_bank in self: + pin_bank.value = 0 + + def write_control(self, control_byte): + """Writes a raw control byte to the chip.""" + self.master.transaction( + writing_bytes(displayController, 0b00000000, control_byte) + ) + self.configured = True + + def write(self): + """Writes the segment outputs to the chip.""" + if(not self.configured): + self.configure() + + i2c_messages = [pin_bank.i2c_message for pin_bank in self._pin_bank] + self.master.transaction(*i2c_messages) + + @property + def mode(self): + """Returns the display mode the chip is currently configured in, 0: static, 1: dynamic""" + return self._mode + + @mode.setter + def mode(self, mode): + """Changes the display mode the chip is configured in.""" + if mode > 1 or mode < 0: + raise ValueError('invalid mode ' + str(mode) + ' only STATIC_MODE and DYNAMIC_MODE are supported') + self._mode = mode + + @property + def brightness(self): + """Returns the current brightness level configured for the LED displays.""" + return self._brightness >> 5 + + @brightness.setter + def brightness(self, brightness): + """Sets the brightness of the LED displays, between 0-7.""" + if brightness > 7: + raise ValueError('invalid brightness, valid between 0-7.') + self._brightness = brightness << 5 + + def digit(self, index): + """Returns a Digit object for the display at given 'index'.""" + return Digit(self._pin_bank[index]) + + def bank(self, index): + """Returns a PinBank object for the display at given 'index'.""" + return self._pin_bank[index] + + def __getitem__(self, n): + """Allows the SAA1064 to iterate through it's pin banks.""" + if 0 < n < len(self): + raise ValueError('no pin bank index {n} out of range', n=n) + return self._pin_bank[n] + + def __len__(self): + return len(self._pin_bank) + +class Digit(object): + def __init__(self, pin_bank): + self._pin_bank = pin_bank + + value = property(lambda p: p.get(), + lambda p,v: p.set(v), + doc="""The value represented by the digit""") + + def get(self): + return self._pin_bank.value + + def set(self, new_value): + digit = str(new_value) + try: + byte_value = digit_map[digit[:1]] + if digit.find(".") > -1: + byte_value |= DECIMAL_POINT + self._pin_bank.value=byte_value + except: + raise ValueError('cannot display digit ' + new_value) + + +class _PinBank(PinBankAPI): + + def __init__(self, index): + super(_PinBank,self).__init__() + self._value = 0 + self._segment_output = tuple(_OutputPin(self, i) for i in range(8)) + self.segment_address = index+1 + + def segment_output(self, index): + return self._segment_output[index] + + pin = segment_output + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._value = value + + @property + def i2c_message(self): + return writing_bytes(displayController, self.segment_address, self._value) + + def __getitem__(self, n): + if 0 < n < len(self): + raise ValueError('no segment output index {n} out of range', n=n) + return self._segment_output[n] + + def __len__(self): + return len(self._segment_output) + +class _OutputPin(PinAPI): + def __init__(self, pin_bank, index): + super(_OutputPin,self).__init__(pin_bank, index) + self._binary = 1 << index + + def open(self): + pass + + def close(self): + pass + + def get(self): + """The current value of the pin: 1 if the pin is high or 0 if the pin is low.""" + return (self._bank.value & (1 << self._index)) >> self._index + + def set(self, new_value): + self._bank.value = self._bank.value | (self._binary * new_value) diff --git a/quick2wire/parts/seven_segment_display.py b/quick2wire/parts/seven_segment_display.py new file mode 100644 index 0000000..2fb8ebf --- /dev/null +++ b/quick2wire/parts/seven_segment_display.py @@ -0,0 +1,17 @@ +import re + +__author__ = 'stuartervine' + +class SevenSegmentDisplay(object): + def __init__(self, driver_chip): + self._driver_chip = driver_chip + + def display(self, value): + self._driver_chip.reset() + + digit_values = re.findall(".\.?", str(value)) + for i, digit_value in zip(reversed(range(len(self._driver_chip))), reversed(digit_values)): + self._driver_chip.digit(i).value=digit_value + + self._driver_chip.write() + diff --git a/quick2wire/parts/test_pcf8591.py b/quick2wire/parts/test_pcf8591.py index cd2bee8..3cee12f 100644 --- a/quick2wire/parts/test_pcf8591.py +++ b/quick2wire/parts/test_pcf8591.py @@ -1,50 +1,10 @@ from quick2wire.i2c_ctypes import I2C_M_RD from quick2wire.gpio import In +from quick2wire.parts.fake_i2c import FakeI2CMaster from quick2wire.parts.pcf8591 import PCF8591, FOUR_SINGLE_ENDED, THREE_DIFFERENTIAL, SINGLE_ENDED_AND_DIFFERENTIAL, TWO_DIFFERENTIAL import pytest - -class FakeI2CMaster: - def __init__(self): - self._requests = [] - self._responses = [] - self._next_response = 0 - self.message_precondition = lambda m: True - - def all_messages_must(self, p): - self.message_precondition - - def clear(self): - self.__init__() - - def transaction(self, *messages): - for m in messages: - self.message_precondition(m) - - self._requests.append(messages) - - read_count = sum(bool(m.flags & I2C_M_RD) for m in messages) - if read_count == 0: - return [] - elif self._next_response < len(self._responses): - response = self._responses[self._next_response] - self._next_response += 1 - return response - else: - return [(0x00,)]*read_count - - def add_response(self, *messages): - self._responses.append(messages) - - @property - def request_count(self): - return len(self._requests) - - def request(self, n): - return self._requests[n] - - i2c = FakeI2CMaster() def is_read(m): @@ -56,7 +16,6 @@ def is_write(m): def assert_is_approx(expected, value, delta=0.005): assert abs(value - expected) <= delta - def correct_message_for(adc): def check(m): assert m.addr == adc.address @@ -70,8 +29,6 @@ def check(m): return check - - def setup_function(f): i2c.clear() @@ -84,7 +41,6 @@ def assert_all_input_pins_report_direction(adc): assert all(adc.single_ended_input(p).direction == In for p in range(adc.single_ended_input_count)) assert all(adc.differential_input(p).direction == In for p in range(adc.differential_input_count)) - def test_can_be_created_with_four_single_ended_inputs(): adc = PCF8591(i2c, FOUR_SINGLE_ENDED) assert adc.single_ended_input_count == 4 diff --git a/quick2wire/parts/test_saa1064.py b/quick2wire/parts/test_saa1064.py new file mode 100644 index 0000000..2aadb52 --- /dev/null +++ b/quick2wire/parts/test_saa1064.py @@ -0,0 +1,237 @@ +import pytest +from quick2wire.i2c_ctypes import I2C_M_RD +from quick2wire.parts.fake_i2c import FakeI2CMaster +from quick2wire.parts.saa1064 import SAA1064, STATIC_MODE, DYNAMIC_MODE + +__author__ = 'stuartervine' + +i2c = FakeI2CMaster() + +def setup_function(f): + i2c.clear() + +def test_cannot_be_created_with_invalid_number_of_digits(): + with pytest.raises(ValueError): + SAA1064(i2c, digits=5) + +def test_static_non_blanked_brightest_display_single_digit_by_default(): + saa1064 = SAA1064(i2c) + assert len(saa1064._pin_bank) == 1 + + saa1064.configure() + assert i2c.request_count == 1 + + controlMessage = i2c.request_at(0).message(0) + assert controlMessage.len == 2 + assert controlMessage.byte(0) == 0b00000000 + assert controlMessage.byte(1) == 0b11100110 + +def test_configuring_dynamic_display(): + saa1064 = SAA1064(i2c) + saa1064.mode=DYNAMIC_MODE + saa1064.configure() + + assert i2c.request_count == 1 + + controlMessage = i2c.request_at(0).message(0) + assert controlMessage.len == 2 + assert controlMessage.byte(0) == 0b00000000 + assert controlMessage.byte(1) == 0b11100111 + +def test_configuring_display_brightness(): + saa1064 = SAA1064(i2c) + saa1064.mode=DYNAMIC_MODE + saa1064.brightness=3 + saa1064.configure() + + assert i2c.request_count == 1 + + controlMessage = i2c.request_at(0).message(0) + assert controlMessage.len == 2 + assert controlMessage.byte(0) == 0b00000000 + assert controlMessage.byte(1) == 0b01100111 + +def test_cannot_be_configured_with_invalid_mode(): + saa1064 = SAA1064(i2c) + with pytest.raises(ValueError): + saa1064.mode=99 + +def test_cannot_be_configured_with_invalid_brightness(): + saa1064 = SAA1064(i2c) + for x in range(8): + saa1064.brightness = x + + with pytest.raises(ValueError): + saa1064.brightness=8 + +def test_first_write_sends_configuration_to_chip(): + saa1064 = SAA1064(i2c, digits=1) + saa1064.write() + + assert i2c.request_count == 2 + dataMessage = i2c.message(0) + assert dataMessage.len == 2 + assert dataMessage.byte(0) == 0b00000000 + assert dataMessage.byte(1) == 0b11100110 + +def test_second_write_only_writes_data_to_chip(): + saa1064 = SAA1064(i2c, digits=1) + saa1064.write() + saa1064.write() + + assert i2c.request_count == 3 + dataMessage = i2c.request_at(2).message(0) + assert dataMessage.len == 2 + assert dataMessage.byte(0) == 0b00000001 + assert dataMessage.byte(1) == 0b00000000 + +def test_writing_single_digit_segment_outputs_to_i2c(): + saa1064 = SAA1064(i2c, digits=1) + saa1064.configured = True + + saa1064.bank(0).segment_output(0).value=1 + saa1064.bank(0).segment_output(1).value=0 + saa1064.bank(0).segment_output(2).value=0 + saa1064.bank(0).segment_output(3).value=1 + saa1064.bank(0).segment_output(4).value=1 + saa1064.bank(0).segment_output(5).value=1 + saa1064.bank(0).segment_output(6).value=0 + saa1064.bank(0).segment_output(7).value=1 + saa1064.write() + + assert i2c.request_count == 1 + dataMessage = i2c.request_at(0).message(0) + assert dataMessage.len == 2 + assert dataMessage.byte(0) == 0b00000001 + assert dataMessage.byte(1) == 0b10111001 + +def test_individual_pin_values_can_be_read(): + saa1064 = SAA1064(i2c, digits=1) + + saa1064.bank(0).segment_output(0).value=1 + saa1064.bank(0).segment_output(1).value=0 + saa1064.bank(0).segment_output(2).value=0 + saa1064.bank(0).segment_output(3).value=1 + + assert saa1064.bank(0).segment_output(0).value == 1 + assert saa1064.bank(0).segment_output(1).value == 0 + assert saa1064.bank(0).segment_output(2).value == 0 + assert saa1064.bank(0).segment_output(3).value == 1 + +def test_writing_two_digit_segment_outputs_to_i2c(): + saa1064 = SAA1064(i2c, digits=2) + saa1064.configured = True + + saa1064.bank(0).segment_output(0).value=1 + saa1064.bank(0).segment_output(1).value=0 + saa1064.bank(0).segment_output(2).value=1 + saa1064.bank(0).segment_output(3).value=0 + saa1064.bank(0).segment_output(4).value=0 + saa1064.bank(0).segment_output(5).value=0 + saa1064.bank(0).segment_output(6).value=0 + saa1064.bank(0).segment_output(7).value=0 + + saa1064.bank(1).segment_output(0).value=0 + saa1064.bank(1).segment_output(1).value=0 + saa1064.bank(1).segment_output(2).value=0 + saa1064.bank(1).segment_output(3).value=0 + saa1064.bank(1).segment_output(4).value=1 + saa1064.bank(1).segment_output(5).value=0 + saa1064.bank(1).segment_output(6).value=1 + saa1064.bank(1).segment_output(7).value=0 + saa1064.write() + + assert i2c.request_count == 1 + + message1 = i2c.request_at(0).message(0) + assert message1.len == 2 + assert message1.byte(0) == 0b00000001 + assert message1.byte(1) == 0b00000101 + + message2 = i2c.request_at(0).message(1) + assert message2.len == 2 + assert message2.byte(0) == 0b00000010 + assert message2.byte(1) == 0b01010000 + +def test_writing_four_digit_segment_outputs_to_i2c(): + saa1064 = SAA1064(i2c, digits=4) + saa1064.configured = True + + saa1064.bank(0).value=255 + saa1064.bank(1).value=127 + saa1064.bank(2).value=63 + saa1064.bank(3).value=31 + saa1064.write() + + assert i2c.request_count == 1 + + message1 = i2c.request_at(0).message(0) + assert message1.len == 2 + assert message1.byte(0) == 1 + assert message1.byte(1) == 255 + + message2 = i2c.message(1) + assert message2.len == 2 + assert message2.byte(0) == 2 + assert message2.byte(1) == 127 + + message3 = i2c.message(2) + assert message3.len == 2 + assert message3.byte(0) == 3 + assert message3.byte(1) == 63 + + message4 = i2c.message(3) + assert message4.len == 2 + assert message4.byte(0) == 4 + assert message4.byte(1) == 31 + +def test_digits_are_mapped_into_correct_i2c_message(): + saa1064 = SAA1064(i2c, digits=2) + saa1064.configured = True + + saa1064.digit(0).value='9' + saa1064.digit(1).value='5' + saa1064.write() + + assert i2c.request_count == 1 + message1 = i2c.message(0) + assert message1.len == 2 + assert message1.byte(0) == 1 + assert message1.byte(1) == 123 + + message2 = i2c.message(1) + assert message2.len == 2 + assert message2.byte(0) == 2 + assert message2.byte(1) == 91 + +def test_digits_can_be_set_using_integer_value(): + saa1064 = SAA1064(i2c, digits=2) + saa1064.configured = True + + saa1064.digit(0).value=9 + saa1064.write() + + assert i2c.request_count == 1 + message1 = i2c.message(0) + assert message1.len == 2 + assert message1.byte(0) == 1 + assert message1.byte(1) == 123 + +def test_digit_with_decimal_point_sets_bit_for_decimal_point(): + saa1064 = SAA1064(i2c, digits=2) + saa1064.configured = True + + saa1064.digit(0).value='9.' + saa1064.write() + + assert i2c.request_count == 1 + message1 = i2c.message(0) + assert message1.len == 2 + assert message1.byte(0) == 1 + assert message1.byte(1) == 251 + +def test_does_not_accept_invalid_digit_values(): + saa1064 = SAA1064(i2c, digits=2) + + with pytest.raises(ValueError): + saa1064.digit(0).value='@' diff --git a/quick2wire/parts/test_seven_segment_display.py b/quick2wire/parts/test_seven_segment_display.py new file mode 100644 index 0000000..3d95f63 --- /dev/null +++ b/quick2wire/parts/test_seven_segment_display.py @@ -0,0 +1,57 @@ +from quick2wire.parts.saa1064 import DECIMAL_POINT +from quick2wire.parts.seven_segment_display import SevenSegmentDisplay + +__author__ = 'stuartervine' + +class FakeDigit(object): + def __init__(self): + self.value = 0 + +class FakeSAA1064(object): + def __init__(self): + self._fake_digits = [FakeDigit() for x in range(4)] + + def reset(self): + for digit in self._fake_digits: + digit.value = 0 + + def digit(self, index): + return self._fake_digits[index] + + def write(self): + pass + + def __len__(self): + return len(self._fake_digits) + +saa1064 = FakeSAA1064() + +def test_cannot_be_created_with_invalid_number_of_digits(): + display = SevenSegmentDisplay(saa1064) + display.display('1234') + assert saa1064.digit(0).value == '1' + assert saa1064.digit(1).value == '2' + assert saa1064.digit(2).value == '3' + assert saa1064.digit(3).value == '4' + +def test_shows_decimal_point(): + display = SevenSegmentDisplay(saa1064) + display.display('0.123') + assert saa1064.digit(0).value == '0.' + assert saa1064.digit(1).value == '1' + assert saa1064.digit(2).value == '2' + assert saa1064.digit(3).value == '3' + +def test_hands_through_potentially_undisplayable_values(): + display = SevenSegmentDisplay(saa1064) + display.display('@') + assert saa1064.digit(3).value == '@' + +def test_pads_unused_digits_when_not_all_are_used(): + display = SevenSegmentDisplay(saa1064) + display.display('12') + assert saa1064.digit(0).value == 0 + assert saa1064.digit(1).value == 0 + assert saa1064.digit(2).value == '1' + assert saa1064.digit(3).value == '2' + diff --git a/quick2wire/pin.py b/quick2wire/pin.py new file mode 100644 index 0000000..0c426e7 --- /dev/null +++ b/quick2wire/pin.py @@ -0,0 +1,36 @@ +class PinAPI(object): + def __init__(self, bank, index): + self._bank = bank + self._index = index + + @property + def index(self): + return self._index + + @property + def bank(self): + return self._bank + + def __enter__(self): + self.open() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + value = property(lambda p: p.get(), + lambda p,v: p.set(v), + doc="""The value of the pin: 1 if the pin is high, 0 if the pin is low.""") + + +class PinBankAPI(object): + def __getitem__(self, n): + if 0 < n < len(self): + raise ValueError("no pin index {n} out of range", n=n) + return self.pin(n) + + def write(self): + pass + + def read(self): + pass \ No newline at end of file