Implement iterator for struct generic in no_std

Hello all,

Thanks in advance for the help. I'm coming to rust as an embedded C developer. I'm trying to create an iterator to loop over the members of this struct. I've found a few solutions struct_iterable, or using 'any', but being in a no_std environment my understanding is these aren't available to me (Is my assumption correct?)

Here's a code snippet:

struct DataChannelSendState {
    air_temp : DataChannel<AIR_TEMP_TAG, packers::TempCelsius>,
    liquid_temp : DataChannel<LIQUID_TEMP_TAG, packers::TempCelsius>,
    battery_voltage : DataChannel<BATTERY_VOLTAGE_TAG, packers::BatteryVoltage>,
    fermt_level : DataChannel<FERMT_LEVEL_TAG, packers::FermtLevel>,
    liquid_level : DataChannel<LIQUID_LEVEL_TAG, packers::LiquidLevel>,
}

// impl<const TAG: u8, T: simple_tlv::SimpleValue> Iterator for DataChannelSendState {
impl Iterator for DataChannelSendState {
    type Item = SimpleTlv<{ TAG }, T>; // What goes here?
    fn next(&self) -> Option<Self::Item> {
        if self.air_temp.enabled && self.air_temp.data.is_some() {
            return Some(self.air_temp.data.unwrap());
        } else if self.liquid_temp.enabled && self.liquid_temp.data.is_some() {
            return Some(self.liquid_temp.data.unwrap());
        } else if self.battery_voltage.enabled && self.battery_voltage.data.is_some() {
            return Some(self.battery_voltage.data.unwrap());
        } else if self.fermt_level.enabled && self.fermt_level.data.is_some() {
            return Some(self.fermt_level.data.unwrap());
        } else if self.liquid_level.enabled && self.liquid_level.data.is_some() {
            return Some(self.liquid_level.data.unwrap());
        } else {
            return None;
        }
    }
}

The generics for the DataChannel types are "<const TAG: u8, T: SimpleValue>" where SimpleValue is a trait I've defined. The generics are confusing me as what the "type Item" should be. Intuitively I would think that because each of the members supports the same functions (defined by the 'SimpleValue' trait) that this is workable, but this level of abstraction is new territory for me :slight_smile:.

I've been reading up on this problem but getting a little lost. Any nudge in the right direction is appreciated. Thanks again!

Each TAG value and T type makes your SimpleTlv<TAG, T> a separate and distinct type. You would have to make a type that can encode that distinction as a runtime value. The most straight forward in your case may be an enum with a variant for each outcome.

enum AnySimpleTlv {
    AirTemp(SimpleTlv<AIR_TEMP_TAG, packers::TempCelsius>),
    // ...
}

If the value type is the same at its core, you can also consider a struct. You can have converters to and from the static types.

struct AnySimpleTlv {
    tag: u8,
    value: f32, // example
}

A third alternative would be a trait object. no_std complicates this, but there are crates for "inline" or "small" boxes that don't need a heap allocation. They have a limited capacity instead, but your use case should probably be fine with that.

If you do end up going with the dynamic dispatch solution, you could consider using a lending iterator: make a new struct like DataChannelSendStateIter that borrows the original struct and keeps track of where it is in the iteration, and return &dyn Any (or whatever trait you decide to use).

1 Like

that's not how iterators are typically used. it seems like an XY-problem to me. what problem are you actually trying to solve?

3 Likes

I agree with @nerditation that this is probably an XY-problem. In spite of that here would be a possible solution to what you tried to achieve.

Thanks for you're help and insight. Yes here is more background.

The problem I'm trying to solve is having a variety of sensor types and channels whose data can arrive in an undetermined order. Some are more interrupt based while others are read explicitly, and take various amounts of time to retrieve. I would like to know when all (of those that are enabled) have arrived, and then pack their values into a buffer to be transmitted. It reduces overhead to send the values as a single packet.

Having this be a struct (as opposed to a vec) allows the ability to access each field directly. The reason I'd like to loop over the struct is for the following behavior:

  • Loop over each member and check that of those enabled, they have received their reading
  • Loop over and pack the data

There were a few holes in my knowledge that this post has helped clear up for me (generics creating a unique type, and how to achieve runtime polymorphism with "dyn"). I'm also planning to post some code soon when I can get time to.

All that said if there's a better way to implement/architect the behavior I'm looking for, I'd love to hear it for the sake of learning.

Given those requirements, here is one way to do it (playground):

struct DataChannel<const TAG: u8, T> {
    value: T,
    // ...
}
trait ErasedDataChannel {
    fn tag(&self) -> u8;
    fn value(&self) -> &dyn SimpleValue;
}
impl<const TAG: u8, T: SimpleValue> ErasedDataChannel for DataChannel<TAG, T> {
    fn tag(&self) -> u8 { TAG }
    fn value(&self) -> &dyn SimpleValue { &self.value }
}

struct DataChannelSendState {
    air_temp : DataChannel<AIR_TEMP_TAG, packers::TempCelsius>,
    liquid_temp : DataChannel<LIQUID_TEMP_TAG, packers::TempCelsius>,
    battery_voltage : DataChannel<BATTERY_VOLTAGE_TAG, packers::BatteryVoltage>,
    fermt_level : DataChannel<FERMT_LEVEL_TAG, packers::FermtLevel>,
    liquid_level : DataChannel<LIQUID_LEVEL_TAG, packers::LiquidLevel>,
}

impl DataChannelSendState {
    fn fields(&mut self) -> [&mut dyn ErasedDataChannel; 5] {
        [
            &mut self.air_temp,
            &mut self.liquid_temp,
            &mut self.battery_voltage,
            &mut self.fermt_level,
            &mut self.liquid_level,
        ]        
    }
}

This way, you can iterate over fields() to do your tasks. You’ll need to add methods to ErasedDataChannel for whatever operations you need to do in those loops.

4 Likes