LennartHennigs / Button2

Arduino/ESP button library that provides callback functions to track single, double, triple and long clicks. It also takes care of debouncing.
MIT License
487 stars 81 forks source link

another i2c PE button question #70

Open sercona opened 8 months ago

sercona commented 8 months ago

given that the i2c port expander is going to have to be called periodically, but you get 8 or 16 bits all at once from that.

the current paradigm is to assign a button to a single bit and the overhead of fetching that bit is small-ish. but on i2c, if you want to read 8 bits and you fetch that same byte 8 times, clearly that 'annoys the i2c bus'. (btw, you should never anthropomorphize computers; they hate that. grin)

so, what is a good way to decouple the polling of that byte (or 16 bits) from the test for button presses, that do single bit checks?

you can fetch an i2c byte and timestamp it and cache it, I guess. we'd have to do that outside of your class. there's no provision for a background poller, is there?

what I'm thinking is yet another user-provided vector that will get called by your library via your own loop() method. it would know enough to fetch the byte once.

what is a good abstraction for the user api? you could use the same 'give me pin status' query and it would either pull from the cache or it would get fresh data and reset the timestamp/age of the cache, automatically, for you.

what are your thoughts on this?

more and more, I find that buttons have to be on PE's and not on the native chip.

LennartHennigs commented 8 months ago

Hey, good question. Well, I'd rather not provide an option to call non-button code in my loop() function. Some people don't understand the non-blocking loop concept. This would allow people to slow down the loop, leading to issues that don't pertain to my class per se.

Can you check whether data has changed via I2C, or can you only set the pin mode and read /write? I'd just add a simple loop function for the port expander to get the word and bitshift to the virtual pin values, I think. Does that make sense?

sercona commented 8 months ago

I'm not sure which way to go; I can see the efficiency of reading a whole port expander byte (or word) at once and caching that for a short time. but in some apps, it may be necessary to always ask for 'fresh' data even if you have to re-fetch a whole byte/word.

I guess to allow max flex, when you do the equiv of a digitalRead(), you could add a param that forces a refetch of data, or will allow cached data. and if you leave off that last param, it would have a reasonable default (whatever that is).

dont know if I'm overthinking it or not; but i2c bus traffic has to be managed between all the things on the bus. I have an i2c oled display that I'm trying to keep updated quickly and I also want to scan for keyboard changes and not feel sluggish on the key scan. if I do an i2c request for each keypress check, it would clutter the bus. that's why I'm trying to think of the best way to deal with port expanders and fetching a whole bunch of data even if the user only wanted one bit of it.

thinking aloud, maybe something like:

digitalRead(pin, cache_allowed=true);

I'm thinking that hiding the fetch of the byte/word and the cache timing is something I'd like to hide from the app writer and just let them call the abstract digitalRead(). if they dont have a good reason to disable it, the cache would be used and some holding time would be in a ctor, somewhere.

sercona commented 8 months ago

oh, and there would be a 'digitalReadWholePort()' or something, for when the user really wanted a complete set of bits and is going to do their own AND and OR stuff from that. that would also have a cache_allowed flag, to match the single bit reads. while it might seem less useful to keep the cache if you are asking for all the bits, but you may want to throttle how often the port is polled and this cache thing could be one way to limit how fast the i2c port is being queried.

(this might belong more on the port-expander class, though.)

LennartHennigs commented 8 months ago

out of curiosity which port expander are you using?

You could use the debounce time of the buttons as the expander's interval to read data. Would pass it to either the constructor or the loop(unsigned long interval_ms) And I'd invert the digitalRead()parameter to digitalRead(pin, force_read=false) if you really want live data

sercona commented 8 months ago

I tend to use mcp23008 and pcf8574 (or 75) mostly.

LennartHennigs commented 8 months ago

Have one of the MCPs lying around and never used it so far. Might attach a button or two to it to see. But this might take a while for me to try this.

sercona commented 8 months ago

I often dont have a spare gpio to allocate to the interrupt pin on the PE chip so I have to poll and compare old word to new word, etc.

usually there are not enough pins. hey, isn't that way we use PEs? lol

ryancasler commented 7 months ago

I setup button2 using an MCP23008 and an ESP32. I used a timer to update the state of 8 different buttons and used a global array to store that state of each of the buttons. Inside the button2 custom state handler rather than reading from the MCP I am reading from the array in memory. The fact that button2 "reads" the state of the button from the array more frequently than the timer updates the array doesn't really matter in the end. Both are happening so frequently that it self-mitigates any delays in updating the state. I was able to accurately capture single, double and triple presses and helds accurately. For example, if you are only updating the state of the buttons into the array every 100 ms, even if the button loop was to read at the exactly worst possible time, you're only going to get a maximum of 100ms delay. If you did want to use the interrupt on the MCP23008, you could still do the same thing except without the timer. The fact that the button2 loop is reading a "stale" array that hasn't been updated in a long time doesn't really matter to button2. With an ESP32, unless your sketch is huge, you are going to run through the loop and hit the button2 loop functions more frequently than you are ever going to want to poll an i2c device. So it is easier just to decouple the two of them.