Skip to content
Snippets Groups Projects
Select Git revision
  • 1635636b14c502fead9ee31b2576bd7c283a48db
  • main default protected
  • phhw
  • captouch-threshold
  • t
  • dos
  • test2
  • test
  • slewtest
  • simtest
  • view-think
  • vm-pending
  • media-buf
  • scope
  • passthrough
  • wave
  • vsync
  • dos-main-patch-50543
  • json-error
  • rahix/big-flow3r
  • pippin/media_framework
  • v1.3.0
  • v1.2.0
  • v1.2.0+rc1
  • v1.1.1
  • v1.1.0
  • v1.1.0+rc1
  • v1.0.0
  • v1.0.0+rc6
  • v1.0.0+rc5
  • v1.0.0+rc4
  • v1.0.0+rc3
  • v1.0.0+rc2
  • v1.0.0+rc1
34 results

input.py

Blame
  • Forked from flow3r / flow3r firmware
    186 commits behind the upstream repository.
    Sebastian Krzyszkowiak's avatar
    dos authored
    Apps that don't use an input method shouldn't be penalized with its
    processing time.
    1635636b
    History
    input.py 19.14 KiB
    from st3m.goose import List, Optional, Enum, Tuple
    
    import sys_buttons
    import captouch
    import imu
    from st3m.power import Power
    
    power = Power()
    
    
    class IMUState:
        """
        State of the Inertial Measurement Unit
    
        Acceleration in m/s**2, roation rate in deg/s, pressure in Pascal
        """
    
        __slots__ = ("acc", "gyro", "pressure")
    
        def __init__(
            self,
            acc: Tuple[float, float, float],
            gyro: Tuple[float, float, float],
            pressure: float,
        ) -> None:
            self.acc = acc
            self.gyro = gyro
            self.pressure = pressure
    
    
    class InputButtonState:
        """
        State of the tri-state switches/buttons on the shoulders of the badge.
    
        If you want to detect edges, use the stateful InputController.
    
        By default, the left shoulder button is the 'app' button and the right
        shoulder button is the 'os' button. The user can switch this behaviour in
        the settings menu.
    
        The 'app' button can be freely used by applicaton code. The 'os' menu has
        fixed functions: volume up/down and back.
    
        In cases you want to access left/right buttons independently of app/os
        mapping (for example in applications where the handedness of the user
        doesn't matter), then you can use _left and _right to access their state
        directly.
    
        'app_is_left' is provided to let you figure out on which side of the badge
        the app button is, eg. for use when highlighting buttons on the screen or
        with LEDs.
        """
    
        __slots__ = ("app", "os", "_left", "_right", "app_is_left")
    
        PRESSED_LEFT = sys_buttons.PRESSED_LEFT
        PRESSED_RIGHT = sys_buttons.PRESSED_RIGHT
        PRESSED_DOWN = sys_buttons.PRESSED_DOWN
        NOT_PRESSED = sys_buttons.NOT_PRESSED
    
        def __init__(self, app: int, os: int, app_is_left: bool):
            self.app = app
            self.os = os
            self.app_is_left = app_is_left
    
    
    class InputState:
        """
        Current state of inputs from badge user. Passed via think() to every
        Responder.
    
        If you want to detect edges, use the stateful InputController.
        """
    
        def __init__(self) -> None:
            self._captouch = None
            self._buttons = None
            self._imu = None
            self._temperature = None
            self._battery_voltage = None
            self._pressure = None
    
        @property
        def captouch(self):
            if self._captouch is None:
                self._captouch = captouch.read()
            return self._captouch
    
        @property
        def buttons(self):
            if self._buttons is None:
                app = sys_buttons.get_app()
                os = sys_buttons.get_os()
                app_is_left = sys_buttons.app_is_left()
                self._buttons = InputButtonState(app, os, app_is_left)
            return self._buttons
    
        @property
        def imu(self):
            if self._imu is None:
                acc = imu.acc_read()
                gyro = imu.gyro_read()
                if self._pressure is None:
                    self._pressure, self._temperature = imu.pressure_read()
                self._imu = IMUState(acc, gyro, self._pressure)
            return self._imu
    
        @property
        def battery_voltage(self):
            if self._battery_voltage is None:
                self._battery_voltage = power.battery_voltage
            return self._battery_voltage
    
        @property
        def temperature(self):
            if self._temperature is None:
                self._pressure, self._temperature = imu.pressure_read()
            return self._temperature
    
    
    class RepeatSettings:
        def __init__(self, first: float, subsequent: float) -> None:
            self.first = first
            self.subsequent = subsequent
    
    
    class PressableState(Enum):
        PRESSED = "pressed"
        REPEATED = "repeated"
        RELEASED = "released"
        DOWN = "down"
        UP = "up"
    
    
    class Pressable:
        """
        A pressable button or button-acting object (like captouch petal in button
        mode).
    
        Carries information about current and previous state of button, allowing to
        detect edges (pressed/released) and state (down/up). Additionally implements
        button repeating.
        """
    
        PRESSED = PressableState.PRESSED
        REPEATED = PressableState.REPEATED
        RELEASED = PressableState.RELEASED
        DOWN = PressableState.DOWN
        UP = PressableState.UP
    
        def __init__(self, state: bool) -> None:
            self._state = state
            self._prev_state = state
            self._repeat: Optional[RepeatSettings] = RepeatSettings(400, 200)
    
            self._pressed_at: Optional[float] = None
            self._repeating = False
            self._repeated = False
    
            self._ignoring = 0
    
        def repeat_enable(self, first: int = 400, subsequent: int = 200) -> None:
            """
            Enable key repeat functionality. Arguments are amount to wait in ms
            until first repeat is emitted and until subsequent repeats are emitted.
    
            Repeat is enabled by default on Pressables.
            """
            self._repeat = RepeatSettings(first, subsequent)
    
        def repeat_disable(self) -> None:
            """
            Disable repeat functionality on this Pressable.
            """
            self._repeat = None
    
        def _update(self, ts: int, state: bool) -> None:
            if self._ignoring > 0:
                self._ignoring -= 1
    
            self._prev_state = self._state
            self._state = state
            self._repeated = False
    
            if state == False:
                self._pressed_at = None
                self._repeating = False
            else:
                if self._pressed_at is None:
                    self._pressed_at = ts
    
            repeat = self._repeat
            if state and repeat is not None and self._pressed_at is not None:
                if not self._repeating:
                    if ts > self._pressed_at + repeat.first:
                        self._repeating = True
                        self._repeated = True
                        self._prev_state = False
                        self._pressed_at = ts
                else:
                    if ts > self._pressed_at + repeat.subsequent:
                        self._prev_state = False
                        self._pressed_at = ts
                        self._repeated = True
    
        @property
        def state(self) -> PressableState:
            """
            Returns one of PressableState.{UP,DOWN,PRESSED,RELEASED,REPEATED}.
            """
            prev = self._prev_state
            cur = self._state
    
            if self._ignoring > 0:
                return self.UP
    
            if self._repeated:
                return self.REPEATED
    
            if cur and not prev:
                return self.PRESSED
            if not cur and prev:
                return self.RELEASED
            if cur and prev:
                return self.DOWN
            return self.UP
    
        @property
        def pressed(self) -> bool:
            """
            True if the button has just been pressed.
            """
            return self.state == self.PRESSED
    
        @property
        def repeated(self) -> bool:
            """
            True if the button has been held long enough that a virtual 'repeat'
            press should be acted upon.
            """
            return self.state == self.REPEATED
    
        @property
        def released(self) -> bool:
            """
            True if the button has just been released.
            """
            return self.state == self.RELEASED
    
        @property
        def down(self) -> bool:
            """
            True if the button is held down, after first being pressed.
            """
            return self.state == self.DOWN
    
        @property
        def up(self) -> bool:
            """
            True if the button is currently not being held down.
            """
            return self.state == self.UP
    
        def _ignore_pressed(self) -> None:
            """
            Pretend the button isn't being pressed for the next two update
            iterations. Used to prevent spurious presses to be routed to apps that
            have just been foregrounded.
            """
            self._ignoring = 2
            self._repeating = False
            self._repeated = False
    
        def __repr__(self) -> str:
            return "<Pressable: " + str(self.state) + ">"
    
    
    class TouchableState(Enum):
        UP = "up"
        BEGIN = "begin"
        RESTING = "resting"
        MOVED = "moved"
        ENDED = "ended"
    
    
    class Touchable:
        """
        A Touchable processes incoming captouch positional state into higher-level
        simple gestures.
    
        The Touchable can be in one of four states:
    
            UP: not currently being interacted with
            BEGIN: some gesture has just started
            MOVED: a gesture is continuing
            ENDED: a gesture has just ended
    
        The state can be retrieved by calling phase().
    
        The main API for consumers is current_gesture(), which returns a
        Touchable.Gesture defining the current state of the gesture, from the
        beginning of the touch event up until now (or until the end, if the current
        phase is ENDED).
    
        Under the hood, the Touchable keeps a log of recent samples from the
        captouch petal position, and processes them to eliminate initial noise from
        the beginning of a gesture.
    
        All positional output state is the same format/range as in the low-level
        CaptouchState.
        """
    
        UP = TouchableState.UP
        BEGIN = TouchableState.BEGIN
        MOVED = TouchableState.MOVED
        ENDED = TouchableState.ENDED
    
        class Entry:
            """
            A Touchable's log entry, containing some position measurement at some
            timestamp.
            """
    
            __slots__ = ["ts", "phi", "rad"]
    
            def __init__(self, ts: int, phi: float, rad: float) -> None:
                self.ts = ts
                self.phi = phi
                self.rad = rad
    
            def __repr__(self) -> str:
                return f"{self.ts}: ({self.rad}, {self.phi})"
    
        class Gesture:
            """
            A simple captouch gesture, currently definined as a movement between two
            points: the beginning of the gesture (when the user touched the petal)
            and to the current state. If the gesture is still active, the current
            state is averaged/filtered to reduce noise. If the gesture has ended,
            the current state is the last measured position.
            """
    
            def __init__(self, start: "Touchable.Entry", end: "Touchable.Entry") -> None:
                self.start = start
                self.end = end
    
            @property
            def distance(self) -> Tuple[float, float]:
                """
                Distance traveled by this gesture.
                """
                delta_rad = self.end.rad - self.start.rad
                delta_phi = self.end.phi - self.start.phi
                return (delta_rad, delta_phi)
    
            @property
            def velocity(self) -> Tuple[float, float]:
                """
                Velocity vector of this gesture.
                """
                delta_rad = self.end.rad - self.start.rad
                delta_phi = self.end.phi - self.start.phi
                if self.end.ts == self.start.ts:
                    return (0, 0)
                delta_s = (self.end.ts - self.start.ts) / 1000
                return (delta_rad / delta_s, delta_phi / delta_s)
    
        def __init__(self, pos: tuple[float, float] = (0.0, 0.0)) -> None:
            # Entry log, used for filtering.
            self._log: List[Touchable.Entry] = []
    
            # What the beginning of the gesture is defined as. This is ampled a few
            # entries into the log as the initial press stabilizes.
            self._start: Optional[Touchable.Entry] = None
            self._start_ts: int = 0
    
            # Current and previous 'pressed' state from the petal, used to begin
            # gesture tracking.
            self._pressed = False
            self._prev_pressed = self._pressed
    
            self._state = self.UP
    
            # If not nil, amount of update() calls to wait until the gesture has
            # been considered as started. This is part of the mechanism which
            # eliminates early parts of a gesture while the pressure on the sensor
            # grows and the user's touch contact point changes.
            self._begin_wait: Optional[int] = None
    
            self._last_ts: int = 0
    
        def _append_entry(self, ts: int, petal: captouch.CaptouchPetalState) -> None:
            """
            Append an Entry to the log based on a given CaptouchPetalState.
            """
            (rad, phi) = petal.position
            entry = self.Entry(ts, phi, rad)
            self._log.append(entry)
            overflow = len(self._log) - 10
            if overflow > 0:
                self._log = self._log[overflow:]
    
        def _update(self, ts: int, petal: captouch.CaptouchPetalState) -> None:
            """
            Called when the Touchable is being processed by an InputController.
            """
            self._last_ts = ts
            self._prev_pressed = self._pressed
            self._pressed = petal.pressed
    
            if not self._pressed:
                if not self._prev_pressed or self._start is None:
                    self._state = self.UP
                    self._start = None
                else:
                    self._state = self.ENDED
                return
    
            self._append_entry(ts, petal)
    
            if not self._prev_pressed:
                # Wait 5 samples until we consider the gesture started.
                # TODO(q3k): do better than hardcoding this. Maybe use pressure data?
                self._begin_wait = 5
            elif self._begin_wait is not None:
                self._begin_wait -= 1
                if self._begin_wait < 0:
                    self._begin_wait = None
                    # Okay, the gesture has officially started.
                    self._state = self.BEGIN
                    # Grab latest log entry as gesture start.
                    self._start = self._log[-1]
                    self._start_ts = ts
                    # Prune log.
                    self._log = self._log[-1:]
            else:
                self._state = self.MOVED
    
        def phase(self) -> TouchableState:
            """
            Returns the current phase of a gesture as tracked by this Touchable (petal).
            """
            return self._state
    
        def current_gesture(self) -> Optional[Gesture]:
            if self._start is None:
                return None
    
            assert self._start_ts is not None
            delta_ms = self._last_ts - self._start_ts
    
            first = self._start
            last = self._log[-1]
            # If this gesture hasn't ended, grab last 5 log entries for average of
            # current position. This filters out a bunch of noise.
            if self.phase() != self.ENDED:
                log = self._log[-5:]
                phis = [el.phi for el in log]
                rads = [el.rad for el in log]
                phi_avg = sum(phis) / len(phis)
                rad_avg = sum(rads) / len(rads)
                last = self.Entry(last.ts, phi_avg, rad_avg)
    
            return self.Gesture(first, last)
    
    
    class PetalState:
        def __init__(self, ix: int) -> None:
            self.ix = ix
            self._whole = Pressable(False)
            self._gesture = Touchable()
            self._whole_updated = False
            self._gesture_updated = False
            self._ts = None
            self._petal = None
    
        def _update(self, ts: int, petal: captouch.CaptouchPetalState) -> None:
            self._ts = ts
            self._petal = petal
            self._whole_updated = False
            self._gesture_updated = False
    
        @property
        def whole(self):
            if self._petal and not self._whole_updated:
                self._whole._update(self._ts, self._petal.pressed)
                self._whole_updated = True
            return self._whole
    
        @property
        def pressure(self):
            if not self._petal:
                return 0
            return self._petal.pressure
    
        @property
        def gesture(self):
            if self._petal and not self._gesture_updated:
                self._gesture._update(self._ts, self._petal)
                self._gesture_updated = True
            return self._gesture
    
    
    class CaptouchState:
        """
        State of capacitive touch petals.
    
        The petals are indexed from 0 to 9 (inclusive). Petal 0 is above the USB-C
        socket, then the numbering continues clockwise.
        """
    
        def __init__(self) -> None:
            self._petals = [PetalState(i) for i in range(10)]
            self._ins = None
            self._ts = None
            self._updated = False
    
        def _update(self, ts: int, ins: InputState) -> None:
            self._ins = ins
            self._ts = ts
            self._updated = False
    
        @property
        def petals(self):
            if self._ins and not self._updated:
                for i, petal in enumerate(self._petals):
                    petal._update(self._ts, self._ins.captouch.petals[i])
                self._updated = True
            return self._petals
    
        def _ignore_pressed(self) -> None:
            for petal in self._petals:
                petal.whole._ignore_pressed()
    
    
    class TriSwitchState:
        """
        State of a tri-stat shoulder button
        """
    
        __slots__ = ("left", "middle", "right")
    
        def __init__(self) -> None:
            self.left = Pressable(False)
            self.middle = Pressable(False)
            self.right = Pressable(False)
    
        def _update(self, ts: int, st: int) -> None:
            self.left._update(ts, st == -1)
            self.middle._update(ts, st == 2)
            self.right._update(ts, st == 1)
    
        def _ignore_pressed(self) -> None:
            self.left._ignore_pressed()
            self.middle._ignore_pressed()
            self.right._ignore_pressed()
    
    
    class ButtonsState:
        """
        Edge-trigger detection for input button state.
    
        See  InputButtonState for more information about the meaning of app, os,
        _left, _right and app_is_left.
        """
    
        __slots__ = ("app", "os", "_left", "_right", "app_is_left", "_app_is_left_prev")
    
        def __init__(self) -> None:
            self.app = TriSwitchState()
            self.os = TriSwitchState()
    
            # Defaults. Real data coming from _update will change this to the
            # correct values from an InputState.
            self._left = self.app
            self._right = self.os
            self.app_is_left = True
            self._app_is_left_prev = self.app_is_left
    
        def _update(self, ts: int, hr: InputState) -> None:
            # Check whether we swapped left/right buttons. If so, carry over changes
            # from buttons as mapped previously, otherwise we get spurious presses.
            self.app_is_left = hr.buttons.app_is_left
            if self._app_is_left_prev != self.app_is_left:
                # BUG(q3k): if something is holding on to controller button
                # references, then this will break their code.
                self.app, self.os = self.os, self.app
    
            self.app._update(ts, hr.buttons.app)
            self.os._update(ts, hr.buttons.os)
            self._app_is_left_prev = self.app_is_left
    
            if self.app_is_left:
                self._left = self.app
                self._right = self.os
            else:
                self._left = self.os
                self._right = self.app
    
        def _ignore_pressed(self) -> None:
            self.app._ignore_pressed()
            self.os._ignore_pressed()
    
    
    class InputController:
        """
        A stateful input controller. It accepts InputState updates from the Reactor
        and allows a Responder to detect input events, like a button having just
        been pressed.
    
        To use, instantiate within a Responder and call think() from your
        responder's think().
    
        Then, access the captouch/left_shoulder/right_shoulder fields.
        """
    
        __slots__ = (
            "captouch",
            "buttons",
            "_ts",
        )
    
        def __init__(self) -> None:
            self.captouch = CaptouchState()
            self.buttons = ButtonsState()
            self._ts = 0
    
        def think(self, hr: InputState, delta_ms: int) -> None:
            self._ts += delta_ms
            self.captouch._update(self._ts, hr)
            self.buttons._update(self._ts, hr)
    
        def _ignore_pressed(self) -> None:
            """
            Pretend input buttons aren't being pressed for the next two update
            iterations. Used to prevent spurious presses to be routed to apps that
            have just been foregrounded.
            """
            self.captouch._ignore_pressed()
            self.buttons._ignore_pressed()