From b849bd4d9144f75f4eaf4b9a1a398f59c70a6f6f Mon Sep 17 00:00:00 2001 From: ave <flow3r@ave.zone> Date: Sat, 26 Aug 2023 21:58:51 +0000 Subject: [PATCH] Add w1f1 app & replace wifi in settings menu --- python_payload/apps/w1f1/__init__.py | 284 ++++++++++++++ python_payload/apps/w1f1/flow3r.toml | 13 + python_payload/apps/w1f1/k3yboard.py | 549 +++++++++++++++++++++++++++ python_payload/st3m/settings.py | 23 +- python_payload/st3m/wifi.py | 1 + 5 files changed, 853 insertions(+), 17 deletions(-) create mode 100644 python_payload/apps/w1f1/__init__.py create mode 100644 python_payload/apps/w1f1/flow3r.toml create mode 100644 python_payload/apps/w1f1/k3yboard.py diff --git a/python_payload/apps/w1f1/__init__.py b/python_payload/apps/w1f1/__init__.py new file mode 100644 index 0000000000..607236843f --- /dev/null +++ b/python_payload/apps/w1f1/__init__.py @@ -0,0 +1,284 @@ +from st3m.application import Application, ApplicationContext +from st3m.input import InputState +from st3m.goose import Optional +from st3m.ui.view import ViewManager +from ctx import Context +import network +import leds +import os +import json +import math +from .k3yboard import TextInputModel, KeyboardView + + +class WifiApp(Application): + WIFI_CONFIG_FILE = "/flash/w1f1_config.json" + SETTINGS_JSON_FILE = "/flash/settings.json" + + def __init__(self, app_ctx: ApplicationContext) -> None: + super().__init__(app_ctx) + self._petal_pressed = {} + self._nearby_wlans = [] + self._status_text = "scanning" + self._error_text = "" + self._wlan_offset = 0 + self._is_connecting = False + self._waiting_for_password = False + self._password_model = TextInputModel("") + + if os.path.exists(self.WIFI_CONFIG_FILE): + with open(self.WIFI_CONFIG_FILE) as f: + self._wifi_config = json.load(f) + else: + self._wifi_config = { + "config_version": 2, + "networks": { + "Example SSID": {"psk": "Example PSK"}, + "Camp2023-open": {"psk": None}, + }, + } + with open(self.WIFI_CONFIG_FILE, "w") as f: + json.dump(self._wifi_config, f) + + def on_enter(self, vm: Optional[ViewManager]) -> None: + super().on_enter(vm) + self._connection_timer = 10 + self._scan_timer = 0 + self._iface = network.WLAN(network.STA_IF) + self._current_ssid = None + self._current_psk = None + + self.input._ignore_pressed() + # TODO: big error display + + def draw(self, ctx: Context) -> None: + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + ctx.font = ctx.get_font_name(8) + + ctx.rgb(0, 0, 0).rectangle(-120, -90, 240, 180).fill() + ctx.rgb(0.2, 0.2, 0.2).rectangle(-120, -120, 240, 30).fill() + ctx.rgb(0.2, 0.2, 0.2).rectangle(-120, 90, 240, 30).fill() + + ctx.font_size = 15 + current_ssid = self._iface.config("ssid") + + ctx.save() + ctx.rgb(1, 1, 1) + if self._iface.active(): + ctx.rgb(0, 1, 0) + else: + ctx.rgb(1, 0, 0) + ctx.move_to(0, -105) + ctx.text("^") + ctx.move_to(0, -100) + ctx.text("toggle wlan") + ctx.restore() + + ctx.rgb(1, 1, 1) + ctx.move_to(0, 100) + ctx.text(self._status_text) + + wlan_draw_offset = self._wlan_offset * -20 + + for wlan in self._nearby_wlans: + ssid = wlan[0].decode() + if ( + ssid == current_ssid + and self._iface.active() + and self._iface.isconnected() + ): + ctx.rgb(0, 1, 0) + elif ssid == self._is_connecting: + ctx.rgb(0, 0, 1) + elif ssid in self._wifi_config["networks"]: + ctx.rgb(1, 1, 0) + else: + ctx.rgb(1, 1, 1) + if math.fabs(wlan_draw_offset) > 90: + wlan_draw_offset += 20 + continue + if self._nearby_wlans[self._wlan_offset] == wlan: + ctx.font_size = 25 + else: + ctx.font_size = 15 + ctx.move_to(0, wlan_draw_offset) + ctx.text(ssid) + + # TODO: maybe add signal indicator? + # https://fonts.google.com/icons?selected=Material+Icons+Outlined:network_wifi_1_bar:&icon.query=network+wifi&icon.set=Material+Icons + + # draw a key next to wifi if it isn't open + if wlan[4] != 0: + ctx.save() + ssid_width = ctx.text_width(ssid) + ctx.font = "Material Icons" + ctx.text_align = ctx.LEFT + ctx.move_to((ssid_width / 2) + 5, wlan_draw_offset + 2) + ctx.text("\ue897") + ctx.restore() + + wlan_draw_offset += 20 + + def set_direction_leds(self, direction, r, g, b): + if direction == 0: + leds.set_rgb(39, r, g, b) + else: + leds.set_rgb((direction * 4) - 1, r, g, b) + leds.set_rgb(direction * 4, r, g, b) + leds.set_rgb((direction * 4) + 1, r, g, b) + + def on_exit(self) -> None: + leds.set_all_rgb(0, 0, 0) + leds.update() + + def scan_wifi(self): + # skip hidden WLANs + self._nearby_wlans = [ + wlan for wlan in self._iface.scan() if not wlan[5] and wlan[0] + ] + # TODO: sort by known, then signal strength + print(self._nearby_wlans) + + def update_settings_json(self, ssid: str, psk: str) -> None: + # weirdo case + if os.path.exists(self.SETTINGS_JSON_FILE): + with open(self.SETTINGS_JSON_FILE) as f: + settings_json = json.load(f) + else: + settings_json = {"system": {}} + + if "wifi" not in settings_json["system"]: + settings_json["system"]["wifi"] = { + "enabled": True, + "ssid": "Camp2023-open", + "psk": None, + } + # clean up old config + if "camp_wifi_enabled" in settings_json["system"]: + del settings_json["system"]["camp_wifi_enabled"] + + settings_json["system"]["wifi"]["ssid"] = ssid + settings_json["system"]["wifi"]["psk"] = psk + + with open(self.SETTINGS_JSON_FILE, "w") as f: + json.dump(settings_json, f) + + def add_to_config_json(self, ssid: str, psk: str) -> None: + self._wifi_config["networks"][ssid] = {"psk": psk} + + with open(self.WIFI_CONFIG_FILE, "w") as f: + json.dump(self._wifi_config, f) + + def connect_wifi(self, ssid: str, psk: str = None) -> None: + if ssid in self._wifi_config["networks"]: + psk = self._wifi_config["networks"][ssid]["psk"] + + self._current_ssid = ssid + self._current_psk = psk + + try: + self._is_connecting = ssid + self._iface.connect( + ssid, + psk, + ) + self._status_text = "connecting" + except OSError as e: + self._status_text = str(e) + self._is_connecting = False + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) + leds.set_all_rgb(0, 0, 0) + + if self.input.buttons.app.left.pressed and self._wlan_offset > 0: + self._wlan_offset -= 1 + elif ( + self.input.buttons.app.right.pressed + and self._wlan_offset < len(self._nearby_wlans) - 1 + ): + self._wlan_offset += 1 + + if not self._nearby_wlans and self._iface.active() and self._scan_timer <= 0: + self._status_text = "scanning" + self.scan_wifi() + self._wlan_offset = 0 + self._status_text = "ready" + self._scan_timer = 1 + + if not self._nearby_wlans: + self._iface.disconnect() + + if not self._nearby_wlans: + self._scan_timer -= delta_ms / 1000 + + if ins.captouch.petals[0].pressed: + if not self._petal_pressed.get(0, False): + self._iface.active(not self._iface.active()) + if not self._iface.active(): + self._nearby_wlans = [] + else: + self._status_text = "scanning" + self._petal_pressed[0] = True + else: + self._petal_pressed[0] = False + + if self._iface.active(): + self.set_direction_leds(0, 0, 1, 0) + else: + self.set_direction_leds(0, 1, 0, 0) + self._status_text = "wlan off" + + if ( + self.input.buttons.app.middle.pressed + and self._iface.active() + and self._nearby_wlans + ): + hovered_network = self._nearby_wlans[self._wlan_offset] + ssid = hovered_network[0].decode() + if self._iface.isconnected(): + self._iface.disconnect() + # network[4] = security level, 0 = open + if ssid in self._wifi_config["networks"] or hovered_network[4] == 0: + self.connect_wifi(ssid) + else: + self._waiting_for_password = True + self.vm.push(KeyboardView(self._password_model)) + + if self._waiting_for_password and ( + not self.vm._history or not isinstance(self.vm._history[-1], WifiApp) + ): + ssid = self._nearby_wlans[self._wlan_offset][0].decode() + psk = self._password_model.text + print(ssid, psk) + self.connect_wifi(ssid, psk) + self._password_model = TextInputModel("") + self._waiting_for_password = False + + if self._is_connecting: + self._connection_timer -= delta_ms / 1000 + if self._iface.isconnected(): + self._connection_timer = 10 + self._is_connecting = False + + if self._current_ssid: + self.update_settings_json(self._current_ssid, self._current_psk) + if self._current_ssid not in self._wifi_config["networks"]: + self.add_to_config_json(self._current_ssid, self._current_psk) + elif self._connection_timer <= 0: + self._iface.disconnect() + self._status_text = "conn timed out" + self._is_connecting = False + + if self._iface.isconnected(): + self._status_text = "connected" + + leds.update() + + +# For running with `mpremote run`: +if __name__ == "__main__": + import st3m.run + + st3m.run.run_view(WifiApp(ApplicationContext())) diff --git a/python_payload/apps/w1f1/flow3r.toml b/python_payload/apps/w1f1/flow3r.toml new file mode 100644 index 0000000000..34342edd64 --- /dev/null +++ b/python_payload/apps/w1f1/flow3r.toml @@ -0,0 +1,13 @@ +[app] +name = "WiFi" +menu = "Hidden" + +[entry] +class = "WifiApp" + +[metadata] +author = "ave" +license = "LGPL-3.0-only" +url = "https://git.flow3r.garden/flow3r/flow3r-firmware" +description = "Lets you use multiple wireless networks." +version = 4 diff --git a/python_payload/apps/w1f1/k3yboard.py b/python_payload/apps/w1f1/k3yboard.py new file mode 100644 index 0000000000..f6ba232247 --- /dev/null +++ b/python_payload/apps/w1f1/k3yboard.py @@ -0,0 +1,549 @@ +# code from https://git.flow3r.garden/baldo/k3yboard +# LGPL-v3-only, by baldo, 2023 + +from ctx import Context + +import st3m.run +from st3m import Responder, InputState +from st3m.application import Application, ApplicationContext +from st3m.goose import ABCBase, Enum +from st3m.ui.view import BaseView, ViewManager +from st3m.utils import tau + + +class Model(ABCBase): + """ + Common base-class for models holding state that can be used for rendering views. + """ + + def __init__(self): + pass + + +class TextInputModel(Model): + """ + Model used for rendering the TextInputView. Holds the current input. + + The input is split into the part left and the part right of the cursor. The input candidate is a character that + might be added to the input next and is displayed at the position of the cursor. This is used to give viusal + feedback while toggling through multiple characters inside a input group (associated with a petal). + """ + + class CursorDirection(Enum): + LEFT = -1 + RIGHT = 1 + + def __init__( + self, input_left: str = "", input_right: str = "", input_candidate: str = "" + ) -> None: + super().__init__() + + self._input_left = input_left + self._input_right = input_right + self.input_candidate = input_candidate + + @property + def text(self) -> str: + """ + The complete input string in its current state. + """ + return self._input_left + self._input_right + + @property + def input_left(self) -> str: + return self._input_left + + @property + def input_right(self) -> str: + return self._input_right + + def move_cursor(self, direction: self.CursorDirection) -> None: + """ + Moves the cursor one step in specified direction. Any pending input will be committed beforehand. + """ + self.commit_input() + + if direction == self.CursorDirection.LEFT: + self._input_right = self._input_left[-1:] + self._input_right + self._input_left = self._input_left[:-1] + + elif direction == self.CursorDirection.RIGHT: + self._input_left = self._input_left + self._input_right[0:1] + self._input_right = self._input_right[1:] + + def add_input_character(self, char: str) -> None: + """ + Adds an input character at the current cursor position. + """ + self._input_left += char + self.input_candidate = "" + + def delete_input_character(self) -> None: + """ + Deletes the character left to the cursor (if any). + + If an input candidate is pending, it will be removed instead. + """ + if self.input_candidate == "": + self._input_left = self._input_left[:-1] + + self.input_candidate = "" + + def commit_input(self) -> None: + """ + Adds the pending input candidate (if any) to input left of the cursor. + """ + self.add_input_character(self.input_candidate) + + +class TextInputFieldView(Responder): + """ + Displays the current text input and cursor of a keyboard. + """ + + def __init__( + self, model: TextInputModel, cursor_blink_duration_ms: float = 500 + ) -> None: + super().__init__() + + self._model = model + + self._cursor_blink_duration_ms = cursor_blink_duration_ms + self._cursor_timer_ms = 0 + + def draw_cursor(self, ctx: Context) -> None: + if self._cursor_blink_duration_ms < self._cursor_timer_ms: + return + + ctx.begin_path() + ctx.rgb(0.0, 0.2, 1.0).rectangle(-1.0, -15.0, 2.0, 28.0).fill() + ctx.close_path() + + def draw_text_input(self, ctx: Context) -> None: + ctx.begin_path() + + cursor_offset = 1.5 + + ctx.text_baseline = ctx.MIDDLE + ctx.font_size = 24 + + left_input_width = ctx.text_width(self._model.input_left) + input_candidate_width = ctx.text_width(self._model.input_candidate) + right_input_width = ctx.text_width(self._model.input_right) + + ctx.gray(0.2).rectangle( + -1.0 - cursor_offset - input_candidate_width - left_input_width, + ctx.font_size / 2.4, + left_input_width + + input_candidate_width + + right_input_width + + 2 * cursor_offset + + 2 * 1.0, + 2, + ).fill() + + ctx.text_align = ctx.END + ctx.gray(1.0).move_to(-cursor_offset - input_candidate_width, 0).text( + self._model.input_left + ) + + ctx.text_align = ctx.END + ctx.rgb(0.0, 0.2, 1.0).move_to(-cursor_offset, 0).text( + self._model.input_candidate + ) + + ctx.text_align = ctx.START + ctx.gray(1.0).move_to(cursor_offset, 0).text(self._model.input_right) + + ctx.close_path() + + def draw(self, ctx: Context) -> None: + ctx.begin_path() + + self.draw_text_input(ctx) + self.draw_cursor(ctx) + + ctx.close_path() + + def think(self, ins: InputState, delta_ms: int) -> None: + self._cursor_timer_ms = (self._cursor_timer_ms + delta_ms) % ( + 2 * self._cursor_blink_duration_ms + ) + + +class InputControlsModel(Model): + """ + Model used for rendering the InputControlsView. + + Holds the current active input and control groups (icons displayed in a ring around the edge of the screen). + + Input groups are groups of characters to toggle through to select the character to input. + + Control groups are groups of icons to control the behaviour of the keyboard. + """ + + def __init__( + self, + input_groups: list[list[str]], + control_groups: list[list[str]], + active_input_group: int = 0, + ) -> None: + super().__init__() + + self._input_groups = input_groups + self._control_groups = control_groups + self._active_input_control_group = active_input_group + + @property + def active_input_control_group(self): + return self._active_input_control_group + + def select_input_control_group(self, group: int) -> None: + self._active_input_control_group = group + + @property + def active_input_groups(self) -> list[str]: + return self._input_groups[self._active_input_control_group] + + @property + def active_control_groups(self) -> list[str]: + return self._control_groups[self._active_input_control_group] + + +class InputControlsView(Responder): + """ + Shows a ring of controls and input characters to choose from around the edge. + """ + + def __init__(self, model: InputControlsModel) -> None: + super().__init__() + + self._model = model + + def draw_input_group(self, ctx: Context, group: int) -> None: + inputs = self._model.active_input_groups[group] + + ctx.begin_path() + + bottom_input_group = 2 <= group <= 3 + + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + + ctx.font_size = 18.0 + ctx.gray(0.6) + + angle_offset = tau / 5.0 / 10 + angle = group * tau / 5.0 - angle_offset * (len(inputs) - 1) / 2.0 + + for input in reversed(inputs) if bottom_input_group else inputs: + ctx.save() + ctx.rotate(angle) + ctx.translate(0, -109) + + if bottom_input_group: + ctx.rotate(tau / 2) + + ctx.move_to(0, 0) + ctx.text(input) + ctx.restore() + + angle += angle_offset + + ctx.close_path() + + def draw_control_group(self, ctx, group: int) -> None: + controls = self._model.active_control_groups[group] + + ctx.begin_path() + + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + + ctx.font_size = 18.0 + ctx.gray(0.6) + + ctx.font = "Material Icons" + + angle_offset = tau / 5.0 / 10 + angle = (group + 0.5) * tau / 5.0 - angle_offset * (len(controls) - 1) / 2.0 + + for control in controls: + ctx.save() + ctx.rotate(angle) + ctx.translate(0, -109) + + ctx.rotate(-angle) + + ctx.move_to(0, 0) + ctx.text(control) + ctx.restore() + + angle += angle_offset + + ctx.close_path() + + def draw(self, ctx: Context) -> None: + ctx.begin_path() + + ctx.line_width = 4.0 + ctx.gray(0).arc(0, 0, 98, 0, tau, 0).stroke() + + ctx.line_width = 24.0 + ctx.gray(0.1).arc(0, 0, 110, 0, tau, 0).stroke() + + ctx.line_width = 2.0 + ctx.gray(0.3).arc(0, 0, 99, 0, tau, 0).stroke() + + for i in range(0, len(self._model.active_input_groups)): + self.draw_input_group(ctx, i) + + for i in range(0, len(self._model.active_control_groups)): + self.draw_control_group(ctx, i) + + ctx.close_path() + + def think(self, ins: InputState, delta_ms: int) -> None: + pass + + +class KeyboardView(BaseView): + class ControlPetal(Enum): + SPECIAL_CHARACTERS = 1 + BACKSPACE = 3 + SPACE = 5 + CAPSLOCK = 7 + NUMLOCK = 9 + + class InputControlGroup(Enum): + LOWERCASE_LETTERS = 0 + UPPERCASE_LETTERS = 1 + NUMBERS = 2 + SPECIAL_CHARACTERS = 3 + + def __init__(self, model: TextInputModel) -> None: + super().__init__() + + self._last_input_group_press = -1 + self._last_input_group_character = -1 + self._time_since_last_input_group_press = 0 + self._input_group_timeout = 1000 + + self._text_input_model = model + self._text_input_view = TextInputFieldView(self._text_input_model) + + self._input_controls_model = InputControlsModel( + [ + [ + "fghij", + "klmno", + "uvwxyz", + "pqrst", + "abcde", + ], + [ + "FGHIJ", + "KLMNO", + "UVWXYZ", + "PQRST", + "ABCDE", + ], + [ + "34", + "56", + "90", + "78", + "12", + ], + [ + ".,!?:;", + "'\"@#~", + "%$&<>\\", + "+-*/=", + "()[]{}", + ], + ], + [ + [ + "\ue9ef", # Special characters + "\ue14a", # Backspace + "\ue256", # Space + "\ue5ce", # Shift + "\ue400", # Num + ], + [ + "\ue9ef", # Special characters + "\ue14a", # Backspace + "\ue256", # Space + "\ue5cf", # Shift active + "\ue400", # Num + ], + [ + "\ue9ef", # Special characters + "\ue14a", # Backspace + "\ue256", # Space + "\ue264", # Text + "", + ], + [ + "", + "\ue14a", # Backspace + "\ue256", # Space + "\ue264", # Text + "\ue400", # Num + ], + ], + ) + self._input_controls_view = InputControlsView(self._input_controls_model) + + def on_enter(self, vm: Optional[ViewManager]) -> None: + super().on_enter(vm) + + def on_exit(self) -> None: + super().on_exit() + self.reset_input_state() + + def draw(self, ctx: Context) -> None: + ctx.begin_path() + + ctx.gray(0).rectangle(-120, -120, 240, 240).fill() + + self._text_input_view.draw(ctx) + self._input_controls_view.draw(ctx) + + ctx.close_path() + + def reset_input_state(self) -> None: + self._time_since_last_input_group_press = 0 + self._last_input_group_press = -1 + self._last_input_group_character = -1 + self._text_input_model.input_candidate = "" + + def add_input_character(self, char: str) -> None: + self._text_input_model.commit_input() + self._text_input_model.add_input_character(char) + self.reset_input_state() + + def delete_input_character(self) -> None: + self._text_input_model.commit_input() + self._text_input_model.delete_input_character() + self.reset_input_state() + + def commit_input(self) -> None: + self._text_input_model.commit_input() + self.reset_input_state() + + def select_input_group(self, input_group) -> None: + self.commit_input() + self._input_controls_model.select_input_control_group(input_group) + self.reset_input_state() + + def handle_shoulder_buttons(self) -> None: + if self.input.buttons.app.middle.pressed: + self.commit_input() + if self.vm: + self.vm.pop() + + if self.input.buttons.app.left.pressed: + self._text_input_model.move_cursor(TextInputModel.CursorDirection.LEFT) + + if self.input.buttons.app.right.pressed: + self._text_input_model.move_cursor(TextInputModel.CursorDirection.RIGHT) + + def handle_control_inputs(self) -> None: + if self.input.captouch.petals[ + self.ControlPetal.SPECIAL_CHARACTERS + ].whole.pressed: + self.select_input_group(self.InputControlGroup.SPECIAL_CHARACTERS) + + if self.input.captouch.petals[self.ControlPetal.BACKSPACE].whole.pressed: + self.delete_input_character() + + if self.input.captouch.petals[self.ControlPetal.SPACE].whole.pressed: + self.add_input_character(" ") + + if self.input.captouch.petals[self.ControlPetal.CAPSLOCK].whole.pressed: + self.select_input_group( + self.InputControlGroup.UPPERCASE_LETTERS + if self._input_controls_model.active_input_control_group + == self.InputControlGroup.LOWERCASE_LETTERS + else self.InputControlGroup.LOWERCASE_LETTERS + ) + + if self.input.captouch.petals[self.ControlPetal.NUMLOCK].whole.pressed: + self.select_input_group(self.InputControlGroup.NUMBERS) + + def handle_input_groups(self, delta_ms: int) -> bool: + for i in range(0, 5): + if self.input.captouch.petals[i * 2].whole.pressed: + if ( + self._last_input_group_press >= 0 + and self._last_input_group_press != i + ): + self.commit_input() + + self._last_input_group_press = i + self._last_input_group_character = ( + self._last_input_group_character + 1 + ) % len(self._input_controls_model.active_input_groups[i]) + + self._time_since_last_input_group_press = 0 + self._text_input_model.input_candidate = ( + self._input_controls_model.active_input_groups[i][ + self._last_input_group_character + ] + ) + + return + + self._time_since_last_input_group_press += delta_ms + + if ( + self._last_input_group_press >= 0 + and self._time_since_last_input_group_press > self._input_group_timeout + ): + self.commit_input() + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) + self._text_input_view.think(ins, delta_ms) + self._input_controls_view.think(ins, delta_ms) + + self.handle_shoulder_buttons() + self.handle_control_inputs() + self.handle_input_groups(delta_ms) + + +class KeyboardDemoApp(Application): + def __init__(self, app_ctx: ApplicationContext) -> None: + super().__init__(app_ctx) + + self._model = TextInputModel("Hello world!") + + def draw(self, ctx: Context) -> None: + ctx.gray(0).rectangle(-120, -120, 240, 240).fill() + + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + + ctx.font_size = 24 + ctx.gray(1).move_to(0, -50).text("Keyboard Demo") + + ctx.font_size = 16 + ctx.gray(1).move_to(0, -20).text("Press left button to edit") + + ctx.font_size = 24 + ctx.gray(1).move_to(0, 20).text("Current input:") + + ctx.font_size = 16 + ctx.gray(1).move_to(0, 50).text(self._model.text) + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) # Let Application do its thing + + if self.input.buttons.app.middle.pressed: + self.vm.push(KeyboardView(self._model)) + + +if __name__ == "__main__": + st3m.run.run_view(KeyboardDemoApp(ApplicationContext())) diff --git a/python_payload/st3m/settings.py b/python_payload/st3m/settings.py index 9b2daf5da3..e140526910 100644 --- a/python_payload/st3m/settings.py +++ b/python_payload/st3m/settings.py @@ -24,6 +24,7 @@ from st3m.goose import ( TYPE_CHECKING, ) from st3m.ui.menu import MenuController, MenuItem, MenuItemBack, MenuItemForeground +from st3m.application import BundleMetadata, MenuItemAppLaunch from st3m.ui.elements.menus import SimpleMenu from st3m.ui.view import ViewManager from st3m.utils import lerp, ease_out_cubic, reduce @@ -106,7 +107,7 @@ class TunableWidget(Responder): class UnaryTunable(Tunable): """ - Basic implementation of a Tunable for single values. Most settings will are + Basic implementation of a Tunable for single values. Most settings will be UnaryTunables, with notable exceptions being things like lists or optional settings. @@ -382,7 +383,7 @@ onoff_button_swap = OnOffTunable("Swap Buttons", "system.swap_buttons", False) onoff_debug = OnOffTunable("Debug Overlay", "system.debug", False) onoff_debug_touch = OnOffTunable("Touch Overlay", "system.debug_touch", False) onoff_show_tray = OnOffTunable("Show Icons", "system.show_icons", True) -onoff_wifi = OnOffTunable("Enable WiFi", "system.wifi.enabled", False) +onoff_wifi = OnOffTunable("Enable WiFi on Boot", "system.wifi.enabled", False) str_wifi_ssid = StringTunable("WiFi SSID", "system.wifi.ssid", "Camp2023-open") str_wifi_psk = ObfuscatedStringTunable("WiFi Password", "system.wifi.psk", None) str_hostname = StringTunable("Hostname", "system.hostname", "flow3r") @@ -403,21 +404,14 @@ if TYPE_CHECKING: MenuStructureEntry = Union[UnaryTunable, Tuple[str, List["MenuStructureEntry"]]] MenuStructure = List[MenuStructureEntry] -# WiFi submenu -wifi_settings: "MenuStructure" = [ - onoff_wifi, - str_wifi_ssid, - str_wifi_psk, - str_hostname, -] - # Main settings menu settings_menu_structure: "MenuStructure" = [ onoff_show_tray, onoff_button_swap, onoff_debug, onoff_debug_touch, - ("WiFi", wifi_settings), + onoff_wifi, + MenuItemAppLaunch(BundleMetadata("/flash/sys/apps/w1f1")), ] @@ -474,12 +468,7 @@ def build_menu_recursive(items: "MenuStructure") -> SimpleMenu: mib: MenuItem = MenuItemBack() positions: List[MenuItem] = [ mib, - ] + [ - SettingsMenuItem(t) - if isinstance(t, UnaryTunable) - else MenuItemForeground(t[0], build_menu_recursive(t[1])) - for t in items - ] + ] + [SettingsMenuItem(t) if isinstance(t, UnaryTunable) else t for t in items] return SettingsMenu(positions) diff --git a/python_payload/st3m/wifi.py b/python_payload/st3m/wifi.py index abc31a6628..f07ca3690a 100644 --- a/python_payload/st3m/wifi.py +++ b/python_payload/st3m/wifi.py @@ -15,6 +15,7 @@ def setup_wifi() -> None: assert iface try: if settings.str_wifi_ssid.value: + iface.disconnect() iface.connect(settings.str_wifi_ssid.value, settings.str_wifi_psk.value) except OSError as e: log.error(f"Could not connect to wifi: {e}") -- GitLab