-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Performance optimization and code cleanup #22
Conversation
127d951
to
c5152e0
Compare
e1ba0d4
to
d2b3fc9
Compare
d2b3fc9
to
4a6b7f6
Compare
0b1665e
to
a79b974
Compare
- Remove logging, user can print() or log this themselves - Support creating a GPIOPin instance directly for (slightly) faster IO - Rewrte IO to use bytes mode, for a ~2.5x performance gain over the optimized versions - Add write to match GPIOPin's write/read - Move all pin state handling, setup and teardown to GPIOPin - Remove function aliases - Apply flake8 linting - Move logging inside "_export_lock" so Export/Unexport are logged when they happen - Support list or tuple of pins in setup() and cleanup() for vitiral#13 and vitiral#10 - Support str/int GPIO number for vitiral#7 - costs performance but using GPIOPin directly gets it back - General code tidyup - Version bump to 1.0.0 Co-authored-by: rahulraghu94 <[email protected]> Co-authored-by: sheffieldnick <[email protected]>
a79b974
to
6675711
Compare
Running belt and braces with the new Code: import gpio
pin = gpio.GPIOPin(14, gpio.OUT)
value = 1
while True:
value = 0 if value else 1
pin.write(value) And the RPi.GPIO compatibility API gets around 120KHz: Code: import gpio
gpio.setup(14, gpio.OUT)
value = 1
while True:
value = 0 if value else 1
gpio.output(14, value) Compared to this same code running on #21 - I'd say we have a little headroom for some more hand-holding if it's needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good, I especially like the research you put into it and changing the model to use objects, which should improve understandability and speed.
Left a few comments, feel free to discuss. After all, you're the maintainer now!
gpio/__init__.py
Outdated
def __init__(self, pin, mode=None, initial=LOW, active_low=None): | ||
existing = _open_pins.get(pin) | ||
if existing: | ||
existing.cleanup() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you want to do this on class __init__
. Init should just be a way to "create" the data, it should not perform logic. The only thing you might have it do is throw an error saying it is "not setup" and directing hte user to use GPIOPin.setup
You should have a @classmethod
called setup
which does any setup logic you need. I think throwing an error if the pin already exists is better behavior.
All the logic around setting up the pin should be done in the setup
classmethod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should also be a @static
called already_opened
or something that simply returns the GPIOPin
instance that is already open for a particular pin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed- I wasn't sure how best to implement the case where a (relatively bonkers) user tries to mix gpio.setup()
with pin = gpio.GPIOPin()
but something like already_opened
is a great way to work around this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I forgot to send this, but now looking back over my rpio compat setup
function your approach has some merit)
I don't necessarily agree that __init__
should not have side-effects or run logic. Where do you draw the line?
If a class involves configuring some hardware state (ie: in this case exporting a pin and opening a file descriptor) then you create an issue where you can pin = GPIOPin(n)
but all other class methods fail with an error until you have also called pin.setup()
.
Broadly, if anything in setup
must be called in order for the class to function, then this might just as well be part of __init__
or you're just creating busywork for the programmer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I backtracked a bit myself. I think that if you ensure singleton instances then doing the setup in __init__
is probably fine.
Returns: | ||
str: "in" or "out" | ||
''' | ||
with open(os.path.join(self.root, 'direction'), FMODE) as f: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed in this PR, but it might make sense to have a get_direction and read_direction. get_direction simply returns the direction that GPIOPin believes is current, whereas read_direction actually reads the direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's your impression of properties with setters instead of get
and set
methods? I tend to avoid them since I don't like the fact you can object.some_typo = value
and quietly fail to set a property. __slots__
addresses this to some extent, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure about __slots__
, I forget the complexity there.
Properties are fine. so you could do @property def direction(self)
and def read_direction(self)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for this review I would implement read/write direction. Then in a later review I would track that state internally and use properties.
gpio/__init__.py
Outdated
'''Set the direction of pin | ||
|
||
Args: | ||
mode (str): "in" or "out" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add "low" "high"
Consider using the constants instead of the strings for the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"low" / "high" are technically pin states while "in" / "out" are directions/modes. Mode should probably be "direction".
Thanks for the enormously thorough review. I've got some other code to bash my head against for a while, but I'll come back to this and implement your suggestions ASAP. |
I mean, no rush. The library has been stagnant for like 5 years... Just glad someone's working on it :D |
123924b
to
3658bc0
Compare
- Drop RPi.GPIO compatibility stubs - GPIOPin: Throw RuntimeError if pin is already configured - GPIOPin: Use FMODE for export/unexport - GPIOPin: Implicitly cast str pin to int - GPIOPin: new "configured" @staticmethod for retrieving existing pins - GPIOPin: active_low now writes "0"/"1" - GPIOPin: some docstrings + tweaks - GPIOPin: now handles removing itself from _open_pins - use "configured" in lieu of "set up"
3658bc0
to
3820ee4
Compare
- Remove duplicate code - Allow "configured" to raise a RuntimeError directly for unconfigured pins - Test coverage for cleanup + multi-pin cleanup - Improve cleanup with no arguments - Cut back cleanup to single flow, no recursion - Store pin in class as int (this was an unecessary optimisation)
- Add setup method to GPIOPin - Simplify gpio.setup, avoid recursion, use GPIOPin.setup - Improve docstrings
- Test rpio equivilents of GPIOPin functions - Test invalid pins - Test error when trying to configure pullup - Try to use constants in tests - GPIOPin "mode" is now "direction" - GPIOPin catch ValueError for implicit str to int pin conversion - GPIOPin "active_low" "direction" is now "polarity" - Note: Can't test python2.7 in python3 and vice versa
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking much better. A few minor nits but Approved.
- Support generators such as dict.keys() - Add comment to explain the use of list(dict.keys()) in cleanup - Add tests for generators - Use None instead of '' in tests, since '' results in an empty iterable and a quiet noop in cleanup/setup
I'm going to let you merge BTW. I have no other major comments. If you have concerns feel free to call them out and I'll look. |
No worries! Thank you. I’m going to sleep on it, and push a package up to test pypi tomorrow to make sure I’ve not done anything silly. |
Turns out I slept on this for more than a night! I've been wondering if it's worth adding any functions to give visibility on:
Though, granted, sysfs GPIO is deprecated so I'm not sure what the benefit of expanding the scope of this project beyond a bit of a polish would be! It could also be compelling to add libgpiod support which would be of far more ongoing utility but also a huge undertaking. A package name as succinct as "gpio" is a good opportunity to be the canonical package for ... well... gpio! |
This was an unceremonious merge, but it's probably time to move forward! |
Slight hiccup @vitiral - I cannot upload to https://test.pypi.org/project/gpio/ Permissions/users on test.pypi.org are discrete from those on real pypi. No rush to fix. I have worked around this by publishing to |
This is a work in progress that rolls various suggestions from over the years into one changeset, including a rewrite to use
setup.cfg
for all setup business (albeit this approach is obsolete).This replaces PRs #21 and #12.
This will probably drop Python 2.x support.
The main thrust of this change is tidyup and not performance, but I've achieved around a 2.5x speedup over even the optimized versions mostly by switching to binary IO for reading/writing pin state.
On a Pi 400 the following snippet - using
GPIOPin
directly to avoid extra checks and function calls - achieves a pin switch speed of roughly 160KHz:Changes