Skip to content

Conversation

@daniel-dbg-ginsburg
Copy link

  1. Embedded-hal updated to 1.0.0
  2. Async implementation based on embedded-async-hal

Daniel Ginsburg added 6 commits April 10, 2024 15:14
Copy link
Owner

@rubberduck203 rubberduck203 left a 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()
Copy link
Owner

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.

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?

Copy link
Owner

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.

Copy link
Owner

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.

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.

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();
Copy link
Owner

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.

Copy link

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.

Copy link

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.

Copy link

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).

Copy link
Owner

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.

Comment on lines +39 to +46
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
}
}
}
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link
Owner

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.

Copy link

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();
Copy link

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
Copy link

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()
Copy link

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.

Comment on lines +39 to +46
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
}
}
}
Copy link

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.

@chrysn
Copy link

chrysn commented Apr 3, 2025

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).

@ckrenslehner
Copy link

@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

@rubberduck203
Copy link
Owner

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).

I would be willing to work with folks to hand offer the crate on crates.io should something like that happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants