-
Notifications
You must be signed in to change notification settings - Fork 5
Hal update and async implementation #12
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
base: master
Are you sure you want to change the base?
Hal update and async implementation #12
Conversation
daniel-dbg-ginsburg
commented
Apr 16, 2024
- Embedded-hal updated to 1.0.0
- Async implementation based on embedded-async-hal
HAL 1.0.0 substantially changes API, which in turn changed the crate API (mut requirements). It is a breaking change.
rubberduck203
left a comment
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’s a lot of really good stuff in this PR and I appreciate it greatly. Most of my comments are nit-picky, but I really don’t like the query functions (e.g. is_active) requiring &mut self instead of just self. I would like to understand why that change was made. Was there something that drove that change?
src/input/mod.rs
Outdated
|
|
||
| fn is_active(&self) -> Result<bool, Self::Error> { | ||
| fn is_active(&mut self) -> Result<bool, Self::Error> { | ||
| self.pin.is_high() |
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.
Why mutable? Asking the pin for its state should not mutate it.
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.
Thank you for the review. The reason for this change (and other similar changes) is the change in the embedded-hal. In 1.0.0 the trait InputPin defines both is_high/is_low methods as taking &mut self. So I have propagated it to the switch-hal level.
There's is a solution for that, of course: interior mutability, and I can rework my patch using that approach. I just didn't want to overcomplicate it. So tell me your preference: would you like me to do it?
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.
Ahhhh and now I wish I hadn’t left the working group prior to that crate going 1.0. Give me a moment to go figure out why they made that decision so I can see if it affects the one we make here. Hold tight please. I appreciate your patience with me.
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.
Found the PR where the hal was changed.
Long story short, it should never have happened and a strong personality discarded some very wise advice to not make it mutable. I don’t wish to propagate that mistake if it can be avoided. If you don’t mind taking a stab at preserving our existing &self API, I would appreciate it.
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.
Let me look into 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.
Please review the recent changes
| /// # let pin = mock::Pin::with_state(mock::State::High); | ||
| /// # let mut status_led = mock::Pin::new().into_active_high_switch(); | ||
| /// let button = pin.into_active_low_switch(); | ||
| /// let mut button = pin.into_active_low_switch(); |
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 example highlights why I question the mutability. It makes no sense for a button input to be mutable. For the application code, after setup is complete, it’s a read only peripheral.
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.
Unless you have a Debouncer pin wrapper (maintains state) for example. Hidden mutability is then problematic for multi threading etc. Same for port expanders (again, hidden mutability is needed for the underlying bus).
I would have been OK with both approaches, but this one is more explicitly saying that sharing is not guaranteed.
However that ship sailed 10 months ago, the API is stabilized and I am more affected by switch-hal still depending 0.2 than by the mut :) I can work around the dependency, but it is annoying.
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.
All the rest of this PR has already been changed to use interior mutability; this mut can be removed now.
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.
And sadly the way you did it will prevent me from using the crate now. RefCell requires alloc and heap that not everybody uses. It also prescribes a specific synchronization approach (and is unsound - see the other comments about the hidden panic).
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 crate should be #no-std.
I haven’t looked at it in ages, so I’m not certain the build enforces it, but it should if it’s not.
| async fn wait_for_change(&mut self) -> Result<(), Self::Error> { | ||
| if self.pin.get_mut().is_high()? { | ||
| self.pin.get_mut().wait_for_low().await | ||
| } else { | ||
| self.pin.get_mut().wait_for_high().await | ||
| } | ||
| } | ||
| } |
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.
Hi, doesn't this possibly introduce a race?
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 suppose that's alright behavior considering the semantics of the function.
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.
It does. In context switching environments (FreeRTOS based ESP32s for example) you could miss some toggles this way. But I do not see a way to make it reliable without hardware support or interrupts.
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.
Can't this just be a single get_mut across all the function?
After all, the refcell is kept locked for the duration of the awaiting period already because it is kept across await points, so wait_for_change'ing on a Switch<T, _> already panics with failure to get_mut. That wouldn't get worse by having a single mutex, but the raciness is reduced.
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.
The hidden panic is bad enough TBH.
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.
@MarSik I’m going to ask you to please watch your tone with folks.
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.
It was not my intent to be too harsh even though I do admit I tend to be direct.
The hidden panic coming from the RefCell behavior will cause hard to debug trouble in any multithreaded context. Be it interrupts, async (the reference is kept active within the await) or for real multi threading on cpus like RP2040 (dual core).
The RefCell is also not async aware so it can't await release of resources by a different thread like it could when using platform aware synchronization primitives (all rtic_sync, embassy_sync an even critical_section implementations provide those).
| /// # let pin = mock::Pin::with_state(mock::State::High); | ||
| /// # let mut status_led = mock::Pin::new().into_active_high_switch(); | ||
| /// let button = pin.into_active_low_switch(); | ||
| /// let mut button = pin.into_active_low_switch(); |
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.
And sadly the way you did it will prevent me from using the crate now. RefCell requires alloc and heap that not everybody uses. It also prescribes a specific synchronization approach (and is unsound - see the other comments about the hidden panic).
| type Error = <T as ErrorType>::Error; | ||
|
|
||
| async fn wait_for_active(&mut self) -> Result<(), Self::Error> { | ||
| self.pin.get_mut().wait_for_low().await |
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.
Here you correctly pair &mut self with RefCell:get_mut, however that totally negates the need for RefCell (= alloc and heap) in the first place.
|
|
||
| fn is_active(&self) -> Result<bool, Self::Error> { | ||
| self.pin.is_high() | ||
| self.pin.borrow_mut().is_high() |
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 can panic without the user expecting it. I told you earlier that internal mutability is complicated...
RefCell::borrow_mut - Panics if the value is currently borrowed. For a non-panicking variant, use try_borrow_mut.
| async fn wait_for_change(&mut self) -> Result<(), Self::Error> { | ||
| if self.pin.get_mut().is_high()? { | ||
| self.pin.get_mut().wait_for_low().await | ||
| } else { | ||
| self.pin.get_mut().wait_for_high().await | ||
| } | ||
| } | ||
| } |
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.
The hidden panic is bad enough TBH.
|
It seems to me that there is too much mismatch in how this crate models the world of switches to how embedded-hal models GPIOs for this to go forward. Picking up the discussion in #13 (comment) (as I read it, the maintainer not intending to keep this crate as more than a proof-of-concept, @rubberduck203 please correct me if I'm misrepresenting this), shall those who use embedded-hal team up, fork/rewrite this into a version that starts over with e-h 1.0 concepts, and once more provides a thin expression between voltage levels and semantics? I shouldn't do it alone, but if @daniel-dbg-ginsburg, @ckrenslehner and other commenters from here collaborate, this should be sustainable easily (and maybe can go into embedded-wg maintenance at some point). |
|
@chrysn I am happy to help if needed. Maybe the original PR of @daniel-dbg-ginsburg is a good starting point as it sticks to the embedded-hal design choices as far as I understand |
I would be willing to work with folks to hand offer the crate on crates.io should something like that happen. |