Skip to content
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

How to wrap a pluckable HCons in a struct with outher fields? #221

Open
Ben-PH opened this issue Aug 22, 2023 · 3 comments
Open

How to wrap a pluckable HCons in a struct with outher fields? #221

Ben-PH opened this issue Aug 22, 2023 · 3 comments

Comments

@Ben-PH
Copy link

Ben-PH commented Aug 22, 2023

I would like to refactor this:

/// General Purpose Input/Output driver
pub struct IO {
    _io_mux: IO_MUX,
    pub pins: Pins,
}
(A reduced version of the `Pins` struct:)
struct Pins {
    gpio0: GpioPin<Unknown, 0>,
    gpio1: GpioPin<Unknown, 1>,
    gpio2: GpioPin<Unknown, 2>,
    gpio3: GpioPin<Unknown, 3>,
}

// MODE is a series of ZST structs. `Input<PullDown>`, for example
pub struct GpioPin<MODE, const GPIONUM: u8> {
    _mode: PhantomData<MODE>,
}

into this:

/// General Purpose Input/Output driver
pub struct IO<???> {
    _io_mux: IO_MUX,
    pub pins: PinHList<???>,
}
I've built macros to create the HList type and to construct an initial pin-list:
type InitialPins = gpiopin_HList!(0, 1, 2);
// -> 
type InitialPins = HCons<GpioPin<Unknown, { 0 }>,
                   HCons<GpioPin<Unknown, { 1 }>, 
                   HCons<GpioPin<Unknown, { 2 }>,
                   HNil>>>;


let pin_list = gpiopin_hlist!(0, 1, 2);
// -> 
let pin_list = frunk::hlist!(
    GpioPin::<Unknown>, 0>::new(),
    GpioPin::<Unknown>, 1>::new(),
    GpioPin::<Unknown>, 2>::new()
);
...and go from this:
// blinky example: https://github.com/esp-rs/esp-hal/blob/422eb2118676d4c1c938fa6cb2b8211fb177c232/esp32s3-hal/examples/blinky.rs
// Borrow GPIO4, and set it as an output
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
let mut led = io.pins.gpio4.into_push_pull_output();

// initialize the gpio4 pin by setting it to high
led.set_high().unwrap();

// Initialize the Delay peripheral, and use it to toggle the LED state in a
// loop.
let mut delay = Delay::new(&clocks);

loop {
    led.toggle().unwrap();
    delay.delay_ms(500u32);
}
to something like this:
// Give pin 4 to an initialized, ready to go, blinker implementation.
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
let (blinky, io) = Blinker::<4>::init(io);

// Initialize the Delay peripheral, and use it to toggle the LED state in a
// loop.
let mut delay = Delay::new(&clocks);

blinky.blink_loop(&mut delay, 500);
unreachable!();
And `Blinker` might look something like this:
struct Blinker<const PIN: u8> {
    pin: GpioPin<Output<PushPull>, PIN>
}

impl<const PIN: u8> Blinker<PIN>
{
    fn initialize<???, ???>(io: IO<???>) -> (Self, IO<???>)
    {
        let (pin, io) = io.pins.pluck();
        let mut pin = pin.pin.into_push_pull_output();
        pin.set_high().unwrap();
        (Self{pin}, io)
    }

    fn toggle(&mut self) {
        self.pin.toggle().unwrap();
    }
    
    fn blink_loop(mut self, delay: &mut Delay, rest_period: u16) -> ! {
        loop {
            self.toggle();
            delay.delay_ms(rest_period);
        }
    }
}

Setting up the IO struct is difficult:

  • It seems I need to implement Plucker for IO?
  • is it possible to implement Plucker in such a way that a const NUM: u8 can be used, and pluck solely based on the GpioNums const?
  • I'm not sure the kind of approach I'd need: should I just re-implement HCons from the ground up, copy-pasting a lot of the code? Is there a way I'd be able to re-use the frunk implementations (e.g. by setting the pins field to be HCons<???, ???>)?
  • Are there any other projects out there that I've missed, that I could use as an example. Closest I found was usbd-human-interface-device, but that doesn't take advantage of plucking (which is a core feature I'm after)
@Ben-PH
Copy link
Author

Ben-PH commented Aug 22, 2023

...with a little bit of help from chatGPT, I ended up with this:

use frunk::hlist::Plucker;
struct Mode;
struct GpioPin<MODE, const NUM: u8> {
    _mode: PhantomData<MODE>,
}

impl<MODE, const NUM: u8> GpioPin<MODE, NUM> {
    fn new() -> Self { Self { _mode: PhantomData } }
}

pub struct IO<H> {
    _io_mux: (),
    pub pins: H,
}

impl<H> IO<H> {
    fn pluck_io<const NUM: u8, Remaining>(self) -> (GpioPin<Mode, NUM>, IO<H::Remainder>)
    where
        H: Plucker<GpioPin<Mode, NUM>, Remaining>,
    {
        let (pin, remaining_pins) = self.pins.pluck();
        (pin, IO { _io_mux: self._io_mux, pins: remaining_pins })
    }
}


#[macro_export]
macro_rules! create_pins {
    ($($gpionum:expr),+) => {
        frunk::hlist![$(GpioPin::<Mode, $gpionum>::new()),+]
    };
}
#[macro_export]
macro_rules! create_pin_hlist {
    ($($gpionum:expr),+) => {
        frunk::HList![$(NumThing<Mode, { $gpionum }>),+]
    };
}



fn main() {
    let pins = create_pins!(0, 1, 2);
    let io = IO {_io_mux: (), pins};
    let (pin, pins) = io.pluck_io::<2, _>();
    println!("Hello, world!");
}

...this compiles as I'm hoping, and when I get lsp to insert an explicit type on the let (pin, pins) line, I get:

    let (pin, io): (GpioPin<Mode, 2>, IO<frunk::HCons<GpioPin<Mode, 0>, frunk::HCons<GpioPin<Mode, 1>, frunk::HNil>>>) = io.pluck_io::<2, _>();

...which is exactly what I want!

That's in a bare-bones project though. When IO is a struct that exists in a library, and Blinker is a struct that is defined in another library, things are difficult. Will update when I have a solution, but if anyone can help in the meantime, that would be appreciated.

@lloydmeta
Copy link
Owner

lloydmeta commented Aug 23, 2023

...first of all, I must say this is amazing and I can't think of a better way to do it (I might suggest SO for finding the optimal solution).

However, what is more interesting to me is somehow you got help from ChatGPT?? It's a completely tangential topic from your question that IMO deserves a call out of its own and can enable users of this lib to "learn how to learn how to fish". Can you please give some details as to how you did that?

EDIT (submitted too quickly): Typically, for types that aren't themselves labelled with Frunk-derivation markers, indeed, things are tougher and I think some manual conversion to a custom type that you control might be required before you can do this kind magic, sadly, which also means having to write a converter back to that other type to interface with that lib. Concretely this means writing From/Tos. An alternative is to submit a PR to that lib, with some kind of optional frunk feature that activates frunk as a dep and optionally adds the frunk derivations.

@Ben-PH
Copy link
Author

Ben-PH commented Aug 23, 2023

Glad you like the solution!

this is, in fact, intended to be an upstream commit to resolve partial-move issues when managing gpio pins for esp32 microcontrollers. You can see the initial draft of the PR here: esp-rs/esp-hal#748

To use chatGPT, I started a new chat (model 4):

using the frunk crate:
this initial post here

Basically, I forwarded this post to chatGPT. Took a few iterations of feeding the errors back into the chat in order to get the where clause not completely wrong. At one point I had a bit of an "ah-ha" moment about what concepts the plucker and remainder were handling, and that gave me the mental tools to take it the rest of the way. The chat also tried going off the deep end, trying to ues GATs in the where clause, providing this suggestion:

pub struct IO<T> 
where
    T: for<const NUM: u8, Remaining> Plucker<GpioPin<Unknown, NUM>, Remaining>
{
    _io_mux: IO_MUX,
    pub pins: T
}

I also have this in my custom instructions (new chatgpt feature):

I'm experienced with rust

I like answers/conversations that assist me on my self-learning, rather than being spoon-fed answers (try using the socratic method with me).

The fish lesson, I guess, is: Start with a high-quality question (I spent quite a while working on this question. Never intended to post to chatGPT until after the post), (EDIT: fat-fingered submit) and to use it as a tool to help de-obfuscate the complexity to integrate it into your knowledge-base, rather than as a source of providing a solution.

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

No branches or pull requests

2 participants