UNPKG

homebridge-lutron-caseta-leap-fast

Version:
184 lines 7.92 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ButtonTracker = void 0; var ButtonState; (function (ButtonState) { ButtonState[ButtonState["IDLE"] = 0] = "IDLE"; ButtonState[ButtonState["DOWN"] = 1] = "DOWN"; ButtonState[ButtonState["UP"] = 2] = "UP"; })(ButtonState || (ButtonState = {})); // the "double press timeout" is the amount of time you have to start the // second press after the initial press is *released*. this is the *maximum // dwell time*. the duration of the second press does not matter, only its // initiation matters. after the second press is detected, the state machine is // reset. the next press will be another initial press. const DOUBLE_PRESS_DWELL_MS = new Map([ ['quick', 300], ['default', 300], ['relaxed', 450], ['disabled', 0], ]); // the "long press timeout" is the amount of time you must hold the button down // for an *initial* press to count as "long". when this happens, the state // machine is reset. the next press will be another initial press. const LONG_PRESS_TIMEOUT_MS = new Map([ ['quick', 300], ['default', 350], ['relaxed', 750], ['disabled', 0], ]); // Up- and down-buttons on Picos (eg. PJ2-3BRL and PJ2-2BRL) appear to be // intentionally slowed in their response: // // A millisecond log excerpt of pressing an "up" button as quickly as I can // manage (with no mobility impairments): // // 2022-07-26,10:29:15.862 ... 'ButtonEvent': {'EventType': 'Press'}}}} // 2022-07-26,10:29:15.956 ... 'ButtonEvent': {'EventType': 'Release'}}}} // 2022-07-26,10:29:16.477 ... 'ButtonEvent': {'EventType': 'Press'}}}} // 2022-07-26,10:29:16.495 ... 'ButtonEvent': {'EventType': 'Release'}}}} // // That's a time of 94 ms down, 521 ms dwell, 18 ms down, and 633 ms total. // // Now, the same excerpt from an "on" button: // // 2022-07-26,10:27:47.694 ... 'ButtonEvent': {'EventType': 'Press'}}}} // 2022-07-26,10:27:47.807 ... 'ButtonEvent': {'EventType': 'Release'}}}} // 2022-07-26,10:27:47.887 ... 'ButtonEvent': {'EventType': 'Press'}}}} // 2022-07-26,10:27:47.965 ... 'ButtonEvent': {'EventType': 'Release'}}}} // // That's a time of 113 ms down, 80 ms dwell, 78 ms down, and 271 ms total. // // This obviously includes network propagation delay. Interestingly, I can't // find any indication that the Smart Hub or the Pico itself will emit anything // that looks like a *native* double-press. // // This means that we must handle up- and down-buttons differently, and add // some delay for detecting double-press events. Long-press events are // unaffected. const UP_DOWN_BTN_DELAY_MS = 250; class ButtonTracker { constructor(shortPressCB, doublePressCB, longPressCB, log, href, clickSpeedDouble = 'default', clickSpeedLong = 'default', isUpDownButton = false) { this.shortPressCB = shortPressCB; this.doublePressCB = doublePressCB; this.longPressCB = longPressCB; this.log = log; this.href = href; this.state = ButtonState.IDLE; this.longPressDisabled = false; this.doublePressDisabled = false; log.debug(`btrk ${this.href} created speed ${clickSpeedDouble} dbl ${clickSpeedLong} long`); this.timer = null; if (clickSpeedLong === 'disabled') { this.longPressDisabled = true; } if (clickSpeedDouble === 'disabled') { this.doublePressDisabled = true; } if (!DOUBLE_PRESS_DWELL_MS.has(clickSpeedDouble)) { throw new Error(`Could not get dbl timing for speed ${clickSpeedDouble}`); } if (!LONG_PRESS_TIMEOUT_MS.has(clickSpeedLong)) { throw new Error(`Could not get long timing for speed ${clickSpeedLong}`); } this.longPressTimeout = LONG_PRESS_TIMEOUT_MS.get(clickSpeedLong); this.doublePressTimeout = DOUBLE_PRESS_DWELL_MS.get(clickSpeedDouble); if (isUpDownButton && !this.doublePressDisabled) { this.doublePressTimeout += UP_DOWN_BTN_DELAY_MS; } } reset() { this.state = ButtonState.IDLE; if (this.timer) { clearTimeout(this.timer); } this.timer = null; this.log.debug('btrk reset to IDLE'); } update(action) { this.log.debug(`btrk ${this.href} got event ${action} in state ${this.state}`); // TODO this state machine is ill-formed, and relies on `this.timer` // implicitly being included in state decisions. refactor so the timer // updates the state variable. this will also make the `disabled` // options clearer. const longPressTimeoutHandler = () => { this.log.debug(`btrk ${this.href} long press timeout`); this.reset(); if (this.longPressDisabled) { // unreachable return; } this.log.info(`button ${this.href} got a long press`); this.longPressCB(); }; const doublePressTimeoutHandler = () => { this.log.debug(`btrk ${this.href} double press expiry`); this.reset(); this.log.info(`button ${this.href} got a short press`); this.shortPressCB(); }; switch (this.state) { case ButtonState.IDLE: { if (action === 'Press') { this.state = ButtonState.DOWN; this.log.debug(`btrk ${this.href} now in state DOWN`); if (this.longPressDisabled) { if (this.doublePressDisabled) { this.shortPressCB(); } else { this.log.info(`button ${this.href} long press disabled. suppressing.`); } } else { this.timer = setTimeout(longPressTimeoutHandler, this.longPressTimeout); } } else { // no-op this.log.debug(`btrk ${this.href} no-op IDLE action ${action}`); } break; } case ButtonState.DOWN: { if (action === 'Release') { this.state = ButtonState.UP; if (this.timer) { clearTimeout(this.timer); this.log.debug(`btrk ${this.href} cleared timer`); } this.timer = setTimeout(() => { doublePressTimeoutHandler(); }, this.doublePressTimeout); this.log.debug(`btrk ${this.href} now in UP state`); } else { // action == "Press" this.log.error(`btrk invalid action ${action} for state ${this.state}. resetting`); this.reset(); } break; } case ButtonState.UP: { if (action === 'Press' && this.timer) { // the button was pressed again before the timer fired this.log.debug(`btrk ${this.href} pressed before double-tap expiry`); this.reset(); if (this.doublePressDisabled) { this.log.info(`button ${this.href} double press disabled. suppressing.`); return; } this.log.info(`button ${this.href} got a double press`); this.doublePressCB(); } else { this.log.error(`btrk invalid action ${action} for state ${this.state}. resetting`); this.reset(); } break; } } } } exports.ButtonTracker = ButtonTracker; //# sourceMappingURL=ButtonState.js.map