Seven segment display #488

Open
wants to merge 31 commits into
from

Projects

In progress in GPIO Zero

5 participants

@martinohanlon

Initial PR for seven segment display

Related to issue #485

martinohanlon added some commits Oct 18, 2016
@martinohanlon martinohanlon seven seg display
42fc23f
@martinohanlon martinohanlon updated to LEDBoard
c1ec806
@codecov-io
codecov-io commented Oct 18, 2016 edited

Current coverage is 79.50% (diff: 98.31%)

Merging #488 into master will increase coverage by 1.24%

@@             master       #488   diff @@
==========================================
  Files            31         31          
  Lines          7213       7689   +476   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           5645       6113   +468   
- Misses         1568       1576     +8   
  Partials          0          0          

Powered by Codecov. Last update cbdd9b6...3e833d4

gpiozero/boards.py
+ for led in range(7):
+ self[led].value = layout[led]
+ else:
+ raise ValueError('There is no layout for character %s')
@lurch
lurch Oct 18, 2016 Contributor

%s doesn't get substituted (whoops)

gpiozero/boards.py
+ 'L': (False, False, False, True, True, True, False),
+ 'M': (True, False, True, False, True, False, False),
+ 'N': (True, True, True, False, True, True, False),
+ 'O': (True, True, True, True, True, True, False),
@lurch
lurch Oct 18, 2016 Contributor

This has the same representation as '0' - not sure if that's a good idea, but I'm open to arguments either way...

@SrMouraSilva
SrMouraSilva Oct 18, 2016

I prefer "O" tiny, but some suggestions found on google images put equal to zero

http://chrispurdie.com/wp-content/uploads/2012/12/7seg1_WEB.jpg

@lurch
lurch Oct 18, 2016 Contributor

See, that's what I mean about different people wanting to map different characters to different 7-seg combinations... ;-)

@martinohanlon
martinohanlon Oct 19, 2016

Everyone's a critic...

I'll add a method to set_char_layout(char, layout)

gpiozero/boards.py
+ def __init__(self, *pins, **kwargs):
+ # 7 segment displays must have 7 or 8 pins
+ if len(pins) < 7 or len(pins) > 8:
+ raise ValueError('7 segment display must have 7 or 8 pins')
@lurch
lurch Oct 18, 2016 Contributor

Perhaps change 7 segment display to SevenSegmentDisplay ?

@lurch
Contributor
lurch commented Oct 18, 2016

Ping @SrMouraSilva in case he has any comments.

gpiozero/boards.py
+ assert not isinstance(pin, LEDCollection)
+ pwm = kwargs.pop('pwm', False)
+ active_high = kwargs.pop('active_high', True)
+ if kwargs:
@lurch
lurch Oct 18, 2016 Contributor

In common with LEDBoard, should we also have a boolean initial_value property?

gpiozero/boards.py
+ if len(self) > 7:
+ return self[7].value
+ else:
+ raise OutputDeviceError('There is no 8th pin for the decimal point')
@lurch
lurch Oct 18, 2016 Contributor

I can't decide whether trying to get the decimal point on a 7-pin SevenSegment should just always return False, rather than raising an exception?

(OTOH, trying to set the dp on a 7-pin SevenSegment probably should still raise an exception?)

gpiozero/boards.py
+ 'S': (True, False, True, True, False, True, True),
+ 'T': (False, False, False, True, True, True, True),
+ 'U': (False, True, True, True, True, True, False),
+ 'V': (False, True, True, True, True, True, False),
@lurch
lurch Oct 18, 2016 Contributor

'U' and 'V' are also indistinguishable.

@martinohanlon
martinohanlon Oct 19, 2016

yep - made u a 'little u' 'U': (False, False, True, True, True, False, False),

@SrMouraSilva

I add my opinion based in #357 issue

@@ -1185,3 +1185,86 @@ def on(self):
def off(self):
self.value = False
+class SevenSegmentDisplay(LEDBoard):
@SrMouraSilva
SrMouraSilva Oct 18, 2016 edited

Ignore it
if I want to know if it is on or off, how do?

I suggest a status attribute for it

@SrMouraSilva
SrMouraSilva Oct 18, 2016 edited

A custom __repr__ is a good idea

@lurch
lurch Oct 18, 2016 Contributor

if I want to know if it is on or off, how do?

is_active will tell you if any LEDs are lit https://github.com/RPi-Distro/python-gpiozero/blob/master/gpiozero/devices.py#L357
(unfortunately that's not in the docs yet)

A custom __repr__ is a good idea

What do you suggest? Is the __repr__ provided by CompositeDevice not good enough?

@SrMouraSilva
SrMouraSilva Oct 19, 2016 edited

What do you suggest? Is the __repr__ provided by CompositeDevice not good enough?

I was thinking also display the character represented

@lurch
lurch Oct 19, 2016 Contributor

I was thinking also display the character represented

If that was to be added, wouldn't that be a better fit for __str__ rather than __repr__ ? shrug

gpiozero/boards.py
@@ -1185,3 +1185,86 @@ def on(self):
def off(self):
self.value = False
+class SevenSegmentDisplay(LEDBoard):
+ layouts = {
@SrMouraSilva
SrMouraSilva Oct 18, 2016 edited

If the layout contemplate that both possibilities, I am in favor of a way to get (@property only) the string representation of what is being displayed: char + dp

@lurch
lurch Oct 18, 2016 Contributor

...and what should it do if the 7-seg is displaying a combination that doesn't map to a character?

@SrMouraSilva
SrMouraSilva Oct 19, 2016

So that such a static method that receives a display and displays the character?
Or only raises a Error like if the user call display(notMapedChar).

@lurch
lurch Oct 19, 2016 Contributor

Being (ultimately) based on CompositeOutputDevice, the user is free to set the .value property (tuple of bools) to whatever combination they see fit - we don't restrict what combinations they can use. And as mentioned before, I'm not sure that having a property-getter that throws Exceptions is a good idea :-/

gpiozero/boards.py
+
+ def __init__(self, *pins, **kwargs):
+ # 7 segment displays must have 7 or 8 pins
+ if len(pins) < 7 or len(pins) > 8:
@SrMouraSilva
SrMouraSilva Oct 18, 2016

if 6 < len(a) < 8:

@lurch
lurch Oct 18, 2016 Contributor

I suspect you might have meant if not 6 < len(pins) < 9: ? IMHO that's less readable though.

@SrMouraSilva
SrMouraSilva Oct 19, 2016

It's true. I'm sorry

@martinohanlon
martinohanlon Oct 19, 2016

I think its more readable as is.

gpiozero/boards.py
+ if len(char) > 1:
+ raise ValueError('Only a single character can be displayed')
+ if char in SevenSegmentDisplay.layouts:
+ layout = SevenSegmentDisplay.layouts[char]
@SrMouraSilva
SrMouraSilva Oct 18, 2016

I prefer that the denial be up to reduce the complexity idiomatic

if char not in SevenSegmentDisplay.layouts:
    raise ValueError('There is no layout for character %s')

layout = SevenSegmentDisplay.layouts[char]
for led in range(7):
    self[led].value = layout[led]
@martinohanlon martinohanlon updates to 7segment
6b2625e
@martinohanlon

Updated PR based on comments above.

+ initial_value = kwargs.pop('initial_value', False)
+ if kwargs:
+ raise TypeError('unexpected keyword argument: %s' % kwargs.popitem()[0])
+
@lurch
lurch Oct 19, 2016 Contributor

Might be worth adding a code-comment in here that the LED segments in the _layouts array are listed in the order (a, b, c, d, e, f, g) ?

gpiozero/boards.py
+ else:
+ raise OutputDeviceError('there is no 8th pin for the decimal point')
+
+ def set_char_layout(self, char, layout):
@lurch
lurch Oct 19, 2016 Contributor

I guess a docstring explaining the char and layout parameters would be really useful? :)

@martinohanlon
martinohanlon Oct 19, 2016

I've added docstrings to the class now as we seem to be reaching a concensus.

gpiozero/boards.py
+ raise OutputDeviceError('there is no 8th pin for the decimal point')
+
+ def set_char_layout(self, char, layout):
+ char = str(char)
@lurch
lurch Oct 19, 2016 Contributor

Hmm, this should probably be char = str(char).upper() to match the display function?

@martinohanlon
martinohanlon Oct 19, 2016

I made a conscious decision to not upper it, but for the life of me I cant think of my reasoning... I'll upper it!

@lurch
Contributor
lurch commented Oct 19, 2016

Yep, looking nice :-)
Could you undo the file-mode changes though? (I think this sometimes happens by accident when you use git on Windows)

Would it be worth adding an allow_override=False parameter to set_char_layout, such that existing characters in _layouts can only be replaced when allow_override is True?

What does @bennuttall think of my suggestion of the decimal_point property-getter always returning False for 7-segments that don't have a decimal point LED?

gpiozero/boards.py
+ raise OutputDeviceError('there is no 8th pin for the decimal point')
+
+ def set_char_layout(self, char, layout):
@SrMouraSilva
SrMouraSilva Oct 19, 2016
  1. A setter method is pythonic?
  2. For me the set_char_layout gives the impression that I can only change an existing layout.
  3. If the layout is attribute (self._layout) not a static method, if I change a char layout of a 7seg display, then is desirable that another 7seg haven't this layout?
@martinohanlon
martinohanlon Oct 19, 2016

I purposely made `._layouts' private to the instance rather than static as I felt it might be confusing if a change was reflected across multi instances.

@lurch
lurch Oct 19, 2016 Contributor

I agree that modifiable attributes should be instance attributes rather than class attributes.

@martinohanlon martinohanlon 7 segment updates
a5f7b7e
@martinohanlon

Updated PR based on comments & added docs

+ from gpiozero import SevenSegmentDisplay
+
+ seven = SevenSegmentDisplay(1,2,3,4,5,6,7,8,active_high=False)
+ seven.display("7")
@lurch
lurch Oct 19, 2016 Contributor

'seven display seven' sounds a bit tautological ;-)
Perhaps seven.display("5")

And GPIO1 is one of the 'reserved' pins, so maybe pins 2-9 would be better? (only a minor point)

gpiozero/boards.py
@@ -1267,13 +1320,26 @@ def decimal_point(self):
@decimal_point.setter
def decimal_point(self, value):
+ """
+ Sets the status of the decimal point led
+ """
@lurch
lurch Oct 19, 2016 Contributor

I don't think property-setters need their own docstring?

@martinohanlon
martinohanlon Oct 19, 2016

There were examples that did so I thought id put them in anyway.

@lurch
Contributor
lurch commented Oct 19, 2016

Great docs, thanks :-)

@SrMouraSilva

Added some commentaries about "commentaries"

gpiozero/boards.py
+ """
+ def __init__(self, *pins, **kwargs):
+ # 7 segment displays must have 7 or 8 pins
+ if len(pins) < 7 or len(pins) > 8:
@SrMouraSilva
SrMouraSilva Oct 20, 2016

The commentary is redundant (see your documentation and the if len(pins) < 7 or len(pins) > 8:)

gpiozero/boards.py
+ if len(pins) < 7 or len(pins) > 8:
+ raise ValueError('SevenSegmentDisplay must have 7 or 8 pins')
+ # Don't allow 7 segments to contain collections
+ for pin in pins:
@SrMouraSilva
SrMouraSilva Oct 20, 2016

For me, the commentary is redundant

gpiozero/boards.py
+ Represents the status of the decimal point led
+ """
+ # does the 7seg display have a decimal point (i.e pin 8)
+ if len(self) > 7:
@SrMouraSilva
SrMouraSilva Oct 20, 2016

This commentary is necessary?

@SrMouraSilva

It is being planned you perform some test?

martinohanlon added some commits Oct 22, 2016
@martinohanlon martinohanlon seven segment test eb16d5a
@martinohanlon martinohanlon seven segment test f9d19f3
@martinohanlon martinohanlon seven segment test
8c98939
@martinohanlon martinohanlon seven segment test 5fe3383
@martinohanlon martinohanlon seven segment test
8d983e3
@martinohanlon martinohanlon seven segment test 80b9e3f
@martinohanlon martinohanlon seven segment test 34f7920
@martinohanlon martinohanlon seven segment test
37ff6e5
@martinohanlon martinohanlon seven segment test
84a940c
@martinohanlon martinohanlon seven segment test 2879ab6
@martinohanlon martinohanlon seven segment test
80e2ad8
@martinohanlon martinohanlon seven segment bug
b60947d
@martinohanlon
martinohanlon commented Oct 22, 2016 edited

I've added tests.

Any advice on locally testing tests, so I dont have to commit changes to get the tests to run.

@martinohanlon martinohanlon referenced this pull request Oct 22, 2016
Closed

7 seg displays #485

tests/test_boards.py
+ pin5 = MockPin(6)
+ pin6 = MockPin(7)
+ pin7 = MockPin(8)
+ pin8 = MockPin(9)
@lurch
lurch Oct 22, 2016 Contributor

I wonder if it'd be worth renaming these to pinA, pinB, ... pinG, pinDP ?

@martinohanlon
martinohanlon Oct 22, 2016

Maybe, I can see the benefit, my motivation was keeping consistency.

+ assert seven_seg[4].active_high
+ assert seven_seg[5].active_high
+ assert seven_seg[6].active_high
+ assert seven_seg[7].active_high
@lurch
lurch Oct 22, 2016 Contributor

I'm not entirely sure if this first bunch of tests is necessary, since I think they're already covered by the CompositeDevice and LEDBoard tests? (although having too many tests is obviously better than having too few!)

@martinohanlon
martinohanlon Oct 22, 2016

My thinking exactly, it provides a level of protection against future change and it's consistent with other tests such as LEDBarGraph.

tests/test_boards.py
+ pin7 = MockPin(8)
+ pin8 = MockPin(9)
+ with SevenSegmentDisplay(pin1, pin2, pin3, pin4, pin5, pin6, pin7, pin8, initial_value=True) as seven_seg:
+ assert (seven_seg[0].value and seven_seg[1].value and seven_seg[2].value and seven_seg[3].value and seven_seg[4].value and seven_seg[5].value and seven_seg[6].value)
@lurch
lurch Oct 22, 2016 Contributor

This should probably test seven_seg[7].value too?

@lurch
Contributor
lurch commented Oct 22, 2016 edited

Great set of tests, thanks Martin! 👍 Probably worth adding a test_seven_segment_display_no_decimal test too?
And could you revert your changes to debian/rules please.
As you've seen, testing the error-cases is just as important as testing the working-cases :)

Any advice on locally testing tests, so I dont have to commit changes to get the tests to run.

You need to setup a virtualenv, activate the virtualenv, install python-gpiozero into the venv with python setup.py install, then run make develop, and then finally you can run make test to run all the unit-tests.
I like to have separate venvs for Python2 and Python3 (with make develop run in each) so that I can check that all my tests still pass on both 'old' and 'new' Python :)
@waveform80 's said that at some point he plans to add a page like http://picamera.readthedocs.io/en/latest/development.html to the GpioZero docs. (and if #450 gets finished and merged, that'll make local-testing heaps easier)

Hmm, I've just had a crazy idea - what about modifying display to something like:

   def display(self, char, brightness=1):
        """
        Display a character on the 7 segment display.

        :param string char:
            A single character to be displayed.

        :param string brightness:
            The brightness to display the character at, if the display was initialised with ``pwm=True``.
        """
        if isinstance(seven_seg[0], LED):
            if brightness != 1:
                raise ValueError('brightness must be 1 with non-PWM LEDs')
        else:
            if not 0 <= brightness <= 1:
                raise ValueError('brightness must be between 0 and 1')
        char = str(char).upper()
        if len(char) > 1:
            raise ValueError('only a single character can be displayed')
        if char not in self._layouts:
            raise ValueError('there is no layout for character - %s' % char)
        layout = self._layouts[char]
        for led in range(7):
            self[led].value = brightness if layout[led] else 0

;-)

martinohanlon added some commits Oct 23, 2016
@martinohanlon martinohanlon minor test bug 8d3fe75
@martinohanlon martinohanlon minor test bug
103d628
+ assert (seven_seg[0].value and seven_seg[1].value and seven_seg[2].value and seven_seg[3].value and seven_seg[4].value and seven_seg[5].value and seven_seg[6].value)
+
+ seven_seg.display_hex(15)
+ assert (seven_seg[0].value and not seven_seg[1].value and not seven_seg[2].value and not seven_seg[3].value and seven_seg[4].value and seven_seg[5].value and seven_seg[6].value)
@lurch
lurch Oct 23, 2016 Contributor

I thought I'd already made this comment, but I can't see it, so perhaps it went missing...
(apologies if it comes up as a duplicate)

Perhaps these assert-value lines would be better as e.g.

    assert seven_seg.value[0:6] == (True, False, False, False, True, True, True)

and / or:

    expected = (True, False, False, False, True, True, True)
    for i in range(7):
        assert seven_seg[i].value == expected[i]
@martinohanlon
martinohanlon Oct 23, 2016 edited

could you revert your changes to debian/rules please.

No idea how that change got into the commit - any advice on how reverse it? So far I've managed to just totally mess up my local repo!

@lurch
lurch Oct 23, 2016 Contributor

You might be able to do something like git checkout <older-commit-id> debian/rules ? And I think when you push that, this PR would then mark it as unchanged again.

@martinohanlon
martinohanlon Oct 23, 2016

Ta - the simplest method is often the easiest...

@lurch
Contributor
lurch commented Oct 23, 2016

BTW based on what @waveform80 is doing over in #459 , you might need to modify your tests to not use pins 2 and 3.

martinohanlon added some commits Oct 23, 2016
@martinohanlon martinohanlon sort debian rules
706235b
@martinohanlon martinohanlon refactor tests to not use pin 2 and 3
78ea8e9
@lurch
Contributor
lurch commented Oct 23, 2016

The 'bad' tests still use MockPin(2) and MockPin(3) ?

@martinohanlon martinohanlon refactor tests to not use pin 2 and 3
ed6de6c
tests/test_boards.py
@@ -836,7 +836,7 @@ def test_seven_segment_initial_value():
pin7 = MockPin(8)
pin8 = MockPin(9)
with SevenSegmentDisplay(pin1, pin2, pin3, pin4, pin5, pin6, pin7, pin8, initial_value=True) as seven_seg:
- assert (seven_seg[0].value and seven_seg[1].value and seven_seg[2].value and seven_seg[3].value and seven_seg[4].value and seven_seg[5].value and seven_seg[6].value)
+ assert (seven_seg[0].value and seven_seg[1].value and seven_seg[2].value and seven_seg[3].value and seven_seg[4].value and seven_seg[5].value and seven_seg[6].valueand seven_seg[7].value)
@SrMouraSilva
SrMouraSilva Oct 23, 2016

Is really valueand (in end of line)?

@martinohanlon

Arggghhh, unsaved changes in the editor. I really need to sort out a local dev environment. Rather than having to commit each time to test.

@martinohanlon

Ive added MultiSevenSegmentDisplay to this PR. Additional tests are needed, which I will do soon, but I wanted to get this in.

+
+ #validate the messages length
+ if (self.has_decimal_point and len(message) - message.count('.') > len(self.digits)) or (not self.has_decimal_point and len(message) > len(self.digits)):
@SrMouraSilva
SrMouraSilva Nov 7, 2016

What are the practical effects of putting a dot if the display does not have support?
If 'ignore' or 'error', I suggest changing it to:

text_size = len(message) - message.count('.')
if (text_size > len(self.digits)):
    ...

else

text_size = len(message) - message.count('.') if self.has_decimal_point else len(message)
if (text_size > len(self.digits)):
    ...
@martinohanlon
martinohanlon Nov 18, 2016

If the display doesnt support a decimal point, it full stop would be considered to be a character in the message. This would generate an error unless the user had created a character layout for "." using set_char_layout.

@martinohanlon

@bennuttall @lurch @waveform80 anyone got any thoughts on why my tests are failing for pypy... I am at a loss.

martinohanlon added some commits Nov 12, 2016
@martinohanlon martinohanlon multi seven seg tests 71cd383
@martinohanlon martinohanlon Merge pull request #4 from martinohanlon/master
Modded multi seven seg tests
0b7db0b
@martinohanlon martinohanlon Merge pull request #5 from martinohanlon/dev
Dev - modded multi seven seg tests
cbe415b
@martinohanlon

Resolved test issues with pypy... Threading - bah.

@bennuttall bennuttall added the RFC label Nov 28, 2016
@martinohanlon martinohanlon fixed 7seg display minor string bug
3e833d4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment