| /* |
| * Generic GPIO / irq buttons |
| * |
| * Copyright (C) 2019 - 2020 Andy Green <[email protected]> |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a copy |
| * of this software and associated documentation files (the "Software"), to |
| * deal in the Software without restriction, including without limitation the |
| * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
| * sell copies of the Software, and to permit persons to whom the Software is |
| * furnished to do so, subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be included in |
| * all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| * IN THE SOFTWARE. |
| */ |
| #include "private-lib-core.h" |
| |
| typedef enum lws_button_classify_states { |
| LBCS_IDLE, /* nothing happening */ |
| LBCS_MIN_DOWN_QUALIFY, |
| |
| LBCS_ASSESS_DOWN_HOLD, |
| LBCS_UP_SETTLE1, |
| LBCS_WAIT_DOUBLECLICK, |
| LBCS_MIN_DOWN_QUALIFY2, |
| |
| LBCS_WAIT_UP, |
| LBCS_UP_SETTLE2, |
| } lws_button_classify_states_t; |
| |
| /* |
| * This is the opaque, allocated, non-const, dynamic footprint of the |
| * button controller |
| */ |
| |
| typedef struct lws_button_state { |
| #if defined(LWS_PLAT_TIMER_TYPE) |
| LWS_PLAT_TIMER_TYPE timer; /* bh timer */ |
| LWS_PLAT_TIMER_TYPE timer_mon; /* monitor timer */ |
| #endif |
| const lws_button_controller_t *controller; |
| struct lws_context *ctx; |
| short mon_refcount; |
| lws_button_idx_t enable_bitmap; |
| lws_button_idx_t state_bitmap; |
| |
| uint16_t mon_timer_count; |
| /* incremented each time the mon timer cb happens */ |
| |
| /* lws_button_each_t per button overallocated after this */ |
| } lws_button_state_t; |
| |
| typedef struct lws_button_each { |
| lws_button_state_t *bcs; |
| uint16_t mon_timer_comp; |
| uint16_t mon_timer_repeat; |
| uint8_t state; |
| /**^ lws_button_classify_states_t */ |
| uint8_t isr_pending; |
| } lws_button_each_t; |
| |
| #if defined(LWS_PLAT_TIMER_START) |
| static const lws_button_regime_t default_regime = { |
| .ms_min_down = 20, |
| .ms_min_down_longpress = 300, |
| .ms_up_settle = 20, |
| .ms_doubleclick_grace = 120, |
| .flags = LWSBTNRGMFLAG_CLASSIFY_DOUBLECLICK |
| }; |
| #endif |
| |
| |
| /* |
| * This is happening in interrupt context, we have to schedule a bottom half to |
| * do the foreground lws_smd queueing, using, eg, a platform timer. |
| * |
| * All the buttons point here and use one timer per button controller. An |
| * interrupt here means, "something happened to one or more buttons" |
| */ |
| #if defined(LWS_PLAT_TIMER_START) |
| void |
| lws_button_irq_cb_t(void *arg) |
| { |
| lws_button_each_t *each = (lws_button_each_t *)arg; |
| |
| each->isr_pending = 1; |
| LWS_PLAT_TIMER_START(each->bcs->timer); |
| } |
| #endif |
| |
| /* |
| * This is the bottom-half scheduled via a timer set in the ISR. From here we |
| * are allowed to hold mutexes etc. We are coming here because any button |
| * interrupt arrived, we have to run another timer that tries to put whatever is |
| * observed on any active button into context and either discard it or arrive at |
| * a definitive event classification. |
| */ |
| |
| #if defined(LWS_PLAT_TIMER_CB) |
| static LWS_PLAT_TIMER_CB(lws_button_bh, th) |
| { |
| lws_button_state_t *bcs = LWS_PLAT_TIMER_CB_GET_OPAQUE(th); |
| lws_button_each_t *each = (lws_button_each_t *)&bcs[1]; |
| const lws_button_controller_t *bc = bcs->controller; |
| size_t n; |
| |
| /* |
| * The ISR and bottom-half is shared by all the buttons. Each gpio |
| * IRQ has an individual opaque ptr pointing to the corresponding |
| * button's dynamic lws_button_each_t, the ISR marks the button's |
| * each->isr_pending and schedules this bottom half. |
| * |
| * So now the bh timer has fired and something to do, we need to go |
| * through all the buttons that have isr_pending set and service their |
| * state. Intermediate states should start / bump the refcount on the |
| * mon timer. That's refcounted so it only runs when a button down. |
| */ |
| |
| for (n = 0; n < bc->count_buttons; n++) { |
| |
| if (!each[n].isr_pending) |
| continue; |
| |
| /* |
| * Hide what we're about to do from the delicate eyes of the |
| * IRQ controller... |
| */ |
| |
| bc->gpio_ops->irq_mode(bc->button_map[n].gpio, |
| LWSGGPIO_IRQ_NONE, NULL, NULL); |
| |
| each[n].isr_pending = 0; |
| |
| /* |
| * Force the network around the switch to the |
| * active level briefly |
| */ |
| |
| bc->gpio_ops->set(bc->button_map[n].gpio, |
| !!(bc->active_state_bitmap & (1 << n))); |
| bc->gpio_ops->mode(bc->button_map[n].gpio, LWSGGPIO_FL_WRITE); |
| |
| if (each[n].state == LBCS_IDLE) { |
| /* |
| * If this is the first sign something happening on this |
| * button, make sure the monitor timer is running to |
| * classify its response over time |
| */ |
| |
| each[n].state = LBCS_MIN_DOWN_QUALIFY; |
| each[n].mon_timer_comp = bcs->mon_timer_count; |
| |
| if (!bcs->mon_refcount++) { |
| #if defined(LWS_PLAT_TIMER_START) |
| LWS_PLAT_TIMER_START(bcs->timer_mon); |
| #endif |
| } |
| } |
| |
| /* |
| * Just for a us or two inbetween here, we're driving it to the |
| * level we were informed by the interrupt it had enetered, to |
| * force to charge on the actual and parasitic network around |
| * the switch to a deterministic-ish state. |
| * |
| * If the switch remains in that state, well, it makes no |
| * difference; if it was a pre-contact and the charge on the |
| * network was left indeterminate, this will dispose it to act |
| * consistently in the short term until the pullup / pulldown |
| * has time to act on it or the switch comes and forces the |
| * network charge state itself. |
| */ |
| bc->gpio_ops->mode(bc->button_map[n].gpio, LWSGGPIO_FL_READ); |
| |
| /* |
| * We could do a better job manipulating the irq mode according |
| * to the switch state. But if an interrupt comes and we have |
| * done that, we can't tell if it's from before or after the |
| * mode change... ie, we don't know what the interrupt was |
| * telling us. We can't trust the gpio state if we read it now |
| * to be related to what the irq from some time before was |
| * trying to tell us. So always set it back to the same mode |
| * and accept the limitation. |
| */ |
| |
| bc->gpio_ops->irq_mode(bc->button_map[n].gpio, |
| bc->active_state_bitmap & (1 << n) ? |
| LWSGGPIO_IRQ_RISING : |
| LWSGGPIO_IRQ_FALLING, |
| lws_button_irq_cb_t, &each[n]); |
| } |
| } |
| #endif |
| |
| #if defined(LWS_PLAT_TIMER_CB) |
| static LWS_PLAT_TIMER_CB(lws_button_mon, th) |
| { |
| lws_button_state_t *bcs = LWS_PLAT_TIMER_CB_GET_OPAQUE(th); |
| lws_button_each_t *each = (lws_button_each_t *)&bcs[1]; |
| const lws_button_controller_t *bc = bcs->controller; |
| const lws_button_regime_t *regime; |
| const char *event_name; |
| int comp_age_ms; |
| char active; |
| size_t n; |
| |
| bcs->mon_timer_count++; |
| |
| for (n = 0; n < bc->count_buttons; n++) { |
| |
| if (each->state == LBCS_IDLE) { |
| each++; |
| continue; |
| } |
| |
| if (bc->button_map[n].regime) |
| regime = bc->button_map[n].regime; |
| else |
| regime = &default_regime; |
| |
| comp_age_ms = (bcs->mon_timer_count - each->mon_timer_comp) * |
| LWS_BUTTON_MON_TIMER_MS; |
| |
| active = bc->gpio_ops->read(bc->button_map[n].gpio) ^ |
| (!(bc->active_state_bitmap & (1 << n))); |
| |
| // lwsl_notice("%d\n", each->state); |
| |
| switch (each->state) { |
| case LBCS_MIN_DOWN_QUALIFY: |
| /* |
| * We're trying to figure out if the initial down event |
| * is a glitch, or if it meets the criteria for being |
| * treated as the definitive start of some kind of click |
| * action. To get past this, he has to be solidly down |
| * for the time mentioned in the applied regime (at |
| * least when we sample it). |
| * |
| * Significant bounce at the start will abort this try, |
| * but if it's really down there will be a subsequent |
| * solid down period... it will simply restart this flow |
| * from a new interrupt and pass the filter then. |
| * |
| * The "brief drive on edge" strategy considerably |
| * reduces inconsistencies here. But physical bounce |
| * will continue to be observed. |
| */ |
| |
| if (!active) { |
| /* We ignore stuff for a bit after discard */ |
| each->mon_timer_comp = bcs->mon_timer_count; |
| each->state = LBCS_UP_SETTLE2; |
| break; |
| } |
| |
| if (comp_age_ms >= regime->ms_min_down) { |
| |
| /* We made it through the initial regime filter, |
| * the next step is wait and see if this down |
| * event evolves into a single/double click or |
| * we can call it as a long-click |
| */ |
| |
| each->mon_timer_repeat = bcs->mon_timer_count; |
| each->state = LBCS_ASSESS_DOWN_HOLD; |
| event_name = "down"; |
| goto emit; |
| } |
| break; |
| |
| case LBCS_ASSESS_DOWN_HOLD: |
| |
| /* |
| * How long is he going to hold it? If he holds it |
| * past the long-click threshold, we can call it as a |
| * long-click and do the up processing afterwards. |
| */ |
| if (comp_age_ms >= regime->ms_min_down_longpress) { |
| /* call it as a longclick */ |
| event_name = "longclick"; |
| each->state = LBCS_WAIT_UP; |
| goto emit; |
| } |
| |
| if (!active) { |
| /* |
| * He didn't hold it past the long-click |
| * threshold... we could end up classifying it |
| * as either a click or a double-click then. |
| * |
| * If double-clicks are not allowed to be |
| * classified, then we can already classify it |
| * as a single-click. |
| */ |
| if (!(regime->flags & |
| LWSBTNRGMFLAG_CLASSIFY_DOUBLECLICK)) |
| goto classify_single; |
| |
| /* |
| * Just wait for the up settle time then start |
| * looking for a second down. |
| */ |
| each->mon_timer_comp = bcs->mon_timer_count; |
| each->state = LBCS_UP_SETTLE1; |
| event_name = "up"; |
| goto emit; |
| } |
| |
| goto stilldown; |
| |
| case LBCS_UP_SETTLE1: |
| if (comp_age_ms > regime->ms_up_settle) |
| /* |
| * Just block anything for the up settle time |
| */ |
| each->state = LBCS_WAIT_DOUBLECLICK; |
| break; |
| |
| case LBCS_WAIT_DOUBLECLICK: |
| if (active) { |
| /* |
| * He has gone down again inside the regime's |
| * doubleclick grace period... he's going down |
| * the double-click path |
| */ |
| each->mon_timer_comp = bcs->mon_timer_count; |
| each->state = LBCS_MIN_DOWN_QUALIFY2; |
| break; |
| } |
| |
| if (comp_age_ms >= regime->ms_doubleclick_grace) { |
| /* |
| * The grace period expired, the second click |
| * was either not forthcoming at all, or coming |
| * quick enough to count: we classify it as a |
| * single-click |
| */ |
| |
| goto classify_single; |
| } |
| break; |
| |
| case LBCS_MIN_DOWN_QUALIFY2: |
| if (!active) { |
| |
| /* |
| * He went up again too quickly, classify it |
| * as a single-click. It could be bounce in |
| * which case you might want to increase the |
| * ms_up_settle in the regime |
| */ |
| classify_single: |
| event_name = "click"; |
| each->mon_timer_comp = bcs->mon_timer_count; |
| each->state = LBCS_UP_SETTLE2; |
| goto emit; |
| } |
| |
| if (comp_age_ms == regime->ms_min_down) { |
| event_name = "down"; |
| goto emit; |
| } |
| |
| if (comp_age_ms > regime->ms_min_down) { |
| /* |
| * It's a double-click |
| */ |
| event_name = "doubleclick"; |
| each->state = LBCS_WAIT_UP; |
| goto emit; |
| } |
| break; |
| |
| case LBCS_WAIT_UP: |
| if (!active) { |
| /* |
| * He has stopped pressing it |
| */ |
| each->mon_timer_comp = bcs->mon_timer_count; |
| each->state = LBCS_UP_SETTLE2; |
| event_name = "up"; |
| goto emit; |
| } |
| stilldown: |
| if (regime->ms_repeat_down && |
| (bcs->mon_timer_count - each->mon_timer_repeat) * |
| LWS_BUTTON_MON_TIMER_MS > regime->ms_repeat_down) { |
| each->mon_timer_repeat = bcs->mon_timer_count; |
| event_name = "stilldown"; |
| goto emit; |
| } |
| break; |
| |
| case LBCS_UP_SETTLE2: |
| if (comp_age_ms < regime->ms_up_settle) |
| break; |
| |
| each->state = LBCS_IDLE; |
| if (!(--bcs->mon_refcount)) { |
| #if defined(LWS_PLAT_TIMER_STOP) |
| LWS_PLAT_TIMER_STOP(bcs->timer_mon); |
| #endif |
| } |
| } |
| |
| each++; |
| continue; |
| |
| emit: |
| lws_smd_msg_printf(bcs->ctx, LWSSMDCL_INTERACTION, |
| "{\"type\":\"button\"," |
| "\"src\":\"%s/%s\",\"event\":\"%s\"}", |
| bc->smd_bc_name, |
| bc->button_map[n].smd_interaction_name, |
| event_name); |
| |
| each++; |
| } |
| } |
| #endif |
| |
| struct lws_button_state * |
| lws_button_controller_create(struct lws_context *ctx, |
| const lws_button_controller_t *controller) |
| { |
| lws_button_state_t *bcs = lws_zalloc(sizeof(lws_button_state_t) + |
| (controller->count_buttons * sizeof(lws_button_each_t)), |
| __func__); |
| lws_button_each_t *each = (lws_button_each_t *)&bcs[1]; |
| size_t n; |
| |
| if (!bcs) |
| return NULL; |
| |
| bcs->controller = controller; |
| bcs->ctx = ctx; |
| |
| for (n = 0; n < controller->count_buttons; n++) |
| each[n].bcs = bcs; |
| |
| #if defined(LWS_PLAT_TIMER_CREATE) |
| /* this only runs inbetween a gpio ISR and the bottom half */ |
| bcs->timer = LWS_PLAT_TIMER_CREATE("bcst", |
| 1, 0, bcs, (TimerCallbackFunction_t)lws_button_bh); |
| if (!bcs->timer) |
| return NULL; |
| |
| /* this only runs when a button activity is being classified */ |
| bcs->timer_mon = LWS_PLAT_TIMER_CREATE("bcmon", LWS_BUTTON_MON_TIMER_MS, |
| 1, bcs, (TimerCallbackFunction_t) |
| lws_button_mon); |
| if (!bcs->timer_mon) |
| return NULL; |
| #endif |
| |
| return bcs; |
| } |
| |
| void |
| lws_button_controller_destroy(struct lws_button_state *bcs) |
| { |
| /* disable them all */ |
| lws_button_enable(bcs, 0, 0); |
| |
| #if defined(LWS_PLAT_TIMER_DELETE) |
| LWS_PLAT_TIMER_DELETE(&bcs->timer); |
| LWS_PLAT_TIMER_DELETE(&bcs->timer_mon); |
| #endif |
| |
| lws_free(bcs); |
| } |
| |
| lws_button_idx_t |
| lws_button_get_bit(struct lws_button_state *bcs, const char *name) |
| { |
| const lws_button_controller_t *bc = bcs->controller; |
| int n; |
| |
| for (n = 0; n < bc->count_buttons; n++) |
| if (!strcmp(name, bc->button_map[n].smd_interaction_name)) |
| return 1 << n; |
| |
| return 0; /* not found */ |
| } |
| |
| void |
| lws_button_enable(lws_button_state_t *bcs, |
| lws_button_idx_t _reset, lws_button_idx_t _set) |
| { |
| lws_button_idx_t u = (bcs->enable_bitmap & (~_reset)) | _set; |
| const lws_button_controller_t *bc = bcs->controller; |
| #if defined(LWS_PLAT_TIMER_START) |
| lws_button_each_t *each = (lws_button_each_t *)&bcs[1]; |
| #endif |
| int n; |
| |
| for (n = 0; n < bcs->controller->count_buttons; n++) { |
| if (!(bcs->enable_bitmap & (1 << n)) && (u & (1 << n))) { |
| /* set as input with pullup or pulldown appropriately */ |
| bc->gpio_ops->mode(bc->button_map[n].gpio, |
| LWSGGPIO_FL_READ | |
| ((bc->active_state_bitmap & (1 << n)) ? |
| LWSGGPIO_FL_PULLDOWN : LWSGGPIO_FL_PULLUP)); |
| #if defined(LWS_PLAT_TIMER_START) |
| /* |
| * This one is becoming enabled... the opaque for the |
| * ISR is the indvidual lws_button_each_t, they all |
| * point to the same ISR |
| */ |
| bc->gpio_ops->irq_mode(bc->button_map[n].gpio, |
| bc->active_state_bitmap & (1 << n) ? |
| LWSGGPIO_IRQ_RISING : |
| LWSGGPIO_IRQ_FALLING, |
| lws_button_irq_cb_t, &each[n]); |
| #endif |
| } |
| if ((bcs->enable_bitmap & (1 << n)) && !(u & (1 << n))) |
| /* this one is becoming disabled */ |
| bc->gpio_ops->irq_mode(bc->button_map[n].gpio, |
| LWSGGPIO_IRQ_NONE, NULL, NULL); |
| } |
| |
| bcs->enable_bitmap = u; |
| } |