diff --git a/components/micropython/usermodule/mp_sys_buttons.c b/components/micropython/usermodule/mp_sys_buttons.c
index 76a87dc03b8ff083dea48e2750c19ec0ef54eb85..0db6816a22bd851180bb286284f8bc24e1e95cce 100644
--- a/components/micropython/usermodule/mp_sys_buttons.c
+++ b/components/micropython/usermodule/mp_sys_buttons.c
@@ -18,21 +18,35 @@
 
 #include "mp_uctx.h"
 
-STATIC mp_obj_t mp_get_left() {
-    return mp_obj_new_int(st3m_io_left_button_get());
+STATIC mp_obj_t mp_get_app(void) {
+    return mp_obj_new_int(st3m_io_app_button_get());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_left_obj, mp_get_left);
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_app_obj, mp_get_app);
 
-STATIC mp_obj_t mp_get_right() {
-    return mp_obj_new_int(st3m_io_right_button_get());
+STATIC mp_obj_t mp_get_os(void) {
+    return mp_obj_new_int(st3m_io_os_button_get());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_right_obj, mp_get_right);
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_os_obj, mp_get_os);
+
+STATIC mp_obj_t mp_configure(mp_obj_t left_in) {
+    bool left = mp_obj_is_true(left_in);
+    st3m_io_app_button_configure(left);
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_configure_obj, mp_configure);
+
+STATIC mp_obj_t mp_app_is_left(void) {
+    return mp_obj_new_bool(st3m_io_app_button_is_left());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_app_is_left_obj, mp_app_is_left);
 
 STATIC const mp_rom_map_elem_t mp_module_sys_buttons_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_buttons) },
 
-    { MP_ROM_QSTR(MP_QSTR_get_left), MP_ROM_PTR(&mp_get_left_obj) },
-    { MP_ROM_QSTR(MP_QSTR_get_right), MP_ROM_PTR(&mp_get_right_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_app), MP_ROM_PTR(&mp_get_app_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_os), MP_ROM_PTR(&mp_get_os_obj) },
+    { MP_ROM_QSTR(MP_QSTR_configure), MP_ROM_PTR(&mp_configure_obj) },
+    { MP_ROM_QSTR(MP_QSTR_app_is_left), MP_ROM_PTR(&mp_app_is_left_obj) },
 
     { MP_ROM_QSTR(MP_QSTR_PRESSED_LEFT), MP_ROM_INT(st3m_tripos_left) },
     { MP_ROM_QSTR(MP_QSTR_PRESSED_RIGHT), MP_ROM_INT(st3m_tripos_right) },
diff --git a/components/st3m/st3m_io.c b/components/st3m/st3m_io.c
index ed82fbb6115ceb63c88464bed1856fe9d4e88120..e9c7a62a257584f91898698a12830bc8579760af 100644
--- a/components/st3m/st3m_io.c
+++ b/components/st3m/st3m_io.c
@@ -5,12 +5,16 @@ static const char *TAG = "st3m-io";
 #include "esp_err.h"
 #include "esp_log.h"
 #include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
 #include "freertos/task.h"
 
 #include "flow3r_bsp.h"
 #include "flow3r_bsp_i2c.h"
 #include "st3m_audio.h"
 
+static bool _app_button_left = true;
+static SemaphoreHandle_t _mu = NULL;
+
 static void _update_button_state() {
     esp_err_t ret = flow3r_bsp_spio_update();
     if (ret != ESP_OK) {
@@ -18,23 +22,35 @@ static void _update_button_state() {
     }
 }
 
-void init_buttons() {
-    esp_err_t ret = flow3r_bsp_spio_init();
-    if (ret != ESP_OK) {
-        ESP_LOGE(TAG, "init failed: %s", esp_err_to_name(ret));
-        for (;;) {
-        }
-    }
+bool st3m_io_charger_state_get() { return flow3r_bsp_spio_charger_state_get(); }
+
+void st3m_io_app_button_configure(bool left) {
+    xSemaphoreTake(_mu, portMAX_DELAY);
+    _app_button_left = left;
+    xSemaphoreGive(_mu);
 }
 
-bool st3m_io_charger_state_get() { return flow3r_bsp_spio_charger_state_get(); }
+bool st3m_io_app_button_is_left(void) {
+    xSemaphoreTake(_mu, portMAX_DELAY);
+    bool res = _app_button_left;
+    xSemaphoreGive(_mu);
+    return res;
+}
 
-st3m_tripos st3m_io_left_button_get() {
-    return flow3r_bsp_spio_left_button_get();
+st3m_tripos st3m_io_app_button_get() {
+    if (st3m_io_app_button_is_left()) {
+        return flow3r_bsp_spio_left_button_get();
+    } else {
+        return flow3r_bsp_spio_right_button_get();
+    }
 }
 
-st3m_tripos st3m_io_right_button_get() {
-    return flow3r_bsp_spio_right_button_get();
+st3m_tripos st3m_io_os_button_get() {
+    if (st3m_io_app_button_is_left()) {
+        return flow3r_bsp_spio_right_button_get();
+    } else {
+        return flow3r_bsp_spio_left_button_get();
+    }
 }
 
 static uint8_t badge_link_enabled = 0;
@@ -102,6 +118,10 @@ static void _task(void *data) {
 }
 
 void st3m_io_init(void) {
+    assert(_mu == NULL);
+    _mu = xSemaphoreCreateMutex();
+    assert(_mu != NULL);
+
     esp_err_t ret = flow3r_bsp_spio_init();
     if (ret != ESP_OK) {
         ESP_LOGE(TAG, "spio init failed: %s", esp_err_to_name(ret));
diff --git a/components/st3m/st3m_io.h b/components/st3m/st3m_io.h
index 5cf59bf15b4d053e79671aa5bcd1bb4b5956c189..9a250a0807823a1e3a1e617ee9dd878fad910f36 100644
--- a/components/st3m/st3m_io.h
+++ b/components/st3m/st3m_io.h
@@ -19,11 +19,20 @@ typedef enum {
     st3m_tripos_right = 1,
 } st3m_tripos;
 
-/* Read the state of the left/right button.
- * This ignores user preference and should be used only with good reason.
+/* Configure whether the app button is on the left (default) or on the right.
  */
-st3m_tripos st3m_io_left_button_get();
-st3m_tripos st3m_io_right_button_get();
+void st3m_io_app_button_configure(bool left);
+
+/* Returns true if the app button is on the left (default), false otherwise.
+ */
+bool st3m_io_app_button_is_left(void);
+
+/* Read the state of the application and OS buttons. By default, the application
+ * button is on the left and the OS button is on the right. However, the user
+ * can change that preference - see st3m_io_app_button_configure.
+ */
+st3m_tripos st3m_io_app_button_get();
+st3m_tripos st3m_io_os_button_get();
 
 #define BADGE_LINK_PIN_MASK_LINE_IN_TIP 0b0001
 #define BADGE_LINK_PIN_MASK_LINE_IN_RING 0b0010
@@ -61,4 +70,4 @@ uint8_t st3m_io_badge_link_enable(uint8_t pin_mask);
 
 /* Returns true if the battery is currently being charged.
  */
-bool st3m_io_charger_state_get();
+bool st3m_io_charger_state_get();
\ No newline at end of file
diff --git a/components/st3m/st3m_mode.c b/components/st3m/st3m_mode.c
index 2b226128ff5847c9877367dff11bc51d66c2f692..ed703bf0a3b578d3406c5b39b2ec3edc43319803 100644
--- a/components/st3m/st3m_mode.c
+++ b/components/st3m/st3m_mode.c
@@ -186,7 +186,7 @@ static void _task(void *arg) {
         st3m_mode_update_display(&restartable);
 
         if (restartable) {
-            st3m_tripos tp = st3m_io_right_button_get();
+            st3m_tripos tp = st3m_io_os_button_get();
             if (tp == st3m_tripos_mid) {
                 st3m_gfx_textview_t tv = {
                     .title = "Restarting...",
diff --git a/python_payload/mypystubs/sys_buttons.pyi b/python_payload/mypystubs/sys_buttons.pyi
index 4b3346a2106c0ceac5d9fa6744dbe5826e7024e1..047ba764c48a5390f8048816dac57a1f7ffd2c94 100644
--- a/python_payload/mypystubs/sys_buttons.pyi
+++ b/python_payload/mypystubs/sys_buttons.pyi
@@ -1,5 +1,7 @@
-def get_left() -> int: ...
-def get_right() -> int: ...
+def get_app() -> int: ...
+def get_os() -> int: ...
+def configure(left: bool) -> None: ...
+def app_is_left() -> bool: ...
 
 PRESSED_LEFT: int
 PRESSED_RIGHT: int
diff --git a/python_payload/st3m/input.py b/python_payload/st3m/input.py
index 27db554c36de0d75b132d1f553cf858a26bfaeb5..8a6faaf82c15a4559c7b9c7818d4edb60ec80b75 100644
--- a/python_payload/st3m/input.py
+++ b/python_payload/st3m/input.py
@@ -58,17 +58,10 @@ class InputButtonState:
     PRESSED_DOWN = sys_buttons.PRESSED_DOWN
     NOT_PRESSED = sys_buttons.NOT_PRESSED
 
-    def __init__(self, left: int, right: int, swapped: bool):
-        app = left
-        os = right
-        if swapped:
-            app, os = os, app
-
+    def __init__(self, app: int, os: int, app_is_left: bool):
         self.app = app
         self.os = os
-        self._left = left
-        self._right = right
-        self.app_is_left = not swapped
+        self.app_is_left = app_is_left
 
 
 class InputState:
@@ -95,15 +88,16 @@ class InputState:
         self.battery_voltage = battery_voltage
 
     @classmethod
-    def gather(cls, swapped_buttons: bool = False) -> "InputState":
+    def gather(cls) -> "InputState":
         """
         Build InputState from current hardware state. Should only be used by the
         Reactor.
         """
         cts = captouch.read()
-        left = sys_buttons.get_left()
-        right = sys_buttons.get_right()
-        buttons = InputButtonState(left, right, swapped_buttons)
+        app = sys_buttons.get_app()
+        os = sys_buttons.get_os()
+        app_is_left = sys_buttons.app_is_left()
+        buttons = InputButtonState(app, os, app_is_left)
 
         acc = imu.acc_read()
         gyro = imu.gyro_read()
diff --git a/python_payload/st3m/reactor.py b/python_payload/st3m/reactor.py
index 505c6096cc4730705cad17c6fb3de924434efe7b..55bc1108676be2b90362c65b1842d9bf843198f6 100644
--- a/python_payload/st3m/reactor.py
+++ b/python_payload/st3m/reactor.py
@@ -84,7 +84,6 @@ class Reactor:
         "_ctx",
         "_ts",
         "_last_ctx_get",
-        "_swap_buttons",
         "stats",
     )
 
@@ -95,7 +94,6 @@ class Reactor:
         self._last_tick: Optional[int] = None
         self._last_ctx_get: Optional[int] = None
         self._ctx: Optional[Context] = None
-        self._swap_buttons = False
         self.stats = ReactorStats()
 
     def set_top(self, top: Responder) -> None:
@@ -114,9 +112,6 @@ class Reactor:
         while True:
             self._run_once()
 
-    def set_buttons_swapped(self, swapped: bool) -> None:
-        self._swap_buttons = swapped
-
     def _run_once(self) -> None:
         start = time.ticks_ms()
         deadline = start + self._tickrate_ms
@@ -143,7 +138,7 @@ class Reactor:
 
         self._ts += delta
 
-        hr = InputState.gather(self._swap_buttons)
+        hr = InputState.gather()
 
         # Think!
         self._top.think(hr, delta)
diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py
index fcd663769af431e5aab3a429ece370483cb726a1..6ba18204a85b73a65785bd87dd441b7e033460df 100644
--- a/python_payload/st3m/run.py
+++ b/python_payload/st3m/run.py
@@ -20,7 +20,7 @@ from st3m.application import (
 from st3m.about import About
 from st3m import settings, logging, processors, wifi
 
-import captouch, audio, leds, gc
+import captouch, audio, leds, gc, sys_buttons
 import os
 
 import machine
@@ -37,7 +37,8 @@ def _make_reactor() -> Reactor:
     reactor = Reactor()
 
     def _onoff_button_swap_update() -> None:
-        reactor.set_buttons_swapped(settings.onoff_button_swap.value)
+        left = not settings.onoff_button_swap.value
+        sys_buttons.configure(left)
 
     settings.onoff_button_swap.subscribe(_onoff_button_swap_update)
     _onoff_button_swap_update()
diff --git a/sim/fakes/sys_buttons.py b/sim/fakes/sys_buttons.py
index 46e4aef28dc6d13f5490367c4459ef99d57d3701..0d340af7dfe62889f5e36add86e1081ed9fc8068 100644
--- a/sim/fakes/sys_buttons.py
+++ b/sim/fakes/sys_buttons.py
@@ -1,12 +1,29 @@
 import _sim
 
+_app_is_left = True
 
-def get_left():
-    return _sim.get_button_state(1)
 
+def get_app():
+    if _app_is_left:
+        return _sim.get_button_state(1)
+    else:
+        return _sim.get_button_state(0)
 
-def get_right():
-    return _sim.get_button_state(0)
+
+def get_os():
+    if _app_is_left:
+        return _sim.get_button_state(0)
+    else:
+        return _sim.get_button_state(1)
+
+
+def app_is_left():
+    return _app_is_left
+
+
+def configure(left):
+    global _app_is_left
+    _app_is_left = left
 
 
 PRESSED_LEFT = -1